@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.
- package/CHANGELOG.md +85 -0
- package/SKILL.md +9 -1
- package/bridges/antigravity-cli-bridge/SKILL.md +1 -1
- package/bridges/antigravity-cli-bridge/bin/agy-review.sh +6 -6
- package/bridges/antigravity-cli-bridge/capability.json +1 -1
- package/bridges/codex-cli-bridge/SKILL.md +1 -1
- package/bridges/codex-cli-bridge/bin/codex-exec.sh +1 -1
- package/bridges/codex-cli-bridge/bin/codex-review.sh +6 -6
- package/bridges/codex-cli-bridge/capability.json +1 -1
- package/capability.json +1 -1
- package/package.json +1 -1
- package/references/modes/commit-guard.md +2 -2
- package/references/modes/control-bytes.md +91 -0
- package/references/modes/flow-writer.md +16 -4
- package/references/modes/procedures.md +2 -0
- package/references/modes/review-state.md +1 -1
- package/references/modes/robustness-brief.md +61 -0
- package/references/modes/velocity.md +2 -0
- package/references/robustness-literals.json +107 -0
- package/tools/changed-surface.mjs +1 -1
- package/tools/commands.mjs +14 -0
- package/tools/commit-guard.mjs +20 -9
- package/tools/control-bytes.mjs +264 -0
- package/tools/core-evidence.mjs +33 -20
- package/tools/coverage-check.mjs +1 -1
- package/tools/dispatch-store-read.mjs +2 -2
- package/tools/flow-check-git-lane.mjs +2 -1
- package/tools/flow-delta-proof.mjs +4 -4
- package/tools/flow-record-shape.mjs +44 -2
- package/tools/flow-round-gates.mjs +151 -0
- package/tools/flow-store-read.mjs +1 -1
- package/tools/flow-writer.mjs +235 -82
- package/tools/fold-scope.mjs +7 -3
- package/tools/fs-read-nofollow.mjs +56 -4
- package/tools/git-env.mjs +120 -0
- package/tools/grounding.mjs +4 -3
- package/tools/plan-shape-facts.mjs +23 -10
- package/tools/plan-shape.mjs +8 -0
- package/tools/procedures.mjs +18 -4
- package/tools/repo-lex.mjs +29 -0
- package/tools/review-state.mjs +43 -34
- package/tools/robustness-brief.mjs +209 -0
- package/tools/robustness-literals.mjs +101 -0
- package/tools/sandbox-masks.mjs +2 -1
- package/tools/set-flow.mjs +2 -1
- package/tools/source-size-scope.mjs +19 -25
- package/tools/tracked-tree-census.mjs +3 -3
- package/tools/velocity-profile.mjs +5 -2
- package/tools/worktree-handoff-return.mjs +2 -2
- 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:
|
|
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(
|
|
170
|
-
const unstaged = gitBuf(
|
|
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
|
|
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 (
|
|
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;
|