@sabaiway/agent-workflow-kit 5.3.0 → 5.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (46) hide show
  1. package/CHANGELOG.md +138 -0
  2. package/README.md +2 -1
  3. package/SKILL.md +5 -1
  4. package/bridges/antigravity-cli-bridge/SKILL.md +1 -1
  5. package/bridges/antigravity-cli-bridge/bin/agy-review.sh +1 -1
  6. package/bridges/antigravity-cli-bridge/capability.json +1 -1
  7. package/bridges/codex-cli-bridge/SKILL.md +53 -5
  8. package/bridges/codex-cli-bridge/bin/codex-exec.sh +622 -30
  9. package/bridges/codex-cli-bridge/bin/codex-exec.test.mjs +731 -3
  10. package/bridges/codex-cli-bridge/bin/codex-review.sh +1 -1
  11. package/bridges/codex-cli-bridge/capability.json +15 -10
  12. package/bridges/codex-cli-bridge/references/sandbox-and-flags.md +16 -12
  13. package/capability.json +1 -1
  14. package/package.json +1 -1
  15. package/references/modes/core-evidence.md +1 -1
  16. package/references/modes/coverage-check.md +1 -1
  17. package/references/modes/dispatch.md +29 -0
  18. package/references/modes/gates.md +7 -2
  19. package/references/modes/receipt-deadline.md +3 -3
  20. package/references/modes/recommendations.md +3 -1
  21. package/references/modes/upgrade.md +1 -1
  22. package/references/modes/velocity.md +5 -1
  23. package/references/scripts/migrate-gates.mjs +102 -10
  24. package/references/scripts/migrate-gates.test.mjs +37 -0
  25. package/tools/commands.mjs +7 -0
  26. package/tools/core-evidence.mjs +79 -5
  27. package/tools/coverage-check.mjs +23 -7
  28. package/tools/coverage-producer.mjs +68 -0
  29. package/tools/coverage-state.mjs +24 -0
  30. package/tools/declared-paths.mjs +32 -0
  31. package/tools/detect-backends.mjs +5 -4
  32. package/tools/dispatch-record.mjs +10 -3
  33. package/tools/dispatch-store.mjs +392 -0
  34. package/tools/dispatch.mjs +1779 -0
  35. package/tools/doc-parity.mjs +27 -4
  36. package/tools/exec-producer.mjs +483 -0
  37. package/tools/exec-receipt.mjs +263 -0
  38. package/tools/flow-store.mjs +111 -462
  39. package/tools/gates-declaration.mjs +49 -0
  40. package/tools/gates-init.mjs +83 -6
  41. package/tools/receipt-deadline.mjs +25 -3
  42. package/tools/recommendations.mjs +63 -19
  43. package/tools/release-scan.mjs +33 -0
  44. package/tools/run-gates.mjs +111 -32
  45. package/tools/store-append.mjs +444 -0
  46. package/tools/velocity-profile.mjs +102 -23
@@ -5,7 +5,9 @@
5
5
  // COMPLETELY in one consented step: it REMOVES the canonical legacy entries, EXTENDS the
6
6
  // canonical `unit-tests` cmd with the built-in lcov reporters (the D3(d) coverage source), and
7
7
  // ADDS the coverage-check gate LAST (removal alone is not a migration — the result must satisfy
8
- // `run-gates --final` and carry a working commit path).
8
+ // `run-gates --final` and carry a working commit path). The checker is added ONLY over a
9
+ // declaration that PRODUCES the lcov it reads; with no producer it is WITHHELD with a loud
10
+ // warning, because a checker with no producer passes (`skipped-no-lcov`) while verifying nothing.
9
11
  //
10
12
  // Matching is by the DOCUMENTED cmd forms ONLY (hand-wired history included — never by seed
11
13
  // provenance): a legacy entry is `node <path>/review-ledger.mjs --check` or
@@ -45,12 +47,69 @@ export const LEGACY_FORMS = Object.freeze([
45
47
  { name: 'fold-completeness', re: legacyRe('fold-completeness\\.mjs') },
46
48
  ]);
47
49
 
48
- // The D3(d) reporter flags the canonical unit-tests cmd gains lcov at the fixed git-dir path
49
- // run-gates --final exports, plus an explicit stdout reporter (without it the lcov reporter
50
- // swallows the human TAP/spec stream).
50
+ // coverage-producer canon >>> BEGIN drift-guarded region
51
+ // Authored TWICE, byte-identically: in the memory substrate's references/scripts/migrate-gates.mjs
52
+ // and in the composition root's tools/coverage-producer.mjs. Neither side imports the other — the
53
+ // substrate is standalone and must not depend on the root, and the root must not import mirrored
54
+ // bytes — so a TEXT drift guard beside the root's copy holds them equal. Edit BOTH, then re-run the
55
+ // mirror sync.
56
+ //
57
+ // The destination is written against AW_GIT_DIR, which run-gates exports to every gate child on a
58
+ // plain run AND on --final (AW_LCOV_FILE is --final only), so one cmd survives the unmet
59
+ // producer-variable preflight in both modes. The explicit stdout reporter is not decoration:
60
+ // without it the lcov reporter swallows the human TAP/spec stream.
51
61
  export const UNIT_TESTS_COVERAGE_FLAGS =
52
62
  '--experimental-test-coverage --test-reporter=lcov --test-reporter-destination="$AW_GIT_DIR/agent-workflow-lcov.info" --test-reporter=spec --test-reporter-destination=stdout';
53
63
 
64
+ // The ONE suite body that produces that lcov with no extra dependency.
65
+ export const COVERAGE_PRODUCER_BODY = `node --test ${UNIT_TESTS_COVERAGE_FLAGS}`;
66
+
67
+ // The per-PM exec wrappers a fill offer puts that body behind. Recognition must cover every form
68
+ // the kit has EMITTED, so the prefixes are matched literally; gates-init's execCmdFor stays the one
69
+ // EMITTER and is bound to this list by a named acceptance test, never by a second grammar.
70
+ const PRODUCER_EXEC_PREFIXES = Object.freeze([
71
+ 'COREPACK_ENABLE_NETWORK=0 npm exec --offline --script-shell /bin/sh -- ',
72
+ 'COREPACK_ENABLE_NETWORK=0 pnpm exec -- ',
73
+ 'COREPACK_ENABLE_NETWORK=0 yarn exec -- ',
74
+ ]);
75
+
76
+ // A trailing suffix is the project's own test paths; a leading one would mean the body is not what
77
+ // this cmd runs. The tail passes a POSITIVE closed grammar — every whitespace-separated token must
78
+ // be path-shaped — never an operator blocklist: `node --test <flags> && rm -f <lcov>` runs the
79
+ // suite and then DELETES the file, so an open-ended tail would certify a producer that leaves
80
+ // nothing behind, and scanning for operator BYTES would put the incomplete-scan failure on the
81
+ // unsafe side (a missed operator is a dead pair) instead of the mild one (an unrecognised
82
+ // legitimate tail merely withholds the offer — add the gate by hand).
83
+ // The token set is everything that appears in a PATH or a glob and can never sequence, redirect or
84
+ // substitute a command — quoting and `~` included, `( ) $ ` ; & | < > # \` excluded. SCOPE, stated
85
+ // exactly: the screen judges each token's SOURCE bytes. Quote removal adds none, but brace SEQUENCE
86
+ // expansion does — `{Y..a}` yields ``[ \ ] ^ _ ` `` (probed) — so "no new bytes" would be a false
87
+ // claim. What holds is the property that matters: bash does not re-scan an expansion result as
88
+ // syntax, so a byte arriving that way is literal argument DATA, never an operator. The leading-`-`
89
+ // exclusion is weaker still — a FIRST-ORDER screen only, defeated by `'--flag'` and
90
+ // `{path,--flag}`. It is kept because the tail is the project's test PATHS and it costs only a loud
91
+ // withhold. Deciding an argument's post-expansion identity needs a shell lexer, which this family
92
+ // deliberately has NOWHERE (AD-079). So the claim is "configured with the reporters", never "the
93
+ // lcov survives the command"; a run that produces none is caught honestly at runtime as
94
+ // `skipped-no-lcov`.
95
+ const PRODUCER_PATH_TOKEN = /^(?!-)[A-Za-z0-9_./*{},:@+=~?[\]!'"-]+$/;
96
+ const pathShapedTail = (tail) => tail === '' || tail.split(/[ \t]+/).every((token) => PRODUCER_PATH_TOKEN.test(token));
97
+ const carriesProducerBody = (text) =>
98
+ text === COVERAGE_PRODUCER_BODY ||
99
+ (text.startsWith(`${COVERAGE_PRODUCER_BODY} `) && pathShapedTail(text.slice(COVERAGE_PRODUCER_BODY.length).trim()));
100
+
101
+ // matchesCoverageProducer(cmd) → CLOSED-WORLD over the full command forms the kit emits, never a
102
+ // substring probe: `echo "$AW_GIT_DIR/agent-workflow-lcov.info"`, a half-written reporter flag set,
103
+ // or the path as a bare substring must all read as NOT a producer — otherwise the checker is
104
+ // declared over a gate that writes nothing and then PASSES while certifying nothing.
105
+ export const matchesCoverageProducer = (cmd) => {
106
+ if (typeof cmd !== 'string') return false;
107
+ const trimmed = cmd.trim();
108
+ if (carriesProducerBody(trimmed)) return true;
109
+ return PRODUCER_EXEC_PREFIXES.some((prefix) => trimmed.startsWith(prefix) && carriesProducerBody(trimmed.slice(prefix.length)));
110
+ };
111
+ // coverage-producer canon <<< END drift-guarded region
112
+
54
113
  // The RETIRED kit-owned git-dir stores the deleted machinery wrote — dead data a consumer's
55
114
  // upgrade would otherwise strand forever. The migration cleans them (consented via the preview;
56
115
  // ENOENT is a silent no-op; any other unlink error is reported loudly but never fails the
@@ -147,13 +206,20 @@ export const buildMigrationPlan = (gates, kitToolsDir) => {
147
206
  plan.push({ action: 'keep', entry: gate, reason: null });
148
207
  }
149
208
  const kept = plan.filter((r) => r.action === 'keep' || r.action === 'extend');
209
+ // The checker READS an lcov; something has to WRITE it. Adding the checker over a declaration
210
+ // with no producer creates the dead pair — the gate PASSES (`skipped-no-lcov`) and certifies
211
+ // nothing, so the migration withholds it and says why instead.
212
+ const hasProducer = kept.some((r) => matchesCoverageProducer(r.entry.cmd));
150
213
  let collision = null;
214
+ let checkerWithheld = false;
151
215
  if (checkerRow === null) {
152
216
  // A surviving NON-canonical entry already holding the checker's id blocks the add — two
153
217
  // `coverage-check` rows would be ambiguous; the customized entry must be resolved by hand
154
218
  // FIRST (the caller turns this into a loud STOP on preview and apply alike).
155
219
  if (kept.some((r) => r.entry.id === 'coverage-check')) {
156
220
  collision = 'coverage-check';
221
+ } else if (!hasProducer) {
222
+ checkerWithheld = true;
157
223
  } else {
158
224
  plan.push({
159
225
  action: 'add',
@@ -165,8 +231,23 @@ export const buildMigrationPlan = (gates, kitToolsDir) => {
165
231
  checkerRow.action = 'move';
166
232
  checkerRow.reason = 'the canonical checker must be the LAST declared gate (nothing may run after it consumed the lcov)';
167
233
  }
234
+ // An ALREADY-declared checker over no producer is the same dead pair the withhold prevents — an
235
+ // earlier deployment could have created it. The migration removes no declared gate, so it reports
236
+ // the inertness and refuses to call the result final-run-capable.
237
+ const checkerInert = checkerRow !== null && !hasProducer;
168
238
  const reviewStateCandidate = `{ "id": "review-state", "title": "Review receipts converged (D3(b))", "cmd": "node \\"${join(kitToolsDir, 'review-state.mjs')}\\" --check" }`;
169
- return { plan, customized, unitTestsExtended, finalCapable: hasReviewState, reviewStateCandidate, collision };
239
+ return {
240
+ plan,
241
+ customized,
242
+ unitTestsExtended,
243
+ finalCapable: hasReviewState && !checkerWithheld && !checkerInert,
244
+ hasProducer,
245
+ hasReviewState,
246
+ checkerWithheld,
247
+ checkerInert,
248
+ reviewStateCandidate,
249
+ collision,
250
+ };
170
251
  };
171
252
 
172
253
  export const resultingGates = (plan) => {
@@ -184,7 +265,7 @@ const customizedRecovery = (gate) =>
184
265
  ? `declare the canonical suite gate by hand so the coverage contract is verifiable: node --test ${UNIT_TESTS_COVERAGE_FLAGS} <your test paths>`
185
266
  : 'remove the entry, or repoint it at a living check — the review-ledger / fold-completeness tools no longer exist.';
186
267
 
187
- const warningLines = ({ customized, finalCapable, reviewStateCandidate }) => {
268
+ const warningLines = ({ customized, finalCapable, hasReviewState = finalCapable, checkerWithheld = false, checkerInert = false, reviewStateCandidate }) => {
188
269
  const lines = [];
189
270
  for (const gate of customized) {
190
271
  lines.push(` CUSTOMIZED (untouched): ${gate.id}: ${gate.cmd}`);
@@ -193,7 +274,15 @@ const warningLines = ({ customized, finalCapable, reviewStateCandidate }) => {
193
274
  if (customized.length) {
194
275
  lines.push(' IMPORTANT: do NOT install the commit guard until every customized entry above is resolved — a declaration that cannot pass run-gates --final would block every commit.');
195
276
  }
196
- if (!finalCapable) {
277
+ if (checkerWithheld) {
278
+ lines.push(' WARNING: the canonical coverage-check gate was NOT added — no declared gate would PRODUCE the lcov it reads, and a checker with no producer passes while verifying nothing. Declare the suite gate first, then re-run this migration:');
279
+ lines.push(` node --test ${UNIT_TESTS_COVERAGE_FLAGS} <your test paths>`);
280
+ }
281
+ if (checkerInert) {
282
+ lines.push(' WARNING: the DECLARED coverage-check gate is INERT — no declared gate PRODUCES the lcov it reads, so it passes while verifying nothing. Nothing is removed for you; declare the suite gate:');
283
+ lines.push(` node --test ${UNIT_TESTS_COVERAGE_FLAGS} <your test paths>`);
284
+ }
285
+ if (!hasReviewState) {
197
286
  lines.push(' WARNING: the result is NOT final-run-capable — no canonical review-state check is declared. Add it (paste-ready), then run-gates --final can mint the receipt:');
198
287
  lines.push(` ${reviewStateCandidate}`);
199
288
  }
@@ -201,7 +290,7 @@ const warningLines = ({ customized, finalCapable, reviewStateCandidate }) => {
201
290
  };
202
291
 
203
292
  export const formatPreview = (analysis, applyHint) => {
204
- const { plan, unitTestsExtended, finalCapable, retiredStores = [] } = analysis;
293
+ const { plan, unitTestsExtended, finalCapable, hasProducer = unitTestsExtended, retiredStores = [] } = analysis;
205
294
  const lines = ['[agent-workflow] legacy gates.json migration preview (dry-run — nothing was written):'];
206
295
  const acted = plan.filter((r) => r.action !== 'keep');
207
296
  for (const r of acted) {
@@ -217,7 +306,9 @@ export const formatPreview = (analysis, applyHint) => {
217
306
  : ' nothing to migrate mechanically — no canonical legacy entries and no retired stores; the warnings below still need a hand.',
218
307
  );
219
308
  }
220
- if (!unitTestsExtended && !plan.some((r) => r.entry.id === 'unit-tests')) {
309
+ // Keyed on the ID, but a PRODUCER is recognized under any id repeating this advice over a
310
+ // working producer sends the user to fix nothing.
311
+ if (!hasProducer && !unitTestsExtended && !plan.some((r) => r.entry.id === 'unit-tests')) {
221
312
  lines.push(' note: no canonical `unit-tests` entry found — declare your suite gate with the lcov reporters by hand (the coverage-check gate reads the file it produces).');
222
313
  }
223
314
  lines.push(...warningLines(analysis));
@@ -317,7 +408,8 @@ Usage:
317
408
  Default is a dry-run PREVIEW (writes nothing). --apply rewrites ${GATES_REL} atomically:
318
409
  canonical legacy entries (review-ledger / fold-completeness --check, matched by their documented
319
410
  single-invocation forms) are REMOVED; the canonical unit-tests cmd gains the built-in lcov
320
- reporters; the coverage-check gate is ADDED last (resolved, QUOTED path). Customized entries are
411
+ reporters; the coverage-check gate is ADDED last (resolved, QUOTED path) and WITHHELD, loudly,
412
+ when no declared gate produces the lcov it reads. Customized entries are
321
413
  NEVER auto-touched — the preview names each with a paste-ready recovery, and the commit guard
322
414
  must not be installed until they are resolved.`;
323
415
 
@@ -12,6 +12,7 @@ import { spawnSync } from 'node:child_process';
12
12
  import {
13
13
  LEGACY_FORMS,
14
14
  UNIT_TESTS_COVERAGE_FLAGS,
15
+ COVERAGE_PRODUCER_BODY,
15
16
  RETIRED_STORE_BASENAMES,
16
17
  findRetiredStores,
17
18
  buildMigrationPlan,
@@ -71,6 +72,42 @@ describe('migrate-gates — the pure migration plan', () => {
71
72
  assert.ok(!again.plan.some((r) => r.action === 'add'), 'a declaration already carrying the checker gains no duplicate');
72
73
  });
73
74
 
75
+ it('a declaration with NO producer never GAINS the checker — the pair is declared together or not at all', () => {
76
+ const npmSuite = { id: 'suite', title: 'S', cmd: 'npm test' };
77
+ const analysis = buildMigrationPlan([LEGACY_LEDGER, npmSuite], KIT_TOOLS);
78
+ assert.ok(!analysis.plan.some((r) => r.action === 'add'), 'no checker is added over a declaration that produces no lcov');
79
+ assert.deepEqual(resultingGates(analysis.plan).map((g) => g.id), ['suite'], 'the legacy entry still goes, nothing dead arrives');
80
+ assert.equal(analysis.finalCapable, false, 'a declaration with no checker is not final-run-capable');
81
+ const preview = formatPreview(analysis, 'APPLY');
82
+ assert.match(preview, /coverage-check/, 'the withheld checker is named');
83
+ assert.match(preview, /produce/i, 'the preview says WHY — no gate produces the lcov it would read');
84
+ });
85
+
86
+ it('an ALREADY-declared checker over no producer is reported INERT, is never removed, and is not final-run-capable', () => {
87
+ const checker = { id: 'coverage-check', title: 'CC', cmd: `node "${join(KIT_TOOLS, 'coverage-check.mjs')}" --check` };
88
+ const reviewState = { id: 'review-state', title: 'RS', cmd: `node "${join(KIT_TOOLS, 'review-state.mjs')}" --check` };
89
+ const analysis = buildMigrationPlan([{ id: 'suite', title: 'S', cmd: 'npm test' }, reviewState, checker], KIT_TOOLS);
90
+ assert.equal(analysis.finalCapable, false, 'a review-state present must NOT make an inert pair read as final-run-capable');
91
+ assert.ok(resultingGates(analysis.plan).some((g) => g.id === 'coverage-check'), 'the declared checker is never removed');
92
+ const preview = formatPreview(analysis, 'APPLY');
93
+ assert.match(preview, /INERT/, 'the inert pair is named');
94
+ assert.match(preview, /--experimental-test-coverage/, 'the paste-ready suite cmd is carried');
95
+ assert.doesNotMatch(preview, /already final-run-capable/);
96
+ });
97
+
98
+ it('a gates-init-shaped producer (the exec-wrapped offer form) is recognized — the checker IS added over it', () => {
99
+ const offered = {
100
+ id: 'test',
101
+ title: 'T',
102
+ cmd: `COREPACK_ENABLE_NETWORK=0 npm exec --offline --script-shell /bin/sh -- ${COVERAGE_PRODUCER_BODY}`,
103
+ };
104
+ const analysis = buildMigrationPlan([offered], KIT_TOOLS);
105
+ assert.deepEqual(resultingGates(analysis.plan).map((g) => g.id), ['test', 'coverage-check']);
106
+ // The `no canonical unit-tests entry` advice is keyed on the ID, but a producer is recognized
107
+ // under ANY id — repeating the advice over a working producer sends the user to fix nothing.
108
+ assert.doesNotMatch(formatPreview(analysis, 'APPLY'), /declare your suite gate with the lcov reporters by hand/);
109
+ });
110
+
74
111
  it('a CUSTOMIZED dead-tool reference (compound form) is kept untouched and reported', () => {
75
112
  const analysis = buildMigrationPlan([CUSTOM], KIT_TOOLS);
76
113
  assert.equal(analysis.plan.find((r) => r.entry.id === 'my-ledger-wrap').action, 'keep');
@@ -259,6 +259,13 @@ const CATALOG = [
259
259
  kind: READ_ONLY,
260
260
  oneLine: 'Check that the documented contract tokens still match the live code constants they describe — a read-only lint that fails closed on drift; --check turns it into a gate exit code.',
261
261
  },
262
+ {
263
+ key: 'dispatch',
264
+ invocation: invocationOf('dispatch'),
265
+ group: 'Orchestrate',
266
+ kind: WRITER,
267
+ oneLine: 'Measure delegation: check a sub-task brief’s contract block (form only — never whether the task is genuinely bounded), pre-register an acceptance wave with its thresholds, record one observation, open a delegated thread from that brief, wait for that one dispatch to answer — a wait that ends without an answer says so and authorizes nothing — absorb the wrapper’s receipt back into the ledger, fold the returned work or close the thread with a recorded degrade, and print the per-class report of how much a delegated sub-task actually bought, derived from what was dispatched, returned and folded. Writes only its own ledger file beside the repo; never commits.',
268
+ },
262
269
  {
263
270
  // NEVER `guarded` — that kind promises dry-run-first, which these writers do not have; the
264
271
  // honest strongest caution is `writer` with the destructive arm named in the line itself.
@@ -24,7 +24,8 @@
24
24
  // re-derived): safe repo-relative path resolution, no-follow real-path containment, shell-free
25
25
  // argv, per-run timeout, N/N reruns, the quarantine lane (reruns 3, timeout 120s).
26
26
  // Import posture: this module is the DAG BOTTOM — it imports only node built-ins + atomic-write +
27
- // changed-surface, and OWNS the canonical review-domain primitives (tree fingerprint, receipt read
27
+ // changed-surface + the pure leaves (repo-lex, fs-read-nofollow, coverage-state), and OWNS the
28
+ // canonical review-domain primitives (tree fingerprint, receipt read
28
29
  // path, attesting predicate, verdict vocabulary, testId format, base resolution). review-state
29
30
  // RE-EXPORTS its historical public API from here, so its consumers (and the bash-twin parity
30
31
  // tests) are unchanged while review-state can import the degrade reader without an import cycle.
@@ -42,6 +43,9 @@ import { writeContainedFileAtomic } from './atomic-write.mjs';
42
43
  import { parsePositiveIntKnob, probeVerdict } from './changed-surface.mjs';
43
44
  import { readRegularFileNoFollow } from './fs-read-nofollow.mjs';
44
45
  import { lexicalRepoRelative } from './repo-lex.mjs';
46
+ // The coverage vocabulary leaf: run-gates RECORDS the token this validator checks, and run-gates
47
+ // imports THIS module (the sole-writer boundary), so their shared home sits below both.
48
+ import { COVERAGE, FINAL_COVERAGE_STATES } from './coverage-state.mjs';
45
49
 
46
50
  export const CORE_EVIDENCE_STOP = 'CORE_EVIDENCE_STOP';
47
51
  const stop = (message) => Object.assign(new Error(`[agent-workflow-kit] ${message}`), { name: 'CoreEvidenceStop', code: CORE_EVIDENCE_STOP });
@@ -52,7 +56,9 @@ const GIT_MAX_BUFFER = 256 * 1024 * 1024; // a full-tree diff / TAP stream can b
52
56
 
53
57
  const gitRaw = (args, cwd) => spawnSync('git', args, { cwd, maxBuffer: GIT_MAX_BUFFER, windowsHide: true });
54
58
 
55
- const gitBuf = (args, cwd) => {
59
+ // Exported so a consumer computing over the SAME domain (the exec metric producer) reads git through
60
+ // this one runner instead of growing a second spawnSync wrapper with its own buffer bound.
61
+ export const gitBuf = (args, cwd) => {
56
62
  const r = gitRaw(args, cwd);
57
63
  if (r.error || r.status !== 0) return null;
58
64
  return r.stdout;
@@ -670,6 +676,17 @@ export const validateEvidenceRecord = (record) => {
670
676
  if (record.lcovSha256 !== null && (typeof record.lcovSha256 !== 'string' || !HEX64_RE.test(record.lcovSha256))) {
671
677
  return { ok: false, reason: 'final: lcovSha256 must be a 64-hex sha256 of the consumed lcov file, or null when none was produced' };
672
678
  }
679
+ // The run's coverage token is ADDITIVE (absent = a pre-token receipt, still valid) and CLOSED.
680
+ // `certified` is cross-checked against the bound digest — a run that certified a verdict read
681
+ // bytes, so the two fields can never disagree in the store.
682
+ if ('coverage' in record) {
683
+ if (!FINAL_COVERAGE_STATES.includes(record.coverage)) {
684
+ return { ok: false, reason: `final: coverage, when present, must be one of ${FINAL_COVERAGE_STATES.join(' | ')} (got ${JSON.stringify(record.coverage)}; a final run always selects the canonical checker, so "none" never rides a final receipt)` };
685
+ }
686
+ if (record.coverage === COVERAGE.certified && record.lcovSha256 === null) {
687
+ return { ok: false, reason: 'final: coverage "certified" requires a bound lcovSha256 — a certified verdict was issued over lcov bytes that were read' };
688
+ }
689
+ }
673
690
  if (!isNonEmptyString(record.timestamp)) return { ok: false, reason: 'final: timestamp must be a non-empty string' };
674
691
  return { ok: true };
675
692
  }
@@ -997,6 +1014,26 @@ export const runDegrade = ({ cwd = process.cwd(), env = process.env, backend, re
997
1014
 
998
1015
  // ── the summary verb (D6): ONE stateless render — receipts + evidence store, no ledger ────────────
999
1016
 
1017
+ // A red-proof binds the BYTES of the test file it was observed on, and the final-run checker refuses
1018
+ // when those bytes have moved (coverage-check.mjs, the hash-mismatch arm). This render used to print a
1019
+ // stale record byte-identically to a live one, so the only place staleness surfaced was that gate —
1020
+ // after both council dispatches, at the commit boundary, which is the most expensive discovery point
1021
+ // in the loop. The currency is therefore computed HERE, from the SAME two helpers the checker uses, so
1022
+ // the render and the gate can never disagree about what stale means.
1023
+ //
1024
+ // An unresolvable or unreadable file is STALE, not unknown: a proof nobody can re-check is not a proof
1025
+ // anyone should read as current.
1026
+ export const redProofCurrency = (rootTop, record) => {
1027
+ if (rootTop == null) return { state: 'stale', detail: 'the work-tree root is not resolvable' };
1028
+ const resolved = resolveTestFile(rootTop, record.file);
1029
+ if (!resolved.ok) return { state: 'stale', detail: resolved.reason };
1030
+ const current = hashFileBytes(resolved.abs);
1031
+ if (current == null) return { state: 'stale', detail: `cannot read "${record.file}"` };
1032
+ return current === record.fileHash
1033
+ ? { state: 'current' }
1034
+ : { state: 'stale', detail: 'the bound test file changed after the mint' };
1035
+ };
1036
+
1000
1037
  export const buildSummaryState = ({ cwd = process.cwd(), env = process.env } = {}) => {
1001
1038
  const fingerprint = computeTreeFingerprint(cwd);
1002
1039
  const base = resolveBase(cwd);
@@ -1011,7 +1048,10 @@ export const buildSummaryState = ({ cwd = process.cwd(), env = process.env } = {
1011
1048
  backend: b,
1012
1049
  summary: summarizeReviewReceiptsForTree(receipts.filter((r) => r.backend === b), fingerprint),
1013
1050
  }));
1014
- const redProofs = authoritativeOfKind(store.records, 'red-proof').filter((r) => r.base === base);
1051
+ const rootTop = gitLine(['rev-parse', '--show-toplevel'], cwd);
1052
+ const redProofs = authoritativeOfKind(store.records, 'red-proof')
1053
+ .filter((r) => r.base === base)
1054
+ .map((r) => ({ ...r, currency: redProofCurrency(rootTop, r) }));
1015
1055
  const degrades = authoritativeOfKind(store.records, 'degrade').filter((r) => r.fingerprint === fingerprint);
1016
1056
  const finalRun = authoritativeOfKind(store.records, 'final').find((r) => r.fingerprintBefore === fingerprint) ?? null;
1017
1057
  // A malformed/unreadable store makes the AUTHORITATIVE selection untrustworthy (a dropped later
@@ -1037,13 +1077,40 @@ const verdictLine = ({ backend, summary }) => {
1037
1077
  return `${backend}: no attesting receipt for the current tree (stale or missing)`;
1038
1078
  };
1039
1079
 
1080
+ // Why the run issued no verdict, keyed by the token IT recorded. `certified` maps to nothing —
1081
+ // there is no absence to name. A legacy receipt predates the token: the only honest thing to say
1082
+ // is what the RECORD carries, which is true by construction. Exported as a test seam (keyFor).
1083
+ const COVERAGE_QUALIFIER = Object.freeze({
1084
+ [COVERAGE.notRun]: 'the recorded run issued no coverage verdict',
1085
+ [COVERAGE.unknown]: 'the recorded run produced no readable coverage signal',
1086
+ });
1087
+ const LEGACY_COVERAGE_QUALIFIER = `coverage=${COVERAGE.unknown}: this legacy receipt carries no coverage token and binds no lcov digest`;
1088
+
1089
+ export const coverageQualifierFor = (finalRun) => {
1090
+ if (!finalRun) return '';
1091
+ if (typeof finalRun.coverage === 'string') {
1092
+ const why = COVERAGE_QUALIFIER[finalRun.coverage];
1093
+ return why ? ` — coverage=${finalRun.coverage}: ${why}` : '';
1094
+ }
1095
+ return finalRun.lcovSha256 == null ? ` — ${LEGACY_COVERAGE_QUALIFIER}` : '';
1096
+ };
1097
+
1040
1098
  export const renderSummary = (s) => {
1041
1099
  const short = (hex) => (typeof hex === 'string' ? `${hex.slice(0, 12)}…` : String(hex));
1042
1100
  const evidenceSections = s.evidenceUnavailable
1043
1101
  ? [` evidence sections WITHHELD — the store is unavailable (${s.storeMalformed} malformed line(s)${s.storeReadError ? `, read error: ${s.storeReadError}` : ''}); a dropped line could resurrect a superseded record — inspect ${s.storePath}`]
1044
1102
  : [
1045
1103
  ` red-proof records (current base): ${s.redProofs.length ? '' : '(none)'}`,
1046
- ...s.redProofs.map((r) => ` ${r.testId} — ${r.reds}/${r.runs} red, hash ${short(r.fileHash)}, pre-fix fingerprint ${short(r.fingerprint)}`),
1104
+ ...s.redProofs.map((r) => {
1105
+ const stale = r.currency?.state === 'stale';
1106
+ const mark = stale ? `STALE (${r.currency.detail})` : 'CURRENT';
1107
+ return ` ${r.testId} — ${mark} · ${r.reds}/${r.runs} red, hash ${short(r.fileHash)}, pre-fix fingerprint ${short(r.fingerprint)}`;
1108
+ }),
1109
+ // The recovery, printed once and only when it is needed — it was written down nowhere, and
1110
+ // the loop paid for that three times.
1111
+ ...(s.redProofs.some((r) => r.currency?.state === 'stale')
1112
+ ? [' ↳ a STALE proof is refused by run-gates --final: park the fix so the bound test fails again, re-observe it with `core-evidence red-proof "<testId>"`, then restore the fix. Edit test files FIRST and re-observe ONCE per park — every proof bound to an edited file goes stale together.']
1113
+ : []),
1047
1114
  ` degrade records (current tree): ${s.degrades.length ? '' : '(none)'}`,
1048
1115
  ...s.degrades.map((d) => ` ${d.backend} — ${d.reason} (${d.timestamp})`),
1049
1116
  ];
@@ -1053,10 +1120,17 @@ export const renderSummary = (s) => {
1053
1120
  ' review verdicts (current tree):',
1054
1121
  ...(s.verdicts.length ? s.verdicts.map((v) => ` ${verdictLine(v)}`) : [' (no receipts)']),
1055
1122
  ];
1123
+ // The withheld coverage verdict travels here too: an unqualified GREEN repeats, one surface
1124
+ // further on, the false reassurance the checker's own attested=no exists to close. It is read
1125
+ // from the token the RUN recorded — `lcovSha256` says what the receipt binds, never whether a
1126
+ // verdict was issued — and it rides RED as well: an absent verdict is a property of the run, not
1127
+ // of its colour. A LEGACY receipt (no token) is named as exactly that, never as a claim about
1128
+ // what it read. DETAIL beside an unchanged status word.
1129
+ const coverageQualifier = coverageQualifierFor(s.finalRun);
1056
1130
  const finalLine = s.evidenceUnavailable
1057
1131
  ? null
1058
1132
  : s.finalRun
1059
- ? ` final gate run: ${s.finalRun.status === 'green' ? 'GREEN' : 'RED'} (${s.finalRun.results.filter((r) => r.ok).length}/${s.finalRun.results.length} gates, ${s.finalRun.timestamp})`
1133
+ ? ` final gate run: ${s.finalRun.status === 'green' ? 'GREEN' : 'RED'}${coverageQualifier} (${s.finalRun.results.filter((r) => r.ok).length}/${s.finalRun.results.length} gates, ${s.finalRun.timestamp})`
1060
1134
  : ' final gate run: (none recorded for the current tree)';
1061
1135
  const lines = [
1062
1136
  'core-evidence summary — stateless render (review receipts + evidence store; no ledger, no rounds)',
@@ -179,6 +179,11 @@ export const attestationState = ({ env, records, fingerprint, base }) => {
179
179
  return { attesting: true };
180
180
  };
181
181
 
182
+ // The withheld-verdict reason for the one case a VALID context cannot rescue: the run owned the
183
+ // artifact's lifetime and read NO bytes — an absent file and a refused path alike, which is why
184
+ // this states the bytes rather than the skip.
185
+ const NO_BYTES_NO_VERDICT = 'no lcov bytes were read at the checked path, so no verdict could be issued — a run that read nothing certifies nothing; if no gate produces the file, declare a coverage PRODUCER beside the checker (references/modes/gates.md names the exact form)';
186
+
182
187
  // ── the red-proof verification arm (D3(c)) ────────────────────────────────────────────────────────
183
188
 
184
189
  // verifyRedProofs({ rootTop, cwd, env }) → { failures: [...], verified: n } | { storeFailure }.
@@ -276,7 +281,13 @@ export const runCheck = ({ cwd = process.cwd(), env = process.env } = {}) => {
276
281
  const identityAfter = { fingerprint: computeTreeFingerprint(cwd), base: resolveBase(cwd) };
277
282
  const attestAfter = attestationState({ env, records: storeRecords, ...identityAfter });
278
283
  const attestation = attestBefore.refusal ? attestBefore : attestAfter;
279
- const attesting = attestation.attesting === true && attestBefore.attesting === true;
284
+ const contextValid = attestation.attesting === true && attestBefore.attesting === true;
285
+ // `attested=` states whether a coverage VERDICT was ISSUED — pass OR fail — never that coverage
286
+ // passed: a valid handshake over uncovered lines still reads yes and still exits 1. What no
287
+ // context can rescue is an EMPTY read, and the honest predicate is the BYTES CONSUMED, not the
288
+ // skip flag: an absent lcov and a refused path (a symlink) both read nothing, and attesting over
289
+ // either is the same false green this whole arm exists to close, one layer up.
290
+ const attesting = contextValid && cov.lcovSha256 !== null;
280
291
  // EXACTLY ONE fully anchored machine line, the lcov-sha256 contract's sibling — the runner binds
281
292
  // it, so a missing/duplicated/injected one is an integrity failure rather than a silent green.
282
293
  lines.push(`coverage-check: attested=${attesting ? 'yes' : 'no'}`);
@@ -284,11 +295,11 @@ export const runCheck = ({ cwd = process.cwd(), env = process.env } = {}) => {
284
295
  failed = true;
285
296
  lines.push(`coverage-check: REFUSED — ${attestation.refusal}`);
286
297
  } else if (!attesting) {
287
- lines.push(`coverage-check: NO VERDICT — ${attestation.reason}`);
298
+ lines.push(`coverage-check: NO VERDICT — ${contextValid ? NO_BYTES_NO_VERDICT : attestation.reason}`);
288
299
  }
289
300
  // The attestation gates ONLY the coverage claim. Every pre-existing fail-closed refusal above
290
301
  // (symlinked lcov, malformed evidence store, unmet red-proof obligation) keeps its own exit 1.
291
- if (attesting && !failed && !cov.skipped && cov.failures.length === 0) {
302
+ if (attesting && !failed && cov.failures.length === 0) {
292
303
  lines.push('coverage-check: PASS — every changed Node line is covered');
293
304
  }
294
305
  return { code: failed ? 1 : 0, lines };
@@ -314,10 +325,15 @@ store fails (exit 1).
314
325
  A coverage VERDICT is issued ONLY inside the run that owns the artifact's lifetime: run-gates
315
326
  --final deletes the lcov before any gate spawns and hands this checker an attestation context
316
327
  (a nonce whose one-way commitment over {nonce, fingerprint, base} is the final-start attempt id).
317
- One anchored machine line rides every run: coverage-check: attested=<yes|no>.
318
- attested=yes the verdict, exactly as before.
319
- attested=no NO VERDICT (exit 0): findings are still printed, uncovered lines still exit 1;
320
- only the PASS attestation is withheld.
328
+ One anchored machine line rides every run: coverage-check: attested=<yes|no>. It states whether a
329
+ coverage VERDICT was ISSUED pass OR fail — never that coverage passed.
330
+ attested=yes a verdict was issued over the lcov BYTES this run read (a failing one still
331
+ exits 1); the predicate is the consumed digest, never the skip flag.
332
+ attested=no → NO VERDICT: no handshake, or no lcov bytes were read at all (an absent file or a
333
+ refused path — nothing read, nothing certified). Withholding a verdict never
334
+ changes an exit code: an absent lcov stays exit 0, a refused non-regular path
335
+ keeps its own fail-closed exit 1, and uncovered lines still exit 1. Findings are
336
+ still printed; run-gates carries the withheld verdict as coverage=not-run.
321
337
  REFUSED (exit 1) → the context describes another tree, or matches no recorded attempt.
322
338
  Residual, stated: ownership of the fixed path is CONVENTION, not enforcement — a concurrent writer
323
339
  to it can still place foreign evidence (queued as LCOV-EXCLUSIVE-OWNERSHIP).
@@ -0,0 +1,68 @@
1
+ // coverage-producer.mjs — the kit's half of the shared coverage-producer vocabulary: the reporter
2
+ // flags the canonical suite gate carries, and the CLOSED predicate deciding whether a declared gate
3
+ // cmd actually WRITES the lcov the canonical checker reads. A LEAF — imports nothing, so every
4
+ // consumer (gates-init, gates-declaration, the advisor) decides through ONE predicate.
5
+ // Dependency-free, Node >= 22. No side effects on import.
6
+
7
+ // coverage-producer canon >>> BEGIN drift-guarded region
8
+ // Authored TWICE, byte-identically: in the memory substrate's references/scripts/migrate-gates.mjs
9
+ // and in the composition root's tools/coverage-producer.mjs. Neither side imports the other — the
10
+ // substrate is standalone and must not depend on the root, and the root must not import mirrored
11
+ // bytes — so a TEXT drift guard beside the root's copy holds them equal. Edit BOTH, then re-run the
12
+ // mirror sync.
13
+ //
14
+ // The destination is written against AW_GIT_DIR, which run-gates exports to every gate child on a
15
+ // plain run AND on --final (AW_LCOV_FILE is --final only), so one cmd survives the unmet
16
+ // producer-variable preflight in both modes. The explicit stdout reporter is not decoration:
17
+ // without it the lcov reporter swallows the human TAP/spec stream.
18
+ export const UNIT_TESTS_COVERAGE_FLAGS =
19
+ '--experimental-test-coverage --test-reporter=lcov --test-reporter-destination="$AW_GIT_DIR/agent-workflow-lcov.info" --test-reporter=spec --test-reporter-destination=stdout';
20
+
21
+ // The ONE suite body that produces that lcov with no extra dependency.
22
+ export const COVERAGE_PRODUCER_BODY = `node --test ${UNIT_TESTS_COVERAGE_FLAGS}`;
23
+
24
+ // The per-PM exec wrappers a fill offer puts that body behind. Recognition must cover every form
25
+ // the kit has EMITTED, so the prefixes are matched literally; gates-init's execCmdFor stays the one
26
+ // EMITTER and is bound to this list by a named acceptance test, never by a second grammar.
27
+ const PRODUCER_EXEC_PREFIXES = Object.freeze([
28
+ 'COREPACK_ENABLE_NETWORK=0 npm exec --offline --script-shell /bin/sh -- ',
29
+ 'COREPACK_ENABLE_NETWORK=0 pnpm exec -- ',
30
+ 'COREPACK_ENABLE_NETWORK=0 yarn exec -- ',
31
+ ]);
32
+
33
+ // A trailing suffix is the project's own test paths; a leading one would mean the body is not what
34
+ // this cmd runs. The tail passes a POSITIVE closed grammar — every whitespace-separated token must
35
+ // be path-shaped — never an operator blocklist: `node --test <flags> && rm -f <lcov>` runs the
36
+ // suite and then DELETES the file, so an open-ended tail would certify a producer that leaves
37
+ // nothing behind, and scanning for operator BYTES would put the incomplete-scan failure on the
38
+ // unsafe side (a missed operator is a dead pair) instead of the mild one (an unrecognised
39
+ // legitimate tail merely withholds the offer — add the gate by hand).
40
+ // The token set is everything that appears in a PATH or a glob and can never sequence, redirect or
41
+ // substitute a command — quoting and `~` included, `( ) $ ` ; & | < > # \` excluded. SCOPE, stated
42
+ // exactly: the screen judges each token's SOURCE bytes. Quote removal adds none, but brace SEQUENCE
43
+ // expansion does — `{Y..a}` yields ``[ \ ] ^ _ ` `` (probed) — so "no new bytes" would be a false
44
+ // claim. What holds is the property that matters: bash does not re-scan an expansion result as
45
+ // syntax, so a byte arriving that way is literal argument DATA, never an operator. The leading-`-`
46
+ // exclusion is weaker still — a FIRST-ORDER screen only, defeated by `'--flag'` and
47
+ // `{path,--flag}`. It is kept because the tail is the project's test PATHS and it costs only a loud
48
+ // withhold. Deciding an argument's post-expansion identity needs a shell lexer, which this family
49
+ // deliberately has NOWHERE (AD-079). So the claim is "configured with the reporters", never "the
50
+ // lcov survives the command"; a run that produces none is caught honestly at runtime as
51
+ // `skipped-no-lcov`.
52
+ const PRODUCER_PATH_TOKEN = /^(?!-)[A-Za-z0-9_./*{},:@+=~?[\]!'"-]+$/;
53
+ const pathShapedTail = (tail) => tail === '' || tail.split(/[ \t]+/).every((token) => PRODUCER_PATH_TOKEN.test(token));
54
+ const carriesProducerBody = (text) =>
55
+ text === COVERAGE_PRODUCER_BODY ||
56
+ (text.startsWith(`${COVERAGE_PRODUCER_BODY} `) && pathShapedTail(text.slice(COVERAGE_PRODUCER_BODY.length).trim()));
57
+
58
+ // matchesCoverageProducer(cmd) → CLOSED-WORLD over the full command forms the kit emits, never a
59
+ // substring probe: `echo "$AW_GIT_DIR/agent-workflow-lcov.info"`, a half-written reporter flag set,
60
+ // or the path as a bare substring must all read as NOT a producer — otherwise the checker is
61
+ // declared over a gate that writes nothing and then PASSES while certifying nothing.
62
+ export const matchesCoverageProducer = (cmd) => {
63
+ if (typeof cmd !== 'string') return false;
64
+ const trimmed = cmd.trim();
65
+ if (carriesProducerBody(trimmed)) return true;
66
+ return PRODUCER_EXEC_PREFIXES.some((prefix) => trimmed.startsWith(prefix) && carriesProducerBody(trimmed.slice(prefix.length)));
67
+ };
68
+ // coverage-producer canon <<< END drift-guarded region
@@ -0,0 +1,24 @@
1
+ // coverage-state.mjs — the coverage vocabulary LEAF (kit-inert-gate Phase 2, Decision 8).
2
+ //
3
+ // Two consumers need the same closed value set and cannot import each other: run-gates.mjs REPORTS
4
+ // the token on its summary line and records it on the `--final` receipt, and core-evidence.mjs
5
+ // VALIDATES that recorded field — and run-gates already imports core-evidence (the sole-writer
6
+ // boundary), so the dependency can only run this way. ONE home, no drift guard needed: the
7
+ // repo-lex.mjs / gates-declaration.mjs leaf idiom.
8
+ //
9
+ // No imports, no CLI, no side effects. Dependency-free, Node >= 22.
10
+
11
+ // The CLOSED set, with ONE value defined for every run outcome. It is DETAIL, never a new state:
12
+ // no exit code, no `status=` token, no receipt status and no commit-guard disposition reads it.
13
+ export const COVERAGE = Object.freeze({
14
+ certified: 'certified', // the canonical checker consumed lcov bytes and ISSUED a verdict — pass OR fail
15
+ notRun: 'not-run', // the checker ran and issued NO verdict: nothing was read, or the run holds no attestation context
16
+ none: 'none', // no canonical checker ran here (an --only subset, the --pre-review derived subset)
17
+ unknown: 'unknown', // the run ended before the gates produced the signal, or the signal is unreadable
18
+ });
19
+
20
+ // What may ride a `final` receipt. `none` never can: --final REFUSES a declaration that does not
21
+ // carry the canonical checker last, so a final run always selects it (run-gates.mjs --final
22
+ // preflight) — a recorded `none` would mean the receipt describes a run that could not have
23
+ // happened, and the validator says so instead of storing it.
24
+ export const FINAL_COVERAGE_STATES = Object.freeze([COVERAGE.certified, COVERAGE.notRun, COVERAGE.unknown]);
@@ -0,0 +1,32 @@
1
+ // declared-paths.mjs — the shared resolution + containment leaf for a DECLARED settings path
2
+ // (`sandbox.filesystem.allowWrite`). Two consumers must never disagree about what such an entry
3
+ // MEANS: the advisor's worktrees-dir convergence lane asks "does a declared entry cover this dir?",
4
+ // and the autonomy render's allowWrite degrade asks "does this entry resolve outside the repo?" —
5
+ // the same resolution and the same containment rule, read from opposite ends. A second resolver
6
+ // would let those two answers drift, so the rule lives here once.
7
+ //
8
+ // A LEAF: imports node:path only (never the advisor, never the render — the advisor already imports
9
+ // the render). No side effects on import. Dependency-free, Node >= 22.
10
+
11
+ import { resolve, sep } from 'node:path';
12
+
13
+ // What counts as a RESOLVABLE declared entry, shared by both consumers so they cannot disagree about
14
+ // which entries are readable at all. A blank entry is rejected deliberately: it means nothing to a
15
+ // host, but it would resolve to the project root and thereby read as a grant on the whole repo.
16
+ export const isResolvableDeclaredEntry = (entry) => typeof entry === 'string' && entry.trim() !== '';
17
+
18
+ // Resolve a declared entry the way a host that honors the key resolves it: `~` and `~/…` against the
19
+ // resolved home, every other form (relative or absolute) against the project root.
20
+ export const resolveDeclaredDir = (entry, { home, root }) => {
21
+ if (entry === '~') return resolve(home);
22
+ if (entry.startsWith('~/')) return resolve(home, entry.slice(2));
23
+ return resolve(root, entry);
24
+ };
25
+
26
+ // Ancestor-or-equal containment on PATH SEGMENTS, never a raw string prefix — a grant on `<p>/farm`
27
+ // must never read as a grant on the sibling `<p>/farmhouse`. A grant on a DESCENDANT never covers
28
+ // its parent: the parent dir is the one a provision writes into.
29
+ export const dirCovers = (containerDir, candidateDir) => {
30
+ const base = containerDir.endsWith(sep) ? containerDir.slice(0, -sep.length) : containerDir;
31
+ return candidateDir === base || candidateDir.startsWith(`${base}${sep}`);
32
+ };
@@ -67,14 +67,15 @@ const RAW_BACKENDS = [
67
67
  roleContracts: {
68
68
  execute: {
69
69
  invocations: [
70
- 'codex-exec <plan-file|->',
71
- 'codex-exec <plan-file|-> -- <extra codex flags...>',
70
+ 'codex-exec [--nonce <n>] <plan-file|->',
71
+ 'codex-exec [--nonce <n>] <plan-file|-> -- <extra codex flags...>',
72
72
  ],
73
73
  grounding: "automatic — the root AGENTS.md (Hard Constraints) is auto-merged into codex's context and the wrapper prepends the orchestrator execution contract; no grounding flags",
74
74
  continue: [
75
- 'codex-exec --resume-last <plan-file|->',
76
- 'codex-exec --resume <session-id> <plan-file|->',
75
+ 'codex-exec --resume-last [--nonce <n>] <plan-file|->',
76
+ 'codex-exec --resume <session-id> [--nonce <n>] <plan-file|->',
77
77
  ],
78
+ receipt: "side effect — a NONCED run mints ONE exec receipt beside the delegation store: the dispatch nonce seam is the AW_DISPATCH_NONCE environment value or its plain-argument equivalent --nonce <n>, recognised ONLY before the prompt operand (after the operand or a literal '--' it is passthrough payload, never a flag), under the safe grammar [A-Za-z0-9._-]{1,64} — anything else, a duplicate, or a flag disagreeing with a non-empty env value refuses PRE-SPEND. The store directory resolves exactly as the kit's delegation store does: the dirname of an ABSOLUTE AW_DELEGATION_STORE (a relative one, or one ending in a path separator, refuses), else the git common dir. The artifact is agent-workflow-exec-receipt-<backendLength>-<backend>-<nonce>.json in two states: 'reserved' is written atomically and NO-CLOBBER immediately before the CLI runs — that write IS the nonce reservation, so a second dispatch on the same nonce, or an already-taken report name, refuses BEFORE any spend — and 'terminal' replaces it in place at exit. A nonced run also refuses pre-spend when no timeout/gtimeout binary can cap it (an accounted dispatch that cannot be capped can never honour the terminal-exit rule; a nonce-LESS run still warns and runs uncapped), when node is missing (the mint core), and when the prompt rides on stdin instead of a contract FILE — contractDigest is computed BY THIS WRAPPER from the dispatch file it was actually handed, so the kit can refuse a run that executed a different contract than the one it opened. That digest is taken from the SAME bytes already read as the prompt, never a second open of the path: two reads leave a window in which the file can be swapped, and the run would then execute one contract while its receipt claimed the digest of another. The header's own nonce must EQUAL the dispatch nonce — 'dispatch open' copies the nonce FROM the header, so a disagreeing --nonce could only reserve an identity no return would ever absorb, and it refuses pre-spend. A contract file edited BETWEEN 'dispatch open' and the run is caught at ABSORB by the contractDigest comparison, not pre-spend: the wrapper never reads the ledger, and that boundary is what the whole lane rests on. At exit the wrapper FIRST re-reads its reservation and verifies its own opaque owner token — a foreign owner refuses having published NOTHING, neither report nor receipt — THEN writes the delegate's final message atomically to agent-workflow-exec-report-<backendLength>-<backend>-<nonce>.txt, THEN re-verifies the owner and REPLACES the reservation with the terminal receipt {schema, kind, state, backend, nonce, owner, contractDigest, wrapperVersion, posture {model, effort, tier}, capS, killGraceS, sessionId, exitStatus, outcome, reportDigest, reportLength, timestamp}: the report is complete on disk before any artifact says the run arrived. capS and killGraceS are the cap the run ACTUALLY applied. outcome is the wrapper's own SUBSET of the ledger's vocabulary — exit 0 with a session id -> success, exit 0 without one -> missing-identity, ANY nonzero exit including the timeout codes 124 and 137 -> transport-failure; every orchestrator judgment is recorded at absorb time, never claimed here. The session id is captured BEFORE outcome branching, so a FAILED run records one too; in resume mode it is the validated resume id. FAIL-CLOSED, deliberately NOT the review lane's warn-only receipt: a publication that cannot complete exits nonzero with a DISTINCT status, and the message states only what the run can still prove. 70: the reservation could not be verified BEFORE any publication — NOTHING was published, not the report and not the receipt, and because the artifact found there belongs to another run it is never a '--no-receipt' source. 71: a publication stopped after that point — either the report write failed (nothing beyond the reservation was published; the '--no-receipt' absorb then records reportLength 0, ineligible by the name empty-report) or the report IS on disk and the terminal receipt was not completed (the absorb reads it, report-if-present). The post-report lane never claims the reservation still stands, because after that point its fate is no longer something this run observed. Every lane names the tree as partial/dirtied rather than untouched. A nonce-LESS invocation is byte-unchanged: no reservation, no receipt, no artifact, no node.",
78
79
  passthrough: {
79
80
  policy: 'guarded',
80
81
  blocked: ['-c*', '--config*', '-s*', '--sandbox*', '--dangerously-bypass-approvals-and-sandbox', '--dangerously-bypass-hook-trust', '--full-auto', '--oss', '--local-provider*', '-p*', '--profile*', '-m*', '--model*', '-o*', '--output-last-message*', '--json*', '--color*', '--output-schema*', '--ephemeral*'],