@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
@@ -324,7 +324,11 @@ const FIELD_CHECKS = {
324
324
  firstPassNum: { ok: isByteCount, want: 'a non-negative integer first-pass numerator' },
325
325
  firstPassDen: { ok: (v) => Number.isSafeInteger(v) && v >= 1, want: 'a positive integer first-pass denominator' },
326
326
  vehicle: { ok: isPlainObject, want: 'the closed vehicle pair {requested, selected}' },
327
- backend: { ok: isNonEmptyString, want: 'a non-empty backend name' },
327
+ // The safe token grammar, not merely non-empty: the backend NAMES an artifact beside the store
328
+ // (the exec receipt and its report), so a name outside this grammar records a dispatch whose own
329
+ // receipt could never be written. One grammar on both sides, or the ledger accepts what the
330
+ // wrapper cannot mint.
331
+ backend: { ok: isSafeName, want: 'a backend name in the safe token grammar ([A-Za-z0-9._-]{1,64})' },
328
332
  contractDigest: { ok: isHex64, want: 'the 64-hex digest of the canonical contract header' },
329
333
  preTreeDigest: { ok: isHex64, want: 'the 64-hex uncommitted-state fingerprint at dispatch' },
330
334
  postTreeDigest: { ok: isHex64, want: 'the 64-hex uncommitted-state fingerprint after the run' },
@@ -427,8 +431,11 @@ const validateReturnCrossFields = (r) => {
427
431
  if (r.metric.eligible) {
428
432
  return refuse(`return: this return's own fields make the metric INELIGIBLE (${local.ineligibleReason}) — an eligible metric is never claimed over them`);
429
433
  }
430
- if (r.metric.ineligibleReason !== local.ineligibleReason && r.metric.ineligibleReason !== 'dirty-baseline') {
431
- return refuse(`return: metric.ineligibleReason "${r.metric.ineligibleReason}" contradicts this return's own fields (locally "${local.ineligibleReason}") only "dirty-baseline" may override`);
434
+ // The locally PROVABLE name wins outright: "dirty-baseline" is admissible only where the return's
435
+ // own fields leave the metric eligible, so it can never stand in for a reason a reader could
436
+ // have checked. (The store then binds that ONE override to the dispatch's recorded baseline.)
437
+ if (r.metric.ineligibleReason !== local.ineligibleReason) {
438
+ return refuse(`return: metric.ineligibleReason "${r.metric.ineligibleReason}" contradicts this return's own fields (locally "${local.ineligibleReason}") — the provable reason is the one recorded, and "dirty-baseline" never replaces it`);
432
439
  }
433
440
  } else if (!r.metric.eligible && r.metric.ineligibleReason !== 'dirty-baseline') {
434
441
  return refuse(`return: metric.ineligibleReason "${r.metric.ineligibleReason}" is not substantiated by this return's own fields — when the local evaluation finds the metric eligible, only "dirty-baseline" (the store-verified override) may be recorded`);
@@ -0,0 +1,392 @@
1
+ // dispatch-store.mjs — the delegation-ledger IO (delegation Plan 1, Phase 2): common-dir path
2
+ // resolution, the fail-closed reader, the lock-serialized append with its SEMANTIC preflight, and
3
+ // the ONE named tree-fingerprint contract the records bind. No CLI, no side effects on import; the
4
+ // engine (dispatch.mjs, Phase 3) is the only surface that mints records for a human.
5
+ //
6
+ // This is the measurement substrate's storage half. The vocabulary (closed record family, the
7
+ // outcome enum with its allowed-successor table, the metric byte domains) lives in the PURE
8
+ // dispatch-record.mjs and validates ONE record at a time; everything that needs a store SNAPSHOT
9
+ // lives here: thread transitions and terminality, return↔dispatch correlation, fold resolution,
10
+ // the retry rules, and the wave rules.
11
+ //
12
+ // Why its OWN store (D2), separate from the review receipts (`agent-workflow-review-receipts.jsonl`,
13
+ // per-git-dir) and the flow store (`agent-workflow-flow.jsonl`): a shared file would let one class
14
+ // of line answer another class of question — the exact defect the D10 discriminator closes on the
15
+ // review-waiter side. The parity is enforced from BOTH ends: a review receipt entering here is an
16
+ // unknown kind and the reader counts it malformed (the closed family fails closed), and a
17
+ // delegation line reaching the receipts store never satisfies a review waiter. The basename resolves
18
+ // to the git COMMON dir (the flow-store precedent) because delegation accounting is worktree-SHARED,
19
+ // and the `AW_DELEGATION_STORE` seam takes the AW_FLOW_STORE rules verbatim: absolute only, no
20
+ // trailing separator.
21
+ //
22
+ // The lock/CAS discipline is NOT re-implemented here — it is the shared store-append.mjs leaf (D12),
23
+ // so both stores fail closed the same way (bounded lock waits with named refusals per holder class,
24
+ // custody-checked release, fd-based no-follow reads, snapshot-bound rename guarding). This module
25
+ // supplies the nouns, the seams, the validator, the parser and the semantic preflight.
26
+ //
27
+ // Reference domains stay SPLIT (D3): a fold references a return by its per-record CANONICAL digest;
28
+ // thread linkage (`nonce`, `retryOf`) is by nonce identity. The family has NO supersession — a
29
+ // record is never superseded, only closed — so resolution never has to pick a "latest" of a key,
30
+ // and a fold whose returnDigest resolves cross-thread or not at all is a refusal, never a guess.
31
+ //
32
+ // Honest limits: records remain forgeable (a self-discipline mechanism in the git dir, not a
33
+ // security boundary); the fingerprint contract below is blind to the index↔worktree split, which is
34
+ // why the metric additionally requires the dispatch's recorded CLEAN baseline (D5); and the
35
+ // pathname-race residuals of the shared leaf are inherited as declared there.
36
+
37
+ import { join, isAbsolute, normalize, sep } from 'node:path';
38
+ import {
39
+ validateDelegationRecord, canonicalDelegationDigest, allowedSuccessorKinds,
40
+ isThreadTerminalRecord,
41
+ } from './dispatch-record.mjs';
42
+ import { computeTreeFingerprint } from './core-evidence.mjs';
43
+ import { gitLine } from './flow-store-read.mjs';
44
+ import { readRegularFileNoFollow } from './fs-read-nofollow.mjs';
45
+ import { createStoreAppendLane } from './store-append.mjs';
46
+
47
+ export const DELEGATION_STORE_STOP = 'DELEGATION_STORE_STOP';
48
+ // The ONE typed-STOP factory for this store — a caller classifies a delegation refusal by code.
49
+ export const delegationStoreStop = (message) => Object.assign(new Error(`[agent-workflow-kit] ${message}`), { name: 'DelegationStoreStop', code: DELEGATION_STORE_STOP });
50
+ const stop = delegationStoreStop;
51
+
52
+ export const DELEGATION_STORE_BASENAME = 'agent-workflow-delegation.jsonl';
53
+ export const DELEGATION_LOCK_SUFFIX = '.lock';
54
+
55
+ // ── path resolution (common dir + the AW_DELEGATION_STORE seam) ───────────────────────────────────
56
+
57
+ // resolveDelegationStorePath(cwd, env) → the ABSOLUTE store path, or null outside a git WORK tree.
58
+ // is-inside-work-tree gates explicitly (--git-common-dir also succeeds in a bare repo, and the probe
59
+ // prints "false" WITH exit 0, so the STRING is compared). The override must be absolute — a relative
60
+ // one would resolve a different ledger from each worktree/cwd.
61
+ export const resolveDelegationStorePath = (cwd, env = process.env) => {
62
+ if (env.AW_DELEGATION_STORE) {
63
+ if (!isAbsolute(env.AW_DELEGATION_STORE)) {
64
+ throw stop(`AW_DELEGATION_STORE must be an ABSOLUTE path (got "${env.AW_DELEGATION_STORE}") — a relative override resolves a different ledger from each worktree/cwd (fail closed)`);
65
+ }
66
+ const normalized = normalize(env.AW_DELEGATION_STORE);
67
+ // A trailing separator survives normalize() but not the appender's basename/join — refuse the fork.
68
+ if (normalized.endsWith(sep) || normalized.endsWith('/')) {
69
+ throw stop(`AW_DELEGATION_STORE must not end with a path separator (got "${env.AW_DELEGATION_STORE}") — a store is a file, not a directory (fail closed)`);
70
+ }
71
+ return normalized;
72
+ }
73
+ if (gitLine(['rev-parse', '--is-inside-work-tree'], cwd) !== 'true') return null;
74
+ const commonDir = gitLine(['rev-parse', '--path-format=absolute', '--git-common-dir'], cwd);
75
+ return commonDir == null ? null : join(commonDir, DELEGATION_STORE_BASENAME);
76
+ };
77
+
78
+ // The lock is a SIBLING derived from the resolved store path — one store, one lock, everywhere.
79
+ export const resolveDelegationLockPath = (storePath) => `${storePath}${DELEGATION_LOCK_SUFFIX}`;
80
+
81
+ // ── the fail-closed reader ────────────────────────────────────────────────────────────────────────
82
+
83
+ // parseDelegationStoreText(raw) → { records, malformed, malformedReasons }. RAW file order is the
84
+ // only view: thread legality is an ordering property, and the family has no supersession, so there
85
+ // is no "authoritative" projection to compute. A line the closed family does not recognise — a
86
+ // review receipt included — is MALFORMED, never silently skipped.
87
+ export const parseDelegationStoreText = (raw) => {
88
+ const records = [];
89
+ const recordLines = [];
90
+ const malformedReasons = [];
91
+ const lines = String(raw).split('\n');
92
+ for (let i = 0; i < lines.length; i += 1) {
93
+ if (lines[i].trim() === '') continue;
94
+ let parsed;
95
+ try {
96
+ parsed = JSON.parse(lines[i]);
97
+ } catch {
98
+ malformedReasons.push(`line ${i + 1}: invalid JSON`);
99
+ continue;
100
+ }
101
+ const v = validateDelegationRecord(parsed);
102
+ if (v.ok) {
103
+ records.push(parsed);
104
+ // The PHYSICAL line, carried beside the record: a reader that refuses has to say WHERE, and
105
+ // the record's index in a filtered array is not a place anyone can open.
106
+ recordLines.push(i + 1);
107
+ } else malformedReasons.push(`line ${i + 1}: ${v.reason}`);
108
+ }
109
+ return { records, recordLines, malformed: malformedReasons.length, malformedReasons };
110
+ };
111
+
112
+ // readDelegationStore(path, io?) → { records, malformed, malformedReasons, readError? }. Absent →
113
+ // empty (no records yet is not an error); any other failure → readError, and consumers fail closed
114
+ // on malformed > 0 or readError. A dangling symlink must NOT read as an empty ledger.
115
+ export const readDelegationStore = (path, io = {}) => {
116
+ const empty = () => ({ records: [], recordLines: [], malformed: 0, malformedReasons: [] });
117
+ const read = readRegularFileNoFollow(path, io);
118
+ if (read.outcome === 'absent') return empty();
119
+ if (read.outcome === 'foreign') return { ...empty(), readError: `the store is a ${read.className}, not a regular file — refusing to read it (fail closed)` };
120
+ if (read.outcome === 'error') return { ...empty(), readError: read.code };
121
+ return parseDelegationStoreText(read.content);
122
+ };
123
+
124
+ // ── D5: the ONE named tree-fingerprint contract ───────────────────────────────────────────────────
125
+
126
+ // Every tree digest this family records — a dispatch's preTreeDigest, a return's postTreeDigest, a
127
+ // fold's treeDigestAtFold — is THIS ONE computation, so two records can be compared for equality at
128
+ // all. The helper DELEGATES to the frozen core rather than re-deriving the payload: a second
129
+ // implementation of "the uncommitted state" is exactly how two digests of one tree would appear.
130
+ //
131
+ // Domain (pinned by the parity fixtures): staged diff + unstaged diff + untracked-not-ignored
132
+ // contents; an untracked symlink contributes name+target, an untracked directory or unstatable path
133
+ // contributes its name only, a binary file contributes a marker, and the never-committable classes
134
+ // (character device, block device, FIFO, socket) are excluded ENTIRELY — no marker at all.
135
+ //
136
+ // Honest limit, stated where it bites: the domain is blind to the index↔worktree split (the staged
137
+ // and unstaged diffs are concatenated), so a hunk moving into the index leaves it unchanged. That is
138
+ // why metric eligibility additionally requires the dispatch's recorded CLEAN baseline (D5) — the
139
+ // fingerprint alone cannot attribute bytes to a dispatch.
140
+ export const UNCOMMITTED_STATE_FINGERPRINT = 'the uncommitted-state fingerprint';
141
+
142
+ export const uncommittedStateFingerprint = (cwd = process.cwd(), fsx) => {
143
+ const fingerprint = computeTreeFingerprint(cwd, fsx);
144
+ if (fingerprint == null) {
145
+ throw stop(`cannot compute ${UNCOMMITTED_STATE_FINGERPRINT} — not inside a git work tree (or a git probe failed); a record never carries a null tree digest (fail closed)`);
146
+ }
147
+ return fingerprint;
148
+ };
149
+
150
+ // ── thread state (the store's ONE walk; the Phase-3 aggregator consumes it) ───────────────────────
151
+
152
+ const THREAD_KINDS = ['dispatch', 'return', 'fold', 'degrade'];
153
+
154
+ // delegationThreadState(records, nonce) → { records, dispatch, return, closure, last, terminal, open }
155
+ // over ONE nonce thread in RAW file order. A pre-dispatch degrade carries nonce null and therefore
156
+ // belongs to no thread. Terminality is the vocabulary's own predicate — never re-derived here.
157
+ export const delegationThreadState = (records, nonce) => {
158
+ const thread = records.filter((r) => THREAD_KINDS.includes(r.kind) && r.nonce === nonce);
159
+ const last = thread.length === 0 ? null : thread[thread.length - 1];
160
+ const dispatch = thread.find((r) => r.kind === 'dispatch') ?? null;
161
+ const terminal = last !== null && isThreadTerminalRecord(last);
162
+ return {
163
+ records: thread,
164
+ dispatch,
165
+ return: thread.find((r) => r.kind === 'return') ?? null,
166
+ closure: thread.find((r) => r.kind === 'fold' || r.kind === 'degrade') ?? null,
167
+ last,
168
+ terminal,
169
+ open: dispatch !== null && !terminal,
170
+ };
171
+ };
172
+
173
+ // The cap that binds a retry chain is the ORIGIN's — walking back to retryIndex 0. A later contract
174
+ // may legitimately differ (a contract-refusal retry MUST differ), so reading the cap off the newest
175
+ // contract would let a fresh header manufacture a fresh budget. The seen-set is a tampered-store
176
+ // guard: an unresolvable or cyclic chain stops at the strictest link proven so far.
177
+ const retryChainOrigin = (records, dispatch) => {
178
+ let current = dispatch;
179
+ const seen = new Set([current.nonce]);
180
+ while (current.retryOf !== null) {
181
+ const prior = records.find((r) => r.kind === 'dispatch' && r.nonce === current.retryOf);
182
+ if (prior === undefined || seen.has(prior.nonce)) return current;
183
+ seen.add(prior.nonce);
184
+ current = prior;
185
+ }
186
+ return current;
187
+ };
188
+
189
+ const closureLabel = (last) =>
190
+ last.kind === 'fold' ? 'its fold'
191
+ : last.kind === 'degrade' ? 'its recorded degrade'
192
+ : `a terminal return (outcome "${last.outcome}")`;
193
+
194
+ // The kinds that name a wave and therefore require it to be REGISTERED first.
195
+ const WAVE_SCOPED_KINDS = ['dispatch', 'observation', 'degrade'];
196
+
197
+ // ── the semantic preflight (runs INSIDE the critical section, on the LOCKED snapshot) ─────────────
198
+
199
+ // Everything a single record cannot decide about itself. A writer's lock-free walk is advisory: only
200
+ // the snapshot under the lock can refuse a second dispatch, a stale return or a retry of a thread
201
+ // that closed a millisecond ago. Every refusal names the rule it enforces and states that nothing
202
+ // was written.
203
+ const delegationSemanticPreflight = ({ records, snapshot, storePath }) => {
204
+ // Byte-identical replay is the lane's own refusal; this is the CANONICAL one — a key-order
205
+ // permutation serializes differently but IS the same record, and the digest is record identity.
206
+ const digest = canonicalDelegationDigest(snapshot);
207
+ if (records.some((r) => canonicalDelegationDigest(r) === digest)) {
208
+ throw stop(`refusing a canonical duplicate: ${storePath} already carries this exact record (${digest.slice(0, 12)}…), however its keys are ordered — a genuine new record carries new content or a new timestamp; nothing was written`);
209
+ }
210
+
211
+ // The registration is RESOLVED and READ, not merely found: it fixes the classes, the pairing key
212
+ // and the minimum per class BEFORE the first observation it will count, so a record naming a
213
+ // class the wave never registered would invent an acceptance set after the fact.
214
+ const registration = records.find((r) => r.kind === 'pre-registration' && r.waveId === snapshot.waveId);
215
+ if (snapshot.kind === 'pre-registration') {
216
+ if (registration !== undefined) {
217
+ throw stop(`refusing a second pre-registration: the wave "${snapshot.waveId}" is already registered and a registration is IMMUTABLE per wave — thresholds a wave was registered under can never move under its own observations; nothing was written`);
218
+ }
219
+ } else if (WAVE_SCOPED_KINDS.includes(snapshot.kind)) {
220
+ if (registration === undefined) {
221
+ throw stop(`refusing a ${snapshot.kind} that names the UNREGISTERED wave "${snapshot.waveId}" — acceptance is PRE-REGISTERED before the first observation it will count, so the thresholds can never be chosen after the fact; register the wave first; nothing was written`);
222
+ }
223
+ if (!registration.stepClasses.includes(snapshot.stepClass)) {
224
+ throw stop(`refusing a ${snapshot.kind}: step class "${snapshot.stepClass}" is not among the classes wave "${snapshot.waveId}" registered (${registration.stepClasses.join(' | ')}) — the registered set IS the acceptance set; nothing was written`);
225
+ }
226
+ }
227
+
228
+ const nonce = typeof snapshot.nonce === 'string' ? snapshot.nonce : null;
229
+ if (nonce === null) return; // a pre-registration, an observation, or a pre-dispatch degrade
230
+ const state = delegationThreadState(records, nonce);
231
+
232
+ if (snapshot.kind === 'dispatch') {
233
+ if (state.dispatch !== null) {
234
+ throw stop(`refusing a duplicate dispatch: nonce "${nonce}" already carries a dispatch — a nonce IS the thread identity and is minted once; nothing was written`);
235
+ }
236
+ if (snapshot.retryOf !== null) {
237
+ const prior = delegationThreadState(records, snapshot.retryOf);
238
+ if (prior.dispatch === null) {
239
+ throw stop(`refusing a retry: no dispatch for nonce "${snapshot.retryOf}" is in the store — a retry names the thread it retries; nothing was written`);
240
+ }
241
+ // At most ONE retry successor per thread. Bounding only the chain's DEPTH would leave its
242
+ // BRANCHING free — n2, n3, n4 … all legally at retryIndex 1 — and the cap would be decorative.
243
+ // It is also what retryChainOrigin already assumes when it walks back to index 0.
244
+ const successor = records.find((r) => r.kind === 'dispatch' && r.retryOf === snapshot.retryOf);
245
+ if (successor !== undefined) {
246
+ throw stop(`refusing a retry: thread "${snapshot.retryOf}" already has the retry successor "${successor.nonce}" — a thread is retried at most ONCE, or the recorded cap would bound only the chain's depth while its branching stayed free; nothing was written`);
247
+ }
248
+ if (!prior.terminal) {
249
+ throw stop(`refusing a retry: thread "${snapshot.retryOf}" is still OPEN — a thread is retried only after it closed (a success or acceptance-failure return stays live until its fold or degrade); nothing was written`);
250
+ }
251
+ // Terminality alone is not enough: a FOLD is also terminal, and a folded thread's work was
252
+ // accepted into the tree. Retrying it would mint a second counted thread over the same work
253
+ // (D7 counts a folded success in `n` with its metric), inflating both the count and the mean.
254
+ // The legal retry origins are the ones that recorded a FAILED attempt: a terminal-failure
255
+ // return, or a degrade (the recorded no-fold closure).
256
+ if (prior.last.kind === 'fold') {
257
+ throw stop(`refusing a retry: thread "${snapshot.retryOf}" was closed by its fold — folding accepts the attempt into the tree, so a folded thread is never a retry origin whatever its return's outcome was; open a NEW thread instead; nothing was written`);
258
+ }
259
+ if (snapshot.retryIndex !== prior.dispatch.retryIndex + 1) {
260
+ throw stop(`refusing a retry: retryIndex ${snapshot.retryIndex} must be ${prior.dispatch.retryIndex + 1} — a retry increments its origin's index by exactly one, so the chain length is the index; nothing was written`);
261
+ }
262
+ if (snapshot.waveId !== prior.dispatch.waveId) {
263
+ throw stop(`refusing a retry: it names wave "${snapshot.waveId}" but its retry origin "${snapshot.retryOf}" was dispatched in wave "${prior.dispatch.waveId}" — a retry stays in its origin's wave, or one thread would count in two acceptance sets; nothing was written`);
264
+ }
265
+ if (snapshot.stepClass !== prior.dispatch.stepClass) {
266
+ throw stop(`refusing a retry: it declares step class "${snapshot.stepClass}" but its retry origin "${snapshot.retryOf}" was dispatched as "${prior.dispatch.stepClass}" — the pairing key is the step class, so a chain that changes it mid-way would split one attempt's accounting across two classes; nothing was written`);
267
+ }
268
+ const origin = retryChainOrigin(records, prior.dispatch);
269
+ if (snapshot.retryIndex > origin.retryCap) {
270
+ throw stop(`refusing a retry: retryIndex ${snapshot.retryIndex} exceeds the retryCap ${origin.retryCap} recorded on the thread's ORIGIN dispatch ("${origin.nonce}") — a fresh contract never manufactures a fresh retry budget; nothing was written`);
271
+ }
272
+ if (prior.return !== null && prior.return.outcome === 'contract-refusal' && snapshot.contractDigest === prior.dispatch.contractDigest) {
273
+ throw stop(`refusing a retry: it retries a contract-refusal thread and must carry a DIFFERENT contractDigest — an unchanged contract would only be refused again, and a retry loop on one contract is exactly what the cap exists to prevent; nothing was written`);
274
+ }
275
+ }
276
+ return;
277
+ }
278
+
279
+ if (state.dispatch === null) {
280
+ throw stop(`refusing a ${snapshot.kind}: no dispatch for nonce "${nonce}" is in the store — a thread opens with its dispatch, and a record that binds to nothing is never absorbed; nothing was written`);
281
+ }
282
+ if (snapshot.kind === 'return' && state.return !== null) {
283
+ throw stop(`refusing a second return: nonce "${nonce}" already carries a return (outcome "${state.return.outcome}") — one dispatch answers exactly once, so a stale return never lands; nothing was written`);
284
+ }
285
+ const allowed = allowedSuccessorKinds(state.last);
286
+ if (!allowed.includes(snapshot.kind)) {
287
+ throw stop(state.terminal
288
+ ? `refusing a ${snapshot.kind}: thread "${nonce}" is already closed by ${closureLabel(state.last)} — a closed thread never absorbs another record; nothing was written`
289
+ : `refusing a ${snapshot.kind}: nonce "${nonce}" carries no return to fold — the thread's last record is a ${state.last.kind}, whose only legal successors are ${allowed.join(' | ')}; nothing was written`);
290
+ }
291
+
292
+ if (snapshot.kind === 'return') {
293
+ // The nonce binds the thread; these three bind the IDENTITY of what was dispatched — a return
294
+ // from another backend, against another contract, or from another tree is not this answer.
295
+ for (const field of ['backend', 'contractDigest', 'preTreeDigest']) {
296
+ if (snapshot[field] !== state.dispatch[field]) {
297
+ throw stop(`refusing a return: ${field} "${snapshot[field]}" does not equal its dispatch's "${state.dispatch[field]}" — a return is bound to the dispatch it answers by nonce AND by identity; nothing was written`);
298
+ }
299
+ }
300
+ // D5's directional implication, enforced where it CAN be: `baselineClean` lives on the DISPATCH,
301
+ // so the record validator had to admit `dirty-baseline` without being able to verify it. A dirty
302
+ // baseline FORCES ineligibility — but never renames a STRICTER reason the return's own fields
303
+ // already substantiate (the validator pinned that one locally, and the producer can prove it);
304
+ // a CLEAN baseline forbids the claim outright, so an eligible metric is never silently
305
+ // downgraded by an unsubstantiated override.
306
+ if (state.dispatch.baselineClean === false) {
307
+ if (snapshot.metric.eligible) {
308
+ throw stop(`refusing a return: its dispatch recorded baselineClean:false, so the metric is INELIGIBLE — the uncommitted-state fingerprint is blind to the index↔worktree split, so a dirty baseline cannot attribute bytes to this dispatch (D5); record eligible:false with ineligibleReason "dirty-baseline" (or the stricter reason this return's own fields substantiate); nothing was written`);
309
+ }
310
+ } else if (snapshot.metric.ineligibleReason === 'dirty-baseline') {
311
+ throw stop(`refusing a return: it claims ineligibleReason "dirty-baseline" while its dispatch recorded baselineClean:true — the override is unsubstantiated, and only the dispatch decides that fact; nothing was written`);
312
+ }
313
+ }
314
+
315
+ if (snapshot.kind === 'degrade') {
316
+ // A degrade CLOSES a thread, and a closed thread counts in its wave's aggregation with L = 0
317
+ // (D7). Left unbound, a degrade declaring another (registered) wave or class would report one
318
+ // thread into two acceptance sets — the same reason the cross-wave retry rule exists.
319
+ for (const field of ['waveId', 'stepClass']) {
320
+ if (snapshot[field] !== state.dispatch[field]) {
321
+ throw stop(`refusing a degrade: ${field} "${snapshot[field]}" does not equal its dispatch's "${state.dispatch[field]}" — a thread is closed inside the wave and step class it was dispatched in; nothing was written`);
322
+ }
323
+ }
324
+ }
325
+
326
+ if (snapshot.kind === 'fold') {
327
+ const target = records.find((r) => canonicalDelegationDigest(r) === snapshot.returnDigest);
328
+ if (target === undefined) {
329
+ throw stop(`refusing a fold: returnDigest ${snapshot.returnDigest.slice(0, 12)}… matches no record in the store — a fold binds an EXISTING return by its canonical digest; nothing was written`);
330
+ }
331
+ if (target.kind !== 'return') {
332
+ throw stop(`refusing a fold: returnDigest resolves to a ${target.kind} record, not a return — a fold folds a return; nothing was written`);
333
+ }
334
+ if (target.nonce !== nonce) {
335
+ throw stop(`refusing a fold: returnDigest resolves to the return of nonce "${target.nonce}", not this fold's "${nonce}" — a fold never reaches across threads; nothing was written`);
336
+ }
337
+ if (snapshot.treeDigestAtFold !== target.postTreeDigest) {
338
+ throw stop(`refusing a fold: treeDigestAtFold ${snapshot.treeDigestAtFold.slice(0, 12)}… does not equal the folded return's postTreeDigest ${target.postTreeDigest.slice(0, 12)}… — the tree moved between the return and the fold, so what was folded is not what was returned; nothing was written`);
339
+ }
340
+ }
341
+ };
342
+
343
+ // ── the read-side audit (the append path's rules, replayed) ───────────────────────────────────────
344
+
345
+ // auditDelegationStoreSemantics({ records, recordLines, storePath }) → { ok: true } | { ok: false,
346
+ // line, reason }. The reader validates ONE record at a time; every cross-record rule — duplicate
347
+ // nonce, transition legality, correlation, the wave and retry rules, canonical duplicates — lives in
348
+ // the append preflight and was never re-run on read, so a ledger the append path would have REFUSED
349
+ // still parsed as a pile of valid records. A consumer computing over it (the Phase-3 aggregator's
350
+ // per-thread walk) would count a duplicated nonce twice and inflate its own statistic.
351
+ //
352
+ // The replay closes that by running the SAME preflight per record against the prefix before it, in
353
+ // file order — so legality has ONE authority (this store) rather than a second, drifting copy in
354
+ // each reader. It stops at the FIRST illegal record and names its physical line; a consumer never
355
+ // computes over a "legal prefix", because a ledger that lost a record mid-file is not a smaller
356
+ // ledger, it is an unexplained one.
357
+ //
358
+ // Honest limit, unchanged: this is not a security boundary. A forger can write a fully consistent
359
+ // ledger just as easily. What the replay defends against is a BUGGY producer — the exec-side
360
+ // wrapper Plan 2 introduces — and a hand-edit that got the rules wrong.
361
+ export const auditDelegationStoreSemantics = ({ records, recordLines = [], storePath = '(store)' } = {}) => {
362
+ for (let i = 0; i < records.length; i += 1) {
363
+ try {
364
+ delegationSemanticPreflight({ records: records.slice(0, i), snapshot: records[i], storePath });
365
+ } catch (err) {
366
+ return { ok: false, line: recordLines[i] ?? i + 1, reason: err.message };
367
+ }
368
+ }
369
+ return { ok: true };
370
+ };
371
+
372
+ // ── the append ────────────────────────────────────────────────────────────────────────────────────
373
+
374
+ const delegationAppendLane = createStoreAppendLane({
375
+ nouns: { store: 'delegation store', adj: 'delegation-store', record: 'delegation record' },
376
+ envNames: { store: 'AW_DELEGATION_STORE', waitKnob: 'AW_DELEGATION_LOCK_WAIT_MS', pollKnob: 'AW_DELEGATION_LOCK_POLL_MS' },
377
+ stop,
378
+ resolveStorePath: resolveDelegationStorePath,
379
+ resolveLockPath: resolveDelegationLockPath,
380
+ validateRecord: validateDelegationRecord,
381
+ parseStoreText: parseDelegationStoreText,
382
+ });
383
+
384
+ // The store path is always RESOLVED (cwd/env), never caller-supplied — a raw path param would bypass
385
+ // the absolute-normalization door the AW_DELEGATION_STORE seam enforces. The record is validated and
386
+ // serialized ONCE up front; the semantic rules then run under the lock on the captured snapshot.
387
+ export const appendDelegationRecord = ({ cwd = process.cwd(), record, env = process.env, deps = {} } = {}) => {
388
+ const { line, snapshot } = delegationAppendLane.captureRecordSnapshot(record);
389
+ return delegationAppendLane.appendResolvedRecord({
390
+ cwd, env, deps, preflight: delegationSemanticPreflight, makeRecord: () => ({ line, snapshot }),
391
+ });
392
+ };