Skip to content
This repository was archived by the owner on Sep 8, 2026. It is now read-only.

Commit bd93524

Browse files
aid-ninjaclaude
andcommitted
trace v0: address review — strict shape, metadata hatch, fail-closed write
- Schema top-level is now additionalProperties:false (unknown keys rejected, catches drift); additive/experimental fields go under a documented `metadata` object hatch (emitted as {}). - bean-run fails CLOSED on a trace-write failure: prints the outcome to stderr and exits 3 (infra/load error, distinct from any convergence outcome) instead of reporting a clean run. - Conformance asserts metadata present + no unknown top-level keys; docs note the stability rules and that sample-trace fixtures belong in test/fixtures/traces/, never under the ignored .bean/runs/. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 7081dd1 commit bd93524

5 files changed

Lines changed: 69 additions & 15 deletions

File tree

CHANGELOG.md

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,14 @@
55
- **Trace artifact v0 (emit-only).** Every `bean-run` now writes a stable post-run record to
66
`.bean/runs/<run_id>.json`: `schema_version`, `run_id`, `goal`, `started_at`/`ended_at`,
77
`status`, `certificate`, `rounds`, `pivot_count`, `blockers_opened`/`blockers_closed`,
8-
`verifier_verdicts`, `residuals`, `artifacts_changed`. One file per run (not a rolling file),
9-
so future tooling can analyze a corpus of runs without scraping transcripts. Schema:
8+
`verifier_verdicts`, `residuals`, `artifacts_changed`, `metadata`. One file per run (not a
9+
rolling file), so future tooling can analyze a corpus of runs without scraping transcripts.
10+
The top-level shape is **fixed** (`additionalProperties: false`); additive fields go under the
11+
`metadata` hatch. Emission **fails closed**: if the trace can't be written, `bean-run` exits
12+
with the infra error code (`3`) rather than reporting a clean run. Schema:
1013
`schemas/trace.schema.json`; docs: `skills/bean/references/trace.md`; conformance asserts the
11-
shape is stable. **Scope:** this does NOT make bean learn across tasks — no memory mutation,
12-
prompt rewriting, or cross-run optimization. It only emits the artifact.
14+
shape is stable (required keys, no unknown top-level keys). **Scope:** this does NOT make bean
15+
learn across tasks — no memory mutation, prompt rewriting, or cross-run optimization.
1316

1417
## 2.0.2 — driver "pivot, don't stop" + review hardening
1518

rs/src/bin/bean-run.rs

Lines changed: 30 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -482,13 +482,38 @@ fn main() {
482482
"verifier_verdicts": verifier_verdicts,
483483
"residuals": residuals,
484484
"artifacts_changed": Vec::<String>::new(),
485+
// documented extension hatch: additive/experimental fields go HERE, so the top-level
486+
// shape stays fixed (the schema rejects unknown top-level keys).
487+
"metadata": serde_json::Map::new(),
485488
});
489+
// Fail CLOSED on a trace-write failure: the trace is part of bean-run's contract, so we must
490+
// not report a clean exit (0/2/4) if we couldn't persist it. Surface the outcome on stderr,
491+
// then exit with the infra/load error code (3) — distinct from any convergence outcome.
486492
let runs_dir = bean_dir.join("runs");
487-
let _ = std::fs::create_dir_all(&runs_dir);
488-
let _ = std::fs::write(
489-
runs_dir.join(format!("{run_id}.json")),
490-
serde_json::to_string_pretty(&trace_artifact).unwrap() + "\n",
491-
);
493+
let trace_path = runs_dir.join(format!("{run_id}.json"));
494+
let wrote = std::fs::create_dir_all(&runs_dir).and_then(|_| {
495+
std::fs::write(
496+
&trace_path,
497+
serde_json::to_string_pretty(&trace_artifact).unwrap() + "\n",
498+
)
499+
});
500+
if let Err(e) = wrote {
501+
eprintln!(
502+
"bean-run: run outcome was {} (cert {}) but the trace could not be persisted",
503+
outcome,
504+
final_sig
505+
.get("certificate")
506+
.and_then(|v| v.as_str())
507+
.unwrap_or("")
508+
);
509+
die(
510+
3,
511+
&format!(
512+
"could not write trace artifact {}: {e} — failing closed",
513+
trace_path.display()
514+
),
515+
);
516+
}
492517

493518
let report = serde_json::json!({
494519
"outcome": outcome,

schemas/trace.schema.json

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
"$schema": "http://json-schema.org/draft-07/schema#",
33
"$id": "https://github.com/grainulation/bean/schemas/trace.schema.json",
44
"title": "bean run trace artifact (v0)",
5-
"description": "Stable post-run record emitted by bean-run to .bean/runs/<run_id>.json. v0 is an EMIT-ONLY artifact: it lets future tooling analyze runs without scraping transcripts. It does NOT make bean learn across tasks — no memory mutation, prompt rewriting, or cross-run optimization.",
5+
"description": "Stable post-run record emitted by bean-run to .bean/runs/<run_id>.json. v0 is an EMIT-ONLY artifact: it lets future tooling analyze runs without scraping transcripts. It does NOT make bean learn across tasks — no memory mutation, prompt rewriting, or cross-run optimization. The top-level shape is FIXED: unknown top-level keys are rejected; additive/experimental fields go under `metadata`.",
66
"type": "object",
77
"required": [
88
"schema_version",
@@ -20,7 +20,7 @@
2020
"residuals",
2121
"artifacts_changed"
2222
],
23-
"additionalProperties": true,
23+
"additionalProperties": false,
2424
"properties": {
2525
"schema_version": { "const": "trace/v0" },
2626
"run_id": {
@@ -89,6 +89,11 @@
8989
"type": "array",
9090
"description": "Files the run changed, when available. v0 does not track this yet; emitted as [] for shape stability.",
9191
"items": { "type": "string" }
92+
},
93+
"metadata": {
94+
"type": "object",
95+
"additionalProperties": true,
96+
"description": "Documented extension hatch. The top-level shape is fixed; additive or experimental fields go here so adding them never breaks v0 validation. Emitted as {} by default."
9297
}
9398
}
9499
}

skills/bean/references/trace.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,20 @@ Schema: [`schemas/trace.schema.json`](../../../schemas/trace.schema.json) (`sche
3131
| `verifier_verdicts` | The scrubbed verdicts from `.bean/verdicts/`, embedded verbatim. |
3232
| `residuals` | Claims tagged `residual`, each `{id, reason}` (reason = the claim content). |
3333
| `artifacts_changed` | Files the run changed, when available. v0 does not track this yet → `[]`. |
34+
| `metadata` | Extension hatch (`{}` by default). Additive/experimental fields go here. |
35+
36+
## Stability rules
37+
38+
- **The top-level shape is fixed.** The schema is `additionalProperties: false` — unknown
39+
top-level keys are rejected. This catches drift and typos. Any new field goes under
40+
`metadata` (the documented hatch), so adding it never breaks v0 validation. A breaking change
41+
to the fixed keys bumps `schema_version`.
42+
- **Emission fails closed.** If `bean-run` cannot persist the trace, it does NOT report a clean
43+
exit. It prints the run outcome to stderr and exits with the infra/load error code (`3`),
44+
distinct from any convergence outcome — the trace is part of the run's contract.
45+
- **Emitted traces are runtime artifacts, not source.** `**/.bean/runs/` (and
46+
`**/.bean/verdicts-raw/`) are gitignored. Do not commit emitted traces. Sample traces needed
47+
by tests live under `test/fixtures/traces/`, never under a `.bean/runs/` path (which is ignored).
3448

3549
## What consumes it (later, not now)
3650

test/conformance.mjs

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -321,7 +321,9 @@ console.log(`${dpass}/3 driver smoke checks pass`);
321321
const files = fs.existsSync(runsDir)
322322
? fs.readdirSync(runsDir).filter((f) => f.endsWith(".json"))
323323
: [];
324-
const REQUIRED = [
324+
// ALLOWED == the fixed top-level shape. metadata is the only extension hatch; any OTHER
325+
// unknown top-level key is drift and must fail (matches schema additionalProperties:false).
326+
const ALLOWED = [
325327
"schema_version",
326328
"run_id",
327329
"goal",
@@ -336,24 +338,29 @@ console.log(`${dpass}/3 driver smoke checks pass`);
336338
"verifier_verdicts",
337339
"residuals",
338340
"artifacts_changed",
341+
"metadata",
339342
];
340343
let ok = files.length === 1;
341344
let why = ok ? "" : `expected 1 trace file, got ${files.length}`;
342345
if (ok) {
343346
const t = JSON.parse(fs.readFileSync(path.join(runsDir, files[0]), "utf8"));
344-
const missing = REQUIRED.filter((k) => !(k in t));
347+
const missing = ALLOWED.filter((k) => !(k in t));
348+
const unknown = Object.keys(t).filter((k) => !ALLOWED.includes(k));
345349
const shapeOk =
346350
t.schema_version === "trace/v0" &&
347351
missing.length === 0 &&
352+
unknown.length === 0 &&
348353
files[0] === `${t.run_id}.json` &&
349354
t.status === report.outcome &&
350355
Array.isArray(t.verifier_verdicts) &&
351356
Array.isArray(t.residuals) &&
352-
Array.isArray(t.artifacts_changed);
357+
Array.isArray(t.artifacts_changed) &&
358+
typeof t.metadata === "object" &&
359+
!Array.isArray(t.metadata);
353360
ok = shapeOk;
354361
why = shapeOk
355362
? ""
356-
: `schema_version=${t.schema_version} missing=[${missing}] status=${t.status} vs ${report.outcome}`;
363+
: `schema_version=${t.schema_version} missing=[${missing}] unknown=[${unknown}] status=${t.status} vs ${report.outcome}`;
357364
}
358365
if (ok) {
359366
console.log(" ok trace artifact v0 written with stable shape");

0 commit comments

Comments
 (0)