@sabaiway/agent-workflow-kit 10.6.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 (50) hide show
  1. package/CHANGELOG.md +85 -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/flow-writer.md +16 -4
  15. package/references/modes/procedures.md +2 -0
  16. package/references/modes/review-state.md +1 -1
  17. package/references/modes/robustness-brief.md +61 -0
  18. package/references/modes/velocity.md +2 -0
  19. package/references/robustness-literals.json +107 -0
  20. package/tools/changed-surface.mjs +1 -1
  21. package/tools/commands.mjs +14 -0
  22. package/tools/commit-guard.mjs +20 -9
  23. package/tools/control-bytes.mjs +264 -0
  24. package/tools/core-evidence.mjs +33 -20
  25. package/tools/coverage-check.mjs +1 -1
  26. package/tools/dispatch-store-read.mjs +2 -2
  27. package/tools/flow-check-git-lane.mjs +2 -1
  28. package/tools/flow-delta-proof.mjs +4 -4
  29. package/tools/flow-record-shape.mjs +44 -2
  30. package/tools/flow-round-gates.mjs +151 -0
  31. package/tools/flow-store-read.mjs +1 -1
  32. package/tools/flow-writer.mjs +235 -82
  33. package/tools/fold-scope.mjs +7 -3
  34. package/tools/fs-read-nofollow.mjs +56 -4
  35. package/tools/git-env.mjs +120 -0
  36. package/tools/grounding.mjs +4 -3
  37. package/tools/plan-shape-facts.mjs +23 -10
  38. package/tools/plan-shape.mjs +8 -0
  39. package/tools/procedures.mjs +18 -4
  40. package/tools/repo-lex.mjs +29 -0
  41. package/tools/review-state.mjs +43 -34
  42. package/tools/robustness-brief.mjs +209 -0
  43. package/tools/robustness-literals.mjs +101 -0
  44. package/tools/sandbox-masks.mjs +2 -1
  45. package/tools/set-flow.mjs +2 -1
  46. package/tools/source-size-scope.mjs +19 -25
  47. package/tools/tracked-tree-census.mjs +3 -3
  48. package/tools/velocity-profile.mjs +5 -2
  49. package/tools/worktree-handoff-return.mjs +2 -2
  50. package/tools/worktrees.mjs +2 -2
@@ -1,18 +1,18 @@
1
1
  import { join, isAbsolute, normalize, sep } from 'node:path';
2
2
  import { spawnSync } from 'node:child_process';
3
+
3
4
  import {
4
5
  validateDelegationRecord, canonicalDelegationDigest, allowedSuccessorKinds,
5
6
  isThreadTerminalRecord,
6
7
  } from './dispatch-record.mjs';
7
8
  import { readRegularFileNoFollow } from './fs-read-nofollow.mjs';
9
+ import { GIT_MAX_BUFFER } from './git-env.mjs';
8
10
 
9
11
  export const DELEGATION_STORE_STOP = 'DELEGATION_STORE_STOP';
10
12
  export const delegationStoreStop = (message) => Object.assign(new Error(`[agent-workflow-kit] ${message}`), { name: 'DelegationStoreStop', code: DELEGATION_STORE_STOP });
11
13
  const stop = delegationStoreStop;
12
14
 
13
15
  export const DELEGATION_STORE_BASENAME = 'agent-workflow-delegation.jsonl';
14
- const GIT_MAX_BUFFER = 256 * 1024 * 1024;
15
-
16
16
  const runGit = (args, cwd, spawn = spawnSync) => spawn('git', args, { cwd, maxBuffer: GIT_MAX_BUFFER, windowsHide: true });
17
17
  const readGitLine = (args, cwd, spawn) => {
18
18
  const result = runGit(args, cwd, spawn);
@@ -4,13 +4,14 @@
4
4
  // pure decision cores can never reach a git spawn through it.
5
5
 
6
6
  import { spawnSync } from 'node:child_process';
7
+ import { GIT_MAX_BUFFER } from './git-env.mjs';
7
8
 
8
9
  const short = (digest) => `${digest.slice(0, 12)}…`;
9
10
 
10
11
  // computeChangedSurface exists for COVERAGE and excludes test files by design — the base-
11
12
  // intersection inputs come from these helpers instead: every changed path counts, tests included.
12
13
  const gitPathList = (args, cwd) => {
13
- const r = spawnSync('git', args, { cwd, maxBuffer: 256 * 1024 * 1024, windowsHide: true });
14
+ const r = spawnSync('git', args, { cwd, maxBuffer: GIT_MAX_BUFFER, windowsHide: true });
14
15
  if (r.error || r.status !== 0) return null;
15
16
  return r.stdout.toString('utf8').split('\0').filter(Boolean);
16
17
  };
@@ -13,15 +13,15 @@ import { createHash } from 'node:crypto';
13
13
  import { readFileSync, lstatSync, readlinkSync } from 'node:fs';
14
14
  import { join } from 'node:path';
15
15
  import { spawnSync } from 'node:child_process';
16
+ import { GIT_MAX_BUFFER } from './git-env.mjs';
16
17
  import { lstatNoFollow } from './atomic-write.mjs';
17
18
  import { FLOW_SCHEMA_VERSION, canonicalFlowDigest } from './flow-record.mjs';
18
- import { isNeverCommittableStat, isBinaryFile, lexicalRepoRelative, resolveBase } from './core-evidence.mjs';
19
+ import { FINGERPRINT_CACHED_DIFF_ARGV, FINGERPRINT_UNSTAGED_DIFF_ARGV, isNeverCommittableStat, isBinaryFile, lexicalRepoRelative, resolveBase } from './core-evidence.mjs';
19
20
  import { flowStoreStop, gitLine, describeNonRegular } from './flow-store-read.mjs';
20
21
  import { appendFlowRecord } from './flow-append.mjs';
21
22
 
22
23
  const stop = flowStoreStop;
23
24
 
24
- const GIT_MAX_BUFFER = 256 * 1024 * 1024;
25
25
  const gitBuf = (args, cwd) => {
26
26
  const r = spawnSync('git', args, { cwd, maxBuffer: GIT_MAX_BUFFER, windowsHide: true });
27
27
  if (r.error || r.status !== 0) return null;
@@ -166,8 +166,8 @@ const untrackedEntryChunks = (top, rel, lstat) => {
166
166
  const captureFingerprintPieces = (cwd, { lstat = lstatSync } = {}) => {
167
167
  const top = gitLine(['rev-parse', '--show-toplevel'], cwd);
168
168
  if (top == null) return null;
169
- const staged = gitBuf(['diff', '--cached', '--no-ext-diff'], top);
170
- const unstaged = gitBuf(['diff', '--no-ext-diff'], top);
169
+ const staged = gitBuf(FINGERPRINT_CACHED_DIFF_ARGV, top);
170
+ const unstaged = gitBuf(FINGERPRINT_UNSTAGED_DIFF_ARGV, top);
171
171
  const untrackedZ = gitBuf(['ls-files', '--others', '--exclude-standard', '-z'], top);
172
172
  if (staged == null || unstaged == null || untrackedZ == null) return null;
173
173
  const entries = untrackedZ.toString('utf8').split('\0').filter(Boolean)
@@ -79,6 +79,7 @@ const FIELD_CHECKS = {
79
79
  attemptIndex: { ok: (v) => Number.isInteger(v) && v >= 1, want: 'a positive integer attempt index (monotonic per counting context)' },
80
80
  status: { ok: (v) => v === 'green' || v === 'red', want: 'the closed enum green | red' },
81
81
  diagnosis: { ok: isNonEmptyString, want: 'a non-empty diagnosis statement (Decision 8 — the recorded continuation past two reds)' },
82
+ walk: { ok: isPlainObject, want: 'the closed walk object' },
82
83
  };
83
84
 
84
85
  const CHAIN_COMMON_FIELDS = ['planId', 'cycle', 'round', 'commitEpoch', 'owner', 'base', 'timestamp'];
@@ -171,33 +172,54 @@ const validateDispatches = (label, list) => {
171
172
  // The per-finding disposition ledger (#13/#33): every council finding lands as exactly one of the
172
173
  // three closed arms, each carrying its proof (a consult-attestation/red-proof digest, a debt entry,
173
174
  // or a stated rejection reason).
175
+ // The new arms are escalated with its override, and custody-lost keyed on receiptDigest with no findingDigest.
174
176
  const DISPOSITION_KEYS = {
175
177
  folded: ['findingDigest', 'action', 'proofKind', 'proofDigest'],
176
178
  queued: ['findingDigest', 'action', 'debtId', 'debtDigest'],
177
179
  rejected: ['findingDigest', 'action', 'reason'],
180
+ escalated: ['findingDigest', 'action', 'overrideDigest'],
181
+ 'custody-lost': ['action', 'receiptDigest', 'findingManifestDigest', 'reason'],
178
182
  };
183
+ const PROOF_KINDS = ['consult-attestation', 'red-proof'];
184
+ const QUEUED_PROOF_KEYS = ['claim', 'proofKind', 'proofDigest'];
185
+ const LOSS_REASONS = ['absent', 'foreign', 'error', 'malformed', 'foreign-identity', 'swapped'];
179
186
 
180
187
  const validateDispositions = (label, list) => {
181
188
  const seenFindings = new Set();
189
+ const seenLostReceipts = new Set();
182
190
  for (let i = 0; i < list.length; i += 1) {
183
191
  const d = list[i];
184
192
  const at = `${label}: dispositions[${i}]`;
185
193
  if (!isPlainObject(d)) return refuse(`${at} must be an object`);
186
- const armKeys = Object.hasOwn(DISPOSITION_KEYS, d.action) ? DISPOSITION_KEYS[d.action] : undefined;
194
+ const baseKeys = Object.hasOwn(DISPOSITION_KEYS, d.action) ? DISPOSITION_KEYS[d.action] : undefined;
195
+ const armKeys = d.action === 'queued' && QUEUED_PROOF_KEYS.some((key) => key in d) ? [...baseKeys, ...QUEUED_PROOF_KEYS] : baseKeys;
187
196
  if (armKeys === undefined) return refuse(`${at}: action must be one of ${Object.keys(DISPOSITION_KEYS).join(' | ')} (got ${JSON.stringify(d.action)}) — an inherited prototype key never resolves an arm (fail closed)`);
188
197
  const stray = Object.keys(d).find((k) => !armKeys.includes(k));
189
198
  if (stray !== undefined) return refuse(`${at}: unknown field "${stray}" — the ${d.action} arm's key set is closed`);
190
199
  const missing = armKeys.find((k) => !(k in d));
191
200
  if (missing !== undefined) return refuse(`${at}: missing field "${missing}"`);
201
+ if (d.action === 'custody-lost') {
202
+ if (!isHex64(d.receiptDigest)) return refuse(`${at}: receiptDigest must be the 64-hex digest of the lost receipt`);
203
+ if (!isHex64(d.findingManifestDigest)) return refuse(`${at}: findingManifestDigest must be the 64-hex ledger digest of the lost manifest`);
204
+ if (!LOSS_REASONS.includes(d.reason)) return refuse(`${at}: reason must be one of ${LOSS_REASONS.join(' | ')}`);
205
+ if (seenLostReceipts.has(d.receiptDigest)) return refuse(`${at}: duplicate receiptDigest — one custody-lost entry per lost receipt`);
206
+ seenLostReceipts.add(d.receiptDigest);
207
+ continue;
208
+ }
192
209
  if (!isHex64(d.findingDigest)) return refuse(`${at}: findingDigest must be the 64-hex digest of the finding`);
193
210
  if (seenFindings.has(d.findingDigest)) return refuse(`${at}: duplicate findingDigest — every finding gets exactly one disposition, whichever the arm`);
194
211
  seenFindings.add(d.findingDigest);
195
212
  if (d.action === 'folded') {
196
- if (d.proofKind !== 'consult-attestation' && d.proofKind !== 'red-proof') return refuse(`${at}: proofKind must be consult-attestation | red-proof (the fold's proof record class)`);
213
+ if (!PROOF_KINDS.includes(d.proofKind)) return refuse(`${at}: proofKind must be consult-attestation | red-proof (the fold's proof record class)`);
197
214
  if (!isHex64(d.proofDigest)) return refuse(`${at}: proofDigest must be the 64-hex digest of the proof record`);
198
215
  } else if (d.action === 'queued') {
199
216
  if (!isNonEmptyString(d.debtId)) return refuse(`${at}: debtId must be the non-empty stable debt-queue id`);
200
217
  if (!isHex64(d.debtDigest)) return refuse(`${at}: debtDigest must be the 64-hex digest of the debt entry`);
218
+ if ('claim' in d && !isNonEmptyString(d.claim)) return refuse(`${at}: claim must be a non-empty invariant`);
219
+ if ('proofKind' in d && !PROOF_KINDS.includes(d.proofKind)) return refuse(`${at}: proofKind must be consult-attestation | red-proof`);
220
+ if ('proofDigest' in d && !isHex64(d.proofDigest)) return refuse(`${at}: proofDigest must be the 64-hex digest of the proof record`);
221
+ } else if (d.action === 'escalated') {
222
+ if (!isHex64(d.overrideDigest)) return refuse(`${at}: overrideDigest must be the 64-hex digest of the maintainer override`);
201
223
  } else if (!isNonEmptyString(d.reason)) {
202
224
  return refuse(`${at}: reason must be a non-empty statement of why the finding is rejected`);
203
225
  }
@@ -205,6 +227,24 @@ const validateDispositions = (label, list) => {
205
227
  return { ok: true };
206
228
  };
207
229
 
230
+ const validateWalk = (walk) => {
231
+ const keys = ['listVersion', 'rows', 'uncovered'];
232
+ if (!isPlainObject(walk)) return refuse('internal-attestation: walk must be an object');
233
+ const stray = Object.keys(walk).find((key) => !keys.includes(key));
234
+ if (stray !== undefined) return refuse(`internal-attestation: walk has unknown field "${stray}"`);
235
+ const missing = keys.find((key) => !(key in walk));
236
+ if (missing !== undefined) return refuse(`internal-attestation: walk is missing field "${missing}"`);
237
+ if (!Number.isInteger(walk.listVersion) || walk.listVersion < 1) return refuse('internal-attestation: walk.listVersion must be a positive integer');
238
+ for (const name of ['rows', 'uncovered']) {
239
+ if (!Array.isArray(walk[name])) return refuse(`internal-attestation: walk.${name} must be an array`);
240
+ const itemKeys = name === 'rows' ? ['id', 'class', 'checked'] : ['id', 'class'];
241
+ for (const [i, item] of walk[name].entries()) {
242
+ if (!isPlainObject(item) || Object.keys(item).length !== itemKeys.length || !itemKeys.every((key) => key in item && isNonEmptyString(item[key]))) return refuse(`internal-attestation: walk.${name}[${i}] must be the closed {${itemKeys.join(', ')}} string object`);
243
+ }
244
+ }
245
+ return { ok: true };
246
+ };
247
+
208
248
  // The three-layer pre-state digest set (#60): HEAD entry, index entry, worktree bytes — null means
209
249
  // "no entry in that layer". tracked-ness is derived (a HEAD or index entry exists); the presence
210
250
  // class is the WORKTREE layer. The fingerprint domain (staged + unstaged + untracked) is exactly
@@ -272,8 +312,10 @@ export const validateFlowRecord = (record) => {
272
312
  }
273
313
  fieldToCheck.diagnosis = 'diagnosis';
274
314
  }
315
+ if (record.kind === 'internal-attestation' && 'walk' in record) fieldToCheck.walk = 'walk';
275
316
  const checked = checkFields(record.kind, record, fieldToCheck);
276
317
  if (!checked.ok) return checked;
318
+ if (record.kind === 'internal-attestation' && 'walk' in record) return validateWalk(record.walk);
277
319
  if (record.kind === 'down-mark') {
278
320
  if (!isCanonicalInstant(record.timestamp)) return refuse('down-mark: timestamp must be a canonical UTC ISO instant (toISOString round-trip) — the TTL window needs comparable instants');
279
321
  if (Date.parse(record.expiresAt) <= Date.parse(record.timestamp)) return refuse('down-mark: expiresAt must be strictly after timestamp — an already-expired mark is refused at the record level');
@@ -0,0 +1,151 @@
1
+ import { createHash } from 'node:crypto';
2
+ import { escapeForDisplay } from './repo-lex.mjs';
3
+ import { SIGNALS, signalFor } from './review-rounds.mjs';
4
+ import { authoritativeFlowRecords, canonicalFlowDigest, flowRecordKey } from './flow-record.mjs';
5
+ import { isNonEmptyString, isPlainObject, refuse } from './flow-vocabulary.mjs';
6
+ import { isShipVerdict } from './core-evidence.mjs';
7
+
8
+ const sha256 = (value) => createHash('sha256').update(value, typeof value === 'string' ? 'utf8' : undefined).digest('hex');
9
+ const item = (text) => ({ text, digest: sha256(text) });
10
+ const linesOf = (payload) => String(payload).split('\n').map((line) => line.replace(/\r$/, ''));
11
+ const codexSchema = (payload) => {
12
+ let decoded;
13
+ try {
14
+ decoded = JSON.parse(payload);
15
+ } catch {
16
+ return null;
17
+ }
18
+ const severities = ['blocker', 'major', 'minor', 'nit'];
19
+ return isPlainObject(decoded)
20
+ && ['ship', 'revise', 'rethink'].includes(decoded.verdict)
21
+ && Array.isArray(decoded.findings)
22
+ && decoded.findings.every((finding) => isPlainObject(finding) && severities.includes(finding.severity))
23
+ ? decoded : null;
24
+ };
25
+
26
+ export const blockingItems = (backend, payload) => {
27
+ if (backend === 'codex') {
28
+ const decoded = codexSchema(payload);
29
+ if (decoded) {
30
+ const selected = decoded.findings.filter(({ severity }) => severity === 'blocker' || severity === 'major');
31
+ if (!selected.every(({ issue }) => isNonEmptyString(issue))) return refuse('codex schema blocking item issue is empty or not a string');
32
+ return { ok: true, mode: 'schema', items: selected.map(({ issue }) => item(issue)) };
33
+ }
34
+ return { ok: true, mode: 'text', items: linesOf(payload).filter((line) => /^\[(blocker|major)\](\s|$)/.test(line)).map(item) };
35
+ }
36
+ if (backend === 'agy') {
37
+ const lines = linesOf(payload);
38
+ const open = lines.findIndex((line) => /^### Blocking\s*$/.test(line));
39
+ const close = lines.findIndex((line, index) => index > open && /^### /.test(line));
40
+ const section = open < 0 ? [] : lines.slice(open + 1, close < 0 ? undefined : close);
41
+ return { ok: true, mode: 'text', items: section.filter((line) => /^[0-9]+[.)]/.test(line)).map(item) };
42
+ }
43
+ return refuse(`cannot enumerate backend ${JSON.stringify(backend)} — no blocking-item vocabulary predicate`);
44
+ };
45
+
46
+ export const findingQuoted = (backend, findings, quote) => findings.includes(quote)
47
+ || (backend === 'codex' && codexSchema(findings)?.findings.some(({ issue }) => issue === quote) === true);
48
+
49
+ const landed = (head, receipts) => head.dispatches
50
+ .filter(({ receiptDigest }) => receiptDigest !== null)
51
+ .map((entry) => ({ entry, receipt: receipts.get(entry.receiptDigest) }))
52
+ .filter(({ receipt }) => receipt !== undefined);
53
+
54
+ export const roundsForSignal = (heads, receipts) => {
55
+ const rounds = heads.map((head) => ({
56
+ fingerprint: head.fingerprint,
57
+ byBackend: Object.fromEntries(landed(head, receipts).map(({ entry, receipt }) => [entry.backend, receipt])),
58
+ }));
59
+ const backends = [...new Set((heads.length ? landed(heads.at(-1), receipts) : []).map(({ entry }) => entry.backend))];
60
+ return {
61
+ rounds,
62
+ obligation: backends.length ? { backends, perBackend: true, minShip: backends.length } : null,
63
+ };
64
+ };
65
+
66
+ const lossName = (read, expected) => {
67
+ if (read?.outcome !== 'ok') return read?.outcome ?? 'error';
68
+ return sha256(read.bytes) === expected ? null : 'swapped';
69
+ };
70
+ const quoteCovered = (ledger, digest) => ledger.some((entry) => entry.findingDigest === digest
71
+ && (['folded', 'rejected', 'escalated'].includes(entry.action)
72
+ || (entry.action === 'queued' && ['claim', 'proofKind', 'proofDigest'].every((key) => key in entry))));
73
+
74
+ export const capIssue = ({ heads, receipts, readManifest }) => {
75
+ const { rounds, obligation } = roundsForSignal(heads, receipts);
76
+ if (obligation === null) return { issue: null, advisories: [] };
77
+ const signal = signalFor(rounds, obligation, '<artifact>');
78
+ if (signal !== SIGNALS.CAP_REACHED && signal !== SIGNALS.CROSSOVER) return { issue: null, advisories: [] };
79
+ const head = heads.at(-1);
80
+ const ledger = head.dispositions;
81
+ const advisories = [];
82
+ const issues = [];
83
+ const uncovered = [];
84
+ for (const { entry, receipt } of landed(head, receipts)) {
85
+ if (ledger.some((disposition) => disposition.action === 'custody-lost' && disposition.receiptDigest === entry.receiptDigest)) continue;
86
+ const needsFallbackDisposition = !isShipVerdict(receipt.verdict) && ledger.length === 0;
87
+ if (!Number.isInteger(receipt.blocking) || receipt.blocking < 0) {
88
+ const why = `blocking ${JSON.stringify(receipt.blocking)} is not a non-negative integer for backend ${JSON.stringify(entry.backend)}`;
89
+ advisories.push(`advisory: ${why}`);
90
+ if (needsFallbackDisposition) issues.push(`enumeration fallback requires at least one disposition in round ${head.round}: ${why}`);
91
+ continue;
92
+ }
93
+ if (receipt.blocking === 0) continue;
94
+ const read = readManifest(entry.backend, entry.dispatchNonce);
95
+ const loss = lossName(read, entry.findingManifestDigest);
96
+ if (loss !== null) {
97
+ const covered = ledger.some((d) => d.action === 'custody-lost' && d.receiptDigest === entry.receiptDigest);
98
+ if (!covered) issues.push(`lost receipt ${entry.receiptDigest} (${loss}); recovery: round-land --dispose custody-lost --receipt ${entry.receiptDigest}`);
99
+ continue;
100
+ }
101
+ const enumerated = blockingItems(entry.backend, read.manifest.findings);
102
+ if (!enumerated.ok || enumerated.items.length !== receipt.blocking) {
103
+ const why = !enumerated.ok ? enumerated.reason : `enumerated count ${enumerated.items.length} differs from blocking ${receipt.blocking} for backend ${JSON.stringify(entry.backend)}`;
104
+ advisories.push(`advisory: ${why}`);
105
+ if (ledger.length === 0) issues.push(`enumeration fallback requires at least one disposition in round ${head.round}: ${why}`);
106
+ continue;
107
+ }
108
+ for (const found of enumerated.items) if (!quoteCovered(ledger, found.digest)) uncovered.push(found);
109
+ }
110
+ if (uncovered.length) {
111
+ issues.push(`uncovered blocking items:\n${uncovered.map(({ digest, text }) => `${digest} ${escapeForDisplay(text)}`).join('\n')}\nrecovery: round-land --dispose <folded|queued|rejected|escalated> --finding <the item text> on the SAME tree`);
112
+ }
113
+ return { issue: issues.length ? issues.join('\n') : null, advisories };
114
+ };
115
+
116
+ export const walkIssue = ({ records, planId, cycle, stepId, round, base, fingerprint }) => {
117
+ if (round <= 1) return null;
118
+ const coordinates = { kind: 'internal-attestation', planId, cycle, stepId, round: round - 1, base, fingerprint };
119
+ const wanted = flowRecordKey(coordinates);
120
+ const found = authoritativeFlowRecords(records).find((record) => flowRecordKey(record) === wanted);
121
+ return found?.walk ? null
122
+ : `round-open round ${round} requires an authoritative walk for round ${round - 1} at {planId=${planId}, cycle=${cycle}, stepId=${stepId}, round=${round - 1}, base=${base}, fingerprint=${fingerprint}}; recovery: internal-attestation ${planId} --walk <file> at the current tree, or --justification <text>`;
123
+ };
124
+
125
+ export const escalationIssue = ({ records, digest, planId, head }) => {
126
+ const override = records.find((record) => record.kind === 'maintainer-override' && canonicalFlowDigest(record) === digest);
127
+ if (!override) return { issue: `override digest ${digest} does not resolve to a maintainer-override`, dispatch: null };
128
+ const chain = records.find((record) => record.kind === 'chain' && canonicalFlowDigest(record) === override.chainRecord);
129
+ if (!chain || chain.planId !== planId) return { issue: `override ${digest} does not bind a chain record of plan ${planId}`, dispatch: null };
130
+ const dispatch = head.dispatches.find(({ receiptDigest }) => receiptDigest === override.vetoReceiptDigest);
131
+ return dispatch
132
+ ? { issue: null, dispatch }
133
+ : { issue: `override ${digest} does not bind a landed receipt of plan ${planId} round ${head.round}`, dispatch: null };
134
+ };
135
+
136
+ export const validateWalk = (walk, coverage) => {
137
+ if (!isPlainObject(walk) || Object.keys(walk).some((key) => !['listVersion', 'rows'].includes(key)) || !Array.isArray(walk.rows)) return refuse('walk must be the closed {listVersion, rows} object');
138
+ if (walk.listVersion !== coverage.listVersion) return refuse(`walk listVersion ${JSON.stringify(walk.listVersion)} does not match shipped listVersion ${coverage.listVersion}`);
139
+ for (const [i, entry] of walk.rows.entries()) {
140
+ if (!isPlainObject(entry) || Object.keys(entry).length !== 3 || !['id', 'class', 'checked'].every((key) => key in entry) || !isNonEmptyString(entry.id) || !isNonEmptyString(entry.class)) return refuse(`walk rows[${i}] must be the closed {id, class, checked} non-empty string object`);
141
+ if (!isNonEmptyString(entry.checked)) return refuse(`walk checked for ${entry.id}:${entry.class} must be a non-empty string`);
142
+ }
143
+ const judged = coverage.rows.filter((row) => !row.deleted && !row.absent);
144
+ for (const row of judged) {
145
+ for (const classId of new Set([...row.tagged, ...row.present])) {
146
+ if (!walk.rows.some((entry) => entry.id === row.id && entry.class === classId && isNonEmptyString(entry.checked))) return refuse(`walk is missing non-empty checked for ${row.id}:${classId}`);
147
+ }
148
+ }
149
+ const uncovered = judged.flatMap((row) => row.present.filter((classId) => !row.tagged.includes(classId)).map((classId) => ({ id: row.id, class: classId })));
150
+ return { ok: true, walk: { listVersion: coverage.listVersion, rows: walk.rows, uncovered } };
151
+ };
@@ -13,6 +13,7 @@ import { join, isAbsolute, normalize, basename, sep } from 'node:path';
13
13
  import { spawnSync } from 'node:child_process';
14
14
  import { validateFlowRecord, authoritativeFlowRecords } from './flow-record.mjs';
15
15
  import { readRegularFileNoFollow } from './fs-read-nofollow.mjs';
16
+ import { GIT_MAX_BUFFER } from './git-env.mjs';
16
17
 
17
18
  export { lstatNoFollowRead, describeNonRegular, readRegularFileNoFollow, readFileBytesNoFollow } from './fs-read-nofollow.mjs';
18
19
 
@@ -23,7 +24,6 @@ export const flowStoreStop = (message) => Object.assign(new Error(`[agent-workfl
23
24
  export const FLOW_STORE_BASENAME = 'agent-workflow-flow.jsonl';
24
25
  export const FLOW_LOCK_SUFFIX = '.lock';
25
26
 
26
- const GIT_MAX_BUFFER = 256 * 1024 * 1024;
27
27
  const gitBuf = (args, cwd) => {
28
28
  const r = spawnSync('git', args, { cwd, maxBuffer: GIT_MAX_BUFFER, windowsHide: true });
29
29
  if (r.error || r.status !== 0) return null;