@sabaiway/agent-workflow-kit 10.5.0 → 11.0.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 (53) hide show
  1. package/CHANGELOG.md +139 -0
  2. package/SKILL.md +9 -1
  3. package/bridges/antigravity-cli-bridge/SKILL.md +1 -1
  4. package/bridges/antigravity-cli-bridge/bin/agy-review.sh +6 -6
  5. package/bridges/antigravity-cli-bridge/capability.json +1 -1
  6. package/bridges/codex-cli-bridge/SKILL.md +1 -1
  7. package/bridges/codex-cli-bridge/bin/codex-exec.sh +1 -1
  8. package/bridges/codex-cli-bridge/bin/codex-review.sh +6 -6
  9. package/bridges/codex-cli-bridge/capability.json +1 -1
  10. package/capability.json +1 -1
  11. package/package.json +1 -1
  12. package/references/modes/commit-guard.md +2 -2
  13. package/references/modes/control-bytes.md +91 -0
  14. package/references/modes/core-evidence.md +1 -1
  15. package/references/modes/flow-writer.md +16 -4
  16. package/references/modes/procedures.md +5 -1
  17. package/references/modes/review-state.md +3 -3
  18. package/references/modes/robustness-brief.md +61 -0
  19. package/references/modes/velocity.md +2 -0
  20. package/references/robustness-literals.json +107 -0
  21. package/tools/changed-surface.mjs +1 -1
  22. package/tools/commands.mjs +14 -0
  23. package/tools/commit-guard.mjs +24 -12
  24. package/tools/control-bytes.mjs +264 -0
  25. package/tools/core-evidence.mjs +33 -20
  26. package/tools/coverage-check.mjs +1 -1
  27. package/tools/dispatch-store-read.mjs +269 -0
  28. package/tools/dispatch-store.mjs +21 -357
  29. package/tools/flow-check-git-lane.mjs +2 -1
  30. package/tools/flow-delta-proof.mjs +4 -4
  31. package/tools/flow-record-shape.mjs +44 -2
  32. package/tools/flow-round-gates.mjs +151 -0
  33. package/tools/flow-store-read.mjs +1 -1
  34. package/tools/flow-writer.mjs +235 -82
  35. package/tools/fold-scope.mjs +7 -3
  36. package/tools/fs-read-nofollow.mjs +56 -4
  37. package/tools/git-env.mjs +120 -0
  38. package/tools/grounding.mjs +4 -3
  39. package/tools/held-session.mjs +165 -0
  40. package/tools/plan-shape-facts.mjs +23 -10
  41. package/tools/plan-shape.mjs +8 -0
  42. package/tools/procedures.mjs +47 -4
  43. package/tools/repo-lex.mjs +29 -0
  44. package/tools/review-state.mjs +127 -39
  45. package/tools/robustness-brief.mjs +209 -0
  46. package/tools/robustness-literals.mjs +101 -0
  47. package/tools/sandbox-masks.mjs +2 -1
  48. package/tools/set-flow.mjs +2 -1
  49. package/tools/source-size-scope.mjs +19 -25
  50. package/tools/tracked-tree-census.mjs +3 -3
  51. package/tools/velocity-profile.mjs +5 -2
  52. package/tools/worktree-handoff-return.mjs +2 -2
  53. package/tools/worktrees.mjs +2 -2
@@ -24,7 +24,7 @@
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 + the pure leaves (repo-lex, fs-read-nofollow, coverage-state), and OWNS the
27
+ // changed-surface + the pure leaves (repo-lex, fs-read-nofollow, coverage-state, git-env), and OWNS the
28
28
  // canonical review-domain primitives (tree fingerprint, receipt read
29
29
  // path, attesting predicate, verdict vocabulary, testId format, base resolution). review-state
30
30
  // RE-EXPORTS its historical public API from here, so its consumers (and the bash-twin parity
@@ -43,6 +43,7 @@ import { parsePositiveIntKnob, probeVerdict } from './changed-surface.mjs';
43
43
  import { isDirectRun } from './direct-run.mjs';
44
44
  import { readRegularFileNoFollow } from './fs-read-nofollow.mjs';
45
45
  import { lexicalRepoRelative } from './repo-lex.mjs';
46
+ import { GIT_MAX_BUFFER, resolveGitLocation, withGitPath } from './git-env.mjs';
46
47
  // The coverage vocabulary leaf: run-gates RECORDS the token this validator checks, and run-gates
47
48
  // imports THIS module (the sole-writer boundary), so their shared home sits below both.
48
49
  import { COVERAGE, FINAL_COVERAGE_STATES } from './coverage-state.mjs';
@@ -52,7 +53,6 @@ const stop = (message) => Object.assign(new Error(`[agent-workflow-kit] ${messag
52
53
  const usageFail = (message) => Object.assign(new Error(`[agent-workflow-kit] ${message}`), { exitCode: 2 });
53
54
 
54
55
  const isoNow = () => new Date().toISOString();
55
- const GIT_MAX_BUFFER = 256 * 1024 * 1024; // a full-tree diff / TAP stream can be large; never truncate
56
56
 
57
57
  const gitRaw = (args, cwd) => spawnSync('git', args, { cwd, maxBuffer: GIT_MAX_BUFFER, windowsHide: true });
58
58
 
@@ -114,15 +114,32 @@ export const isNeverCommittableStat = (stat) =>
114
114
  stat != null &&
115
115
  (stat.isCharacterDevice() || stat.isBlockDevice() || stat.isFIFO() || stat.isSocket());
116
116
 
117
+ // The git runner seam the tree computations share — (args, dir, input, env) — and its guard: a
118
+ // runner that THROWS answers as a failed spawn (measured: a raw stack out of a fingerprint).
119
+ const defaultRunGit = (args, dir, input, env) => spawnSync('git', args, { cwd: dir, input, env: withGitPath(env ?? process.env), maxBuffer: GIT_MAX_BUFFER, windowsHide: true });
120
+ const guardedRunner = (runGit, env) => (args, dir, input, override) => {
121
+ try { return runGit(args, dir, input, override ?? env); } catch (err) { return { error: err, status: null, signal: null, stdout: Buffer.alloc(0), stderr: '' }; }
122
+ };
123
+ const stdoutOf = (r) => (r.error || r.status !== 0 ? null : r.stdout);
124
+ // Only a work-tree location is computed over (git-env's table under the runner's own env) — a redirecting GIT_DIR never hashes another repository in silence.
125
+ const locateWorkTree = (cwd, run, env) => {
126
+ const location = resolveGitLocation(cwd, { spawn: (cmd, args, opts) => run(args, opts.cwd, undefined, opts.env), env });
127
+ return location.state === 'work-tree' ? location.top : null;
128
+ };
129
+
117
130
  // The canonical payload bytes: staged diff + unstaged diff + the untracked-not-ignored section —
118
- // byte-identical to the wrappers' emit_fingerprint_payload, emitted from the work-tree ROOT. Null
119
- // outside a git work tree. The lstat is injectable ONLY for the never-committable filter tests.
120
- export const computeFingerprintPayload = (cwd, { lstat = lstatSync } = {}) => {
121
- const top = gitLine(['rev-parse', '--show-toplevel'], cwd);
131
+ // byte-identical to the wrappers' emit_fingerprint_payload, emitted from the work-tree ROOT. Null on
132
+ // every location but a work tree. lstat is injectable ONLY for the never-committable filter tests.
133
+ export const FINGERPRINT_CACHED_DIFF_ARGV = Object.freeze(['diff', '--cached', '--no-ext-diff', '--no-textconv', '--ignore-submodules=none']);
134
+ export const FINGERPRINT_UNSTAGED_DIFF_ARGV = Object.freeze(['diff', '--no-ext-diff', '--no-textconv', '--ignore-submodules=none']);
135
+ export const computeFingerprintPayload = (cwd, { lstat = lstatSync, runGit = null, env = process.env } = {}) => {
136
+ const run = guardedRunner(runGit ?? defaultRunGit, env);
137
+ const top = locateWorkTree(cwd, run, env);
122
138
  if (top == null) return null;
123
- const staged = gitBuf(['diff', '--cached', '--no-ext-diff'], top);
124
- const unstaged = gitBuf(['diff', '--no-ext-diff'], top);
125
- const untrackedZ = gitBuf(['ls-files', '--others', '--exclude-standard', '-z'], top);
139
+ const buf = (args) => stdoutOf(run(args, top));
140
+ const staged = buf(FINGERPRINT_CACHED_DIFF_ARGV);
141
+ const unstaged = buf(FINGERPRINT_UNSTAGED_DIFF_ARGV);
142
+ const untrackedZ = buf(['ls-files', '--others', '--exclude-standard', '-z']);
126
143
  if (staged == null || unstaged == null || untrackedZ == null) return null;
127
144
  const chunks = [staged, unstaged];
128
145
  for (const rel of untrackedZ.toString('utf8').split('\0').filter(Boolean)) {
@@ -348,21 +365,17 @@ const flaggedIndexLag = (top, runGit, lstat, readlink) => {
348
365
  return { paths: lagging, submodules: laggingSubmodules, flagged: laggingFlags };
349
366
  };
350
367
 
351
- export const computeWorkingState = (cwd, { lstat = lstatSync, readlink = readlinkSync, runGit = null } = {}) => {
352
- const run = runGit ?? ((args, dir, input, env) => spawnSync('git', args, { cwd: dir, input, env: env ?? process.env, maxBuffer: GIT_MAX_BUFFER, windowsHide: true }));
353
- const topRun = run(['rev-parse', '--show-toplevel'], cwd);
354
- if (topRun.error || topRun.status !== 0) return null;
355
- const top = topRun.stdout.toString('utf8').replace(/\r?\n$/, '');
368
+ export const computeWorkingState = (cwd, { lstat = lstatSync, readlink = readlinkSync, runGit = null, env = process.env } = {}) => {
369
+ const run = guardedRunner(runGit ?? defaultRunGit, env);
370
+ const top = locateWorkTree(cwd, run, env);
371
+ if (top == null) return null;
356
372
  // `--ignore-submodules=none` on the staged probe too: a config-hidden STAGED gitlink would
357
373
  // otherwise make stagedDirty false, and isTreeClean would call such a tree clean.
358
374
  const staged = run(['diff', '--cached', '--quiet', '--ignore-submodules=none'], top);
359
375
  // ONLY 0 or 1 is a usable answer: a signal-killed probe reports status null, which the old
360
376
  // `> 1` guard let through as "nothing staged" — a fail-OPEN the whole arm cannot afford.
361
377
  if (staged.error || (staged.status !== 0 && staged.status !== 1)) return null;
362
- const buf = (args) => {
363
- const r = run(args, top);
364
- return r.error || r.status !== 0 ? null : r.stdout;
365
- };
378
+ const buf = (args) => stdoutOf(run(args, top));
366
379
  // The FULL probe forces submodules back in: diff.ignoreSubmodules / submodule.<n>.ignore would
367
380
  // otherwise erase a dirty submodule from the comparison entirely.
368
381
  const changedZ = buf(['diff', '--name-only', '-z', '--ignore-submodules=none']);
@@ -370,7 +383,7 @@ export const computeWorkingState = (cwd, { lstat = lstatSync, readlink = readlin
370
383
  const untrackedZ = buf(['ls-files', '--others', '--exclude-standard', '-z']);
371
384
  if (changedZ == null || plainZ == null || untrackedZ == null) return null;
372
385
  const split = (b) => b.toString('utf8').split('\0').filter(Boolean);
373
- const changed = split(changedZ);
386
+ const changed = [...new Set(split(changedZ))]; // an unmerged path is listed once per conflict side
374
387
  const plain = new Set(split(plainZ));
375
388
  const flagged = flaggedIndexLag(top, run, lstat, readlink);
376
389
  if (flagged == null) return null;
@@ -950,6 +963,7 @@ export const runRedProof = ({ cwd = process.cwd(), env = process.env, testId } =
950
963
  // captured BEFORE the runs and re-checked after them; any drift refuses below.
951
964
  const base = resolveBase(cwd);
952
965
  const fingerprint = computeTreeFingerprint(cwd);
966
+ if (fingerprint == null) throw stop('cannot compute the tree fingerprint — the git location is not a work tree; nothing was observed, no runs were spent');
953
967
  const { entry, file, resolveReason } = probeBound({ testId, rootTop, env, reruns, timeoutS });
954
968
  const verdict = probeVerdict(entry);
955
969
  const counts = `${entry.greens} green / ${entry.reds} red / ${entry.timeouts} timed out / ${entry.runs - entry.greens - entry.reds - entry.timeouts} unresolved of ${entry.runs} run(s)`;
@@ -974,7 +988,6 @@ export const runRedProof = ({ cwd = process.cwd(), env = process.env, testId } =
974
988
  `${entry.timeouts > 0 ? 'raise the timeout or make the test faster' : 'replace the flaky test'}, then re-observe. Nothing was recorded.`,
975
989
  );
976
990
  }
977
- if (fingerprint == null) throw stop('cannot compute the tree fingerprint — not a git work tree');
978
991
  // Post-run drift recheck: fingerprint, base, and the test-file bytes must all equal the
979
992
  // pre-run capture — a mutating test / parallel edit / commit would otherwise mint a record
980
993
  // whose fields attest different trees.
@@ -24,6 +24,7 @@ import { lstatSync, readFileSync, realpathSync } from 'node:fs';
24
24
  import { createHash } from 'node:crypto';
25
25
  import { join } from 'node:path';
26
26
  import { spawnSync } from 'node:child_process';
27
+ import { GIT_MAX_BUFFER } from './git-env.mjs';
27
28
  import { computeChangedSurface } from './changed-surface.mjs';
28
29
  import { isDirectRun } from './direct-run.mjs';
29
30
  import { lcovCoveredMap, uncoveredChangedFromLcov } from './lcov.mjs';
@@ -47,7 +48,6 @@ const usageFail = (message) => Object.assign(new Error(`[agent-workflow-kit] ${m
47
48
 
48
49
  export const LCOV_BASENAME = 'agent-workflow-lcov.info';
49
50
 
50
- const GIT_MAX_BUFFER = 256 * 1024 * 1024;
51
51
  const gitLine = (args, cwd) => {
52
52
  const r = spawnSync('git', args, { cwd, maxBuffer: GIT_MAX_BUFFER, windowsHide: true });
53
53
  if (r.error || r.status !== 0) return null;
@@ -0,0 +1,269 @@
1
+ import { join, isAbsolute, normalize, sep } from 'node:path';
2
+ import { spawnSync } from 'node:child_process';
3
+
4
+ import {
5
+ validateDelegationRecord, canonicalDelegationDigest, allowedSuccessorKinds,
6
+ isThreadTerminalRecord,
7
+ } from './dispatch-record.mjs';
8
+ import { readRegularFileNoFollow } from './fs-read-nofollow.mjs';
9
+ import { GIT_MAX_BUFFER } from './git-env.mjs';
10
+
11
+ export const DELEGATION_STORE_STOP = 'DELEGATION_STORE_STOP';
12
+ export const delegationStoreStop = (message) => Object.assign(new Error(`[agent-workflow-kit] ${message}`), { name: 'DelegationStoreStop', code: DELEGATION_STORE_STOP });
13
+ const stop = delegationStoreStop;
14
+
15
+ export const DELEGATION_STORE_BASENAME = 'agent-workflow-delegation.jsonl';
16
+ const runGit = (args, cwd, spawn = spawnSync) => spawn('git', args, { cwd, maxBuffer: GIT_MAX_BUFFER, windowsHide: true });
17
+ const readGitLine = (args, cwd, spawn) => {
18
+ const result = runGit(args, cwd, spawn);
19
+ return result.error || result.status !== 0 ? null : result.stdout.toString('utf8').replace(/\r?\n$/, '');
20
+ };
21
+
22
+ export const resolveDelegationStorePath = (cwd, env = process.env) => {
23
+ if (env.AW_DELEGATION_STORE) {
24
+ if (!isAbsolute(env.AW_DELEGATION_STORE)) {
25
+ 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)`);
26
+ }
27
+ const normalized = normalize(env.AW_DELEGATION_STORE);
28
+ if (normalized.endsWith(sep) || normalized.endsWith('/')) {
29
+ 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)`);
30
+ }
31
+ return normalized;
32
+ }
33
+ if (readGitLine(['rev-parse', '--is-inside-work-tree'], cwd) !== 'true') return null;
34
+ const commonDir = readGitLine(['rev-parse', '--path-format=absolute', '--git-common-dir'], cwd);
35
+ return commonDir == null ? null : join(commonDir, DELEGATION_STORE_BASENAME);
36
+ };
37
+
38
+ const parseDelegationLine = (line, index) => {
39
+ if (line.trim() === '') return { empty: true };
40
+ try {
41
+ const record = JSON.parse(line);
42
+ const verdict = validateDelegationRecord(record);
43
+ return verdict.ok
44
+ ? { record, recordLine: index + 1 }
45
+ : { reason: `line ${index + 1}: ${verdict.reason}` };
46
+ } catch {
47
+ return { reason: `line ${index + 1}: invalid JSON` };
48
+ }
49
+ };
50
+
51
+ export const parseDelegationStoreText = (raw) => {
52
+ const parsed = String(raw).split('\n').map(parseDelegationLine);
53
+ const records = parsed.filter((item) => item.record !== undefined).map((item) => item.record);
54
+ const recordLines = parsed.filter((item) => item.recordLine !== undefined).map((item) => item.recordLine);
55
+ const malformedReasons = parsed.filter((item) => item.reason !== undefined).map((item) => item.reason);
56
+ return { records, recordLines, malformed: malformedReasons.length, malformedReasons };
57
+ };
58
+
59
+ const emptyRead = (outcome) => ({ outcome, records: [], recordLines: [], malformed: 0, malformedReasons: [] });
60
+
61
+ export const readDelegationStore = (path, io = {}) => {
62
+ const read = readRegularFileNoFollow(path, io);
63
+ if (read.outcome === 'absent') return emptyRead('absent');
64
+ if (read.outcome === 'foreign') {
65
+ const reason = `the store is a ${read.className}, not a regular file — refusing to read it (fail closed)`;
66
+ return { ...emptyRead('error'), reason, readError: reason };
67
+ }
68
+ if (read.outcome === 'error') return { ...emptyRead('error'), reason: read.code, readError: read.code };
69
+ const parsed = parseDelegationStoreText(read.content);
70
+ return parsed.malformed > 0
71
+ ? { outcome: 'error', reason: parsed.malformedReasons[0], ...parsed }
72
+ : { outcome: 'ok', ...parsed };
73
+ };
74
+
75
+ const describeGitFailure = (result) => {
76
+ if (result.error) return result.error.code ?? result.error.message;
77
+ const stderr = result.stderr?.toString('utf8').trim() ?? '';
78
+ if (stderr !== '') return stderr;
79
+ if (result.signal) return `signal ${result.signal}`;
80
+ return `exit ${result.status}`;
81
+ };
82
+
83
+ export const readHeadInstant = (cwd, spawn = spawnSync) => {
84
+ const inside = runGit(['rev-parse', '--is-inside-work-tree'], cwd, spawn);
85
+ if (inside.error || inside.status !== 0 || inside.stdout.toString('utf8').trim() !== 'true') {
86
+ return { state: 'error', reason: `cannot resolve the git work tree (${describeGitFailure(inside)})` };
87
+ }
88
+ const head = runGit(['log', '-1', '--format=%ct'], cwd, spawn);
89
+ const raw = head.error || head.status !== 0 ? null : head.stdout.toString('utf8').trim();
90
+ if (raw !== null && /^\d+$/.test(raw)) return { state: 'ok', seconds: Number(raw) };
91
+ const status = runGit(['status', '--porcelain=v2', '--branch', '--untracked-files=no'], cwd, spawn);
92
+ if (!status.error && status.status === 0 && status.stdout.toString('utf8').split(/\r?\n/u).includes('# branch.oid (initial)')) {
93
+ return { state: 'unborn' };
94
+ }
95
+ return { state: 'error', reason: `cannot read HEAD committer time (${describeGitFailure(head)})` };
96
+ };
97
+
98
+ const THREAD_KINDS = ['dispatch', 'return', 'fold', 'degrade'];
99
+
100
+ export const delegationThreadState = (records, nonce) => {
101
+ const thread = records.filter((record) => THREAD_KINDS.includes(record.kind) && record.nonce === nonce);
102
+ const last = thread.at(-1) ?? null;
103
+ const dispatch = thread.find((record) => record.kind === 'dispatch') ?? null;
104
+ const terminal = last !== null && isThreadTerminalRecord(last);
105
+ return {
106
+ records: thread,
107
+ dispatch,
108
+ return: thread.find((record) => record.kind === 'return') ?? null,
109
+ closure: thread.find((record) => record.kind === 'fold' || record.kind === 'degrade') ?? null,
110
+ last,
111
+ terminal,
112
+ open: dispatch !== null && !terminal,
113
+ };
114
+ };
115
+
116
+ const findRetryChainOrigin = (records, current, seen = new Set([current.nonce])) => {
117
+ if (current.retryOf === null) return current;
118
+ const prior = records.find((record) => record.kind === 'dispatch' && record.nonce === current.retryOf);
119
+ if (prior === undefined || seen.has(prior.nonce)) return current;
120
+ return findRetryChainOrigin(records, prior, new Set([...seen, prior.nonce]));
121
+ };
122
+
123
+ const describeClosure = (last) =>
124
+ last.kind === 'fold' ? 'its fold'
125
+ : last.kind === 'degrade' ? 'its recorded degrade'
126
+ : `a terminal return (outcome "${last.outcome}")`;
127
+
128
+ const WAVE_SCOPED_KINDS = ['dispatch', 'observation', 'degrade'];
129
+
130
+ export const delegationSemanticPreflight = ({ records, snapshot, storePath }) => {
131
+ const digest = canonicalDelegationDigest(snapshot);
132
+ if (records.some((record) => canonicalDelegationDigest(record) === digest)) {
133
+ 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`);
134
+ }
135
+ const registration = records.find((record) => record.kind === 'pre-registration' && record.waveId === snapshot.waveId);
136
+ if (snapshot.kind === 'pre-registration' && registration !== undefined) {
137
+ 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`);
138
+ }
139
+ if (WAVE_SCOPED_KINDS.includes(snapshot.kind)) {
140
+ if (registration === undefined) {
141
+ 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`);
142
+ }
143
+ if (!registration.stepClasses.includes(snapshot.stepClass)) {
144
+ 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`);
145
+ }
146
+ }
147
+ const nonce = typeof snapshot.nonce === 'string' ? snapshot.nonce : null;
148
+ if (nonce === null) return;
149
+ const state = delegationThreadState(records, nonce);
150
+ if (snapshot.kind === 'dispatch') {
151
+ if (state.dispatch !== null) {
152
+ 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`);
153
+ }
154
+ if (snapshot.retryOf !== null) {
155
+ const prior = delegationThreadState(records, snapshot.retryOf);
156
+ if (prior.dispatch === null) {
157
+ 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`);
158
+ }
159
+ const successor = records.find((record) => record.kind === 'dispatch' && record.retryOf === snapshot.retryOf);
160
+ if (successor !== undefined) {
161
+ 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`);
162
+ }
163
+ if (!prior.terminal) {
164
+ 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`);
165
+ }
166
+ if (prior.last.kind === 'fold') {
167
+ 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`);
168
+ }
169
+ if (snapshot.retryIndex !== prior.dispatch.retryIndex + 1) {
170
+ 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`);
171
+ }
172
+ if (snapshot.waveId !== prior.dispatch.waveId) {
173
+ 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`);
174
+ }
175
+ if (snapshot.stepClass !== prior.dispatch.stepClass) {
176
+ 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`);
177
+ }
178
+ const origin = findRetryChainOrigin(records, prior.dispatch);
179
+ if (snapshot.retryIndex > origin.retryCap) {
180
+ 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`);
181
+ }
182
+ if (prior.return !== null && prior.return.outcome === 'contract-refusal' && snapshot.contractDigest === prior.dispatch.contractDigest) {
183
+ 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`);
184
+ }
185
+ }
186
+ return;
187
+ }
188
+ if (state.dispatch === null) {
189
+ 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`);
190
+ }
191
+ if (snapshot.kind === 'return' && state.return !== null) {
192
+ 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`);
193
+ }
194
+ const allowed = allowedSuccessorKinds(state.last);
195
+ if (!allowed.includes(snapshot.kind)) {
196
+ throw stop(state.terminal
197
+ ? `refusing a ${snapshot.kind}: thread "${nonce}" is already closed by ${describeClosure(state.last)} — a closed thread never absorbs another record; nothing was written`
198
+ : `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`);
199
+ }
200
+ if (snapshot.kind === 'return') {
201
+ for (const field of ['backend', 'contractDigest', 'preTreeDigest']) {
202
+ if (snapshot[field] !== state.dispatch[field]) {
203
+ 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`);
204
+ }
205
+ }
206
+ if (state.dispatch.baselineClean === false && snapshot.metric.eligible) {
207
+ 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`);
208
+ }
209
+ if (state.dispatch.baselineClean !== false && snapshot.metric.ineligibleReason === 'dirty-baseline') {
210
+ 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`);
211
+ }
212
+ }
213
+ if (snapshot.kind === 'degrade') {
214
+ for (const field of ['waveId', 'stepClass']) {
215
+ if (snapshot[field] !== state.dispatch[field]) {
216
+ 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`);
217
+ }
218
+ }
219
+ }
220
+ if (snapshot.kind === 'fold') {
221
+ const target = records.find((record) => canonicalDelegationDigest(record) === snapshot.returnDigest);
222
+ if (target === undefined) {
223
+ 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`);
224
+ }
225
+ if (target.kind !== 'return') {
226
+ throw stop(`refusing a fold: returnDigest resolves to a ${target.kind} record, not a return — a fold folds a return; nothing was written`);
227
+ }
228
+ if (target.nonce !== nonce) {
229
+ 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`);
230
+ }
231
+ if (snapshot.treeDigestAtFold !== target.postTreeDigest) {
232
+ 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`);
233
+ }
234
+ }
235
+ };
236
+
237
+ export const auditDelegationStoreSemantics = ({ records = [], recordLines = [], storePath = '(store)' } = {}) =>
238
+ records.reduce((verdict, record, index) => {
239
+ if (!verdict.ok) return verdict;
240
+ try {
241
+ delegationSemanticPreflight({ records: records.slice(0, index), snapshot: record, storePath });
242
+ return verdict;
243
+ } catch (error) {
244
+ return { ok: false, line: recordLines[index] ?? index + 1, reason: error.message };
245
+ }
246
+ }, { ok: true });
247
+
248
+ export const readDelegationLedger = (cwd, env, deps = {}) => {
249
+ try {
250
+ const {
251
+ resolveStore = resolveDelegationStorePath,
252
+ readStore = readDelegationStore,
253
+ audit = auditDelegationStoreSemantics,
254
+ readHead = readHeadInstant,
255
+ } = deps;
256
+ const storePath = resolveStore(cwd, env);
257
+ if (storePath === null) return { state: 'error', reason: 'the delegation ledger path is unresolvable (fail closed)' };
258
+ const read = readStore(storePath);
259
+ if (read.outcome === 'absent') return { state: 'absent' };
260
+ if (read.outcome === 'error' || read.readError != null || read.malformed > 0) {
261
+ return { state: 'error', reason: read.reason ?? read.readError ?? read.malformedReasons[0] };
262
+ }
263
+ const audited = audit({ records: read.records, recordLines: read.recordLines, storePath });
264
+ if (!audited.ok) return { state: 'error', reason: `line ${audited.line}: ${audited.reason}` };
265
+ return { state: 'ok', records: read.records, head: readHead(cwd) };
266
+ } catch (error) {
267
+ return { state: 'error', reason: error.message };
268
+ }
269
+ };