@sabaiway/agent-workflow-kit 1.35.0 → 1.37.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 +70 -0
- package/README.md +2 -0
- package/SKILL.md +24 -14
- package/capability.json +1 -1
- package/package.json +1 -1
- package/references/modes/fold-completeness.md +22 -0
- package/references/modes/review-ledger.md +28 -0
- package/references/modes/set-autonomy.md +29 -0
- package/references/modes/velocity.md +13 -0
- package/tools/autonomy-config.mjs +306 -0
- package/tools/autonomy-write.mjs +27 -0
- package/tools/commands.mjs +21 -0
- package/tools/fold-completeness-run.mjs +526 -0
- package/tools/fold-completeness.mjs +364 -0
- package/tools/procedures.mjs +12 -3
- package/tools/review-ledger-write.mjs +261 -0
- package/tools/review-ledger.mjs +535 -0
- package/tools/seed-gates.mjs +45 -4
- package/tools/set-autonomy.mjs +195 -0
- package/tools/velocity-profile.mjs +468 -5
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// review-ledger-write.mjs — the SOLE filesystem WRITER for the review-round ledger (AD-045). It is
|
|
3
|
+
// the write half of the family read/write split (mirrors orchestration-config.mjs /
|
|
4
|
+
// orchestration-write.mjs): review-ledger.mjs (schema + read + decideStop + --check) is read-only and
|
|
5
|
+
// NEVER imports this module; an import-split test pins that. This module imports the read core the
|
|
6
|
+
// OTHER direction (the schema + decideStop + the tolerant reader) and appends records through the
|
|
7
|
+
// shared hardened atomic-write core (tools/atomic-write.mjs — exclusive-create tmp + rename, TOCTOU
|
|
8
|
+
// re-check, symlink STOPs). The ledger lives in the git dir (uncommittable by construction).
|
|
9
|
+
//
|
|
10
|
+
// Two record kinds, one JSONL ledger:
|
|
11
|
+
// recordRound — one review round: per-backend counts + verdict + degraded, finding-origin tally,
|
|
12
|
+
// findings[]. Binds to the canonical tree fingerprint. THE TEETH (Decision 5):
|
|
13
|
+
// REFUSES (typed STOP) while decideStop on the existing records is `triage-required`
|
|
14
|
+
// (an unclassified surviving blocking finding at/after the cap), and refuses ANY
|
|
15
|
+
// round beyond the hard-max ceiling unconditionally. Integrity binding (Decision 7):
|
|
16
|
+
// each NON-degraded backend needs a grounded code receipt for the current tree, so a
|
|
17
|
+
// round cannot be recorded for a tree no bridge reviewed.
|
|
18
|
+
// recordTriage — the classification that BREAKS the deadlock: each surviving blocking finding
|
|
19
|
+
// classified fixable-bug / inherent-layer-residual / escalate. No teeth (a triage is
|
|
20
|
+
// exactly what lets the next round proceed), no receipt binding (it reviews nothing).
|
|
21
|
+
//
|
|
22
|
+
// HONEST residual (stated, accepted — like review-state's): the ledger attests a review occurred; it
|
|
23
|
+
// does NOT prove the recorded COUNTS are truthful nor that a self-reported `degraded:true` is real.
|
|
24
|
+
// A self-discipline mechanism against silent process drift, not a security boundary.
|
|
25
|
+
//
|
|
26
|
+
// Dependency-free, Node >= 18. Every fs primitive is injectable (deps.*) so the guards are unit-
|
|
27
|
+
// testable. No side effects on import (the isDirectRun idiom).
|
|
28
|
+
|
|
29
|
+
import { readFileSync } from 'node:fs';
|
|
30
|
+
import { dirname } from 'node:path';
|
|
31
|
+
import { pathToFileURL } from 'node:url';
|
|
32
|
+
import { writeContainedFileAtomic } from './atomic-write.mjs';
|
|
33
|
+
import { computeTreeFingerprint, readReceipts, resolveReceiptsPath } from './review-state.mjs';
|
|
34
|
+
import {
|
|
35
|
+
REVIEW_CAP,
|
|
36
|
+
SCHEMA_VERSION,
|
|
37
|
+
resolveLedgerPath,
|
|
38
|
+
readLedger,
|
|
39
|
+
filterLoopRecords,
|
|
40
|
+
roundSequenceIntact,
|
|
41
|
+
decideStop,
|
|
42
|
+
validateRecord,
|
|
43
|
+
} from './review-ledger.mjs';
|
|
44
|
+
|
|
45
|
+
// The absolute WRITER ceiling (Decision 5): hard-max lives ONLY here — it is NOT a decideStop input.
|
|
46
|
+
// Even a fully-classified resolved-residual loop cannot reach a round beyond this.
|
|
47
|
+
export const HARD_MAX = 3;
|
|
48
|
+
const DEFAULT_ACTIVITY = 'plan-execution';
|
|
49
|
+
|
|
50
|
+
// A typed STOP — a deliberate refusal we surface (the teeth / a malformed record / a missing
|
|
51
|
+
// receipt), distinct from a native fs error. The codebase's typed-error idiom (no classes).
|
|
52
|
+
export const LEDGER_WRITE_STOP = 'LEDGER_WRITE_STOP';
|
|
53
|
+
const stop = (message) => Object.assign(new Error(`[agent-workflow-kit] ${message}`), { name: 'LedgerWriteStop', code: LEDGER_WRITE_STOP });
|
|
54
|
+
|
|
55
|
+
// A tagged usage failure (exit 2) for the CLI parser.
|
|
56
|
+
const usageFail = (message) => Object.assign(new Error(`[agent-workflow-kit] ${message}`), { exitCode: 2 });
|
|
57
|
+
|
|
58
|
+
const isoNow = () => new Date().toISOString();
|
|
59
|
+
|
|
60
|
+
// ── the append primitive (whole-file read → add one JSONL line → atomic rewrite) ────────────────
|
|
61
|
+
// The ledger is a git-dir JSONL, never a docs/ai file, so the append reads the current file, adds the
|
|
62
|
+
// new line in memory, and rewrites through the contained-write core (root = the ledger file's dir).
|
|
63
|
+
const appendRecord = (ledgerPath, record, deps = {}) => {
|
|
64
|
+
const readFile = deps.readFile ?? readFileSync;
|
|
65
|
+
let existing = '';
|
|
66
|
+
try {
|
|
67
|
+
existing = readFile(ledgerPath, 'utf8');
|
|
68
|
+
} catch (err) {
|
|
69
|
+
// Only a truly-absent file starts from empty. A non-ENOENT read failure (EACCES/EIO) must NOT be
|
|
70
|
+
// treated as "no file" — rewriting from empty would DESTROY the existing ledger (codex R1).
|
|
71
|
+
if (err && err.code === 'ENOENT') existing = '';
|
|
72
|
+
else throw stop(`cannot read the ledger before appending (${(err && err.code) || (err && err.message) || err}) — refusing to overwrite it (fail closed)`);
|
|
73
|
+
}
|
|
74
|
+
const prefix = existing === '' ? '' : existing.endsWith('\n') ? existing : `${existing}\n`;
|
|
75
|
+
const body = `${prefix}${JSON.stringify(record)}\n`;
|
|
76
|
+
const root = dirname(ledgerPath);
|
|
77
|
+
writeContainedFileAtomic(root, ledgerPath, body, deps, { stop, label: ledgerPath });
|
|
78
|
+
return { writtenPath: ledgerPath, record };
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
// ── recordRound (the teeth + the integrity binding) ─────────────────────────────────────────────
|
|
82
|
+
|
|
83
|
+
// recordRound({ cwd, env, loop, activity, round, origins, backends, findings, timestamp }, deps) →
|
|
84
|
+
// { writtenPath, record }. THROWS a typed STOP (the teeth / a malformed record / a missing receipt)
|
|
85
|
+
// or a native fs error. Every fs edge + the fingerprint is injectable for hermetic tests.
|
|
86
|
+
export const recordRound = (params, deps = {}) => {
|
|
87
|
+
const { cwd = process.cwd(), env = process.env, loop, activity = DEFAULT_ACTIVITY, round, origins, backends, findings, timestamp } = params;
|
|
88
|
+
const ledgerPath = deps.ledgerPath ?? resolveLedgerPath(cwd, env);
|
|
89
|
+
if (ledgerPath == null) throw stop('cannot resolve the ledger path — not a git work tree and AW_REVIEW_LEDGER is unset');
|
|
90
|
+
if (!(Number.isInteger(round) && round >= 1)) throw stop(`round must be an integer >= 1 (got ${round})`);
|
|
91
|
+
// The hard-max ceiling: refuse ANY round beyond it, independent of triage state (Decision 5).
|
|
92
|
+
if (round > HARD_MAX) {
|
|
93
|
+
throw stop(`refusing to record round ${round}: beyond the hard-max ceiling of ${HARD_MAX} rounds — the loop must converge or the surviving finding must escalate, never another round`);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const fingerprint = deps.computeFingerprint ? deps.computeFingerprint(cwd) : computeTreeFingerprint(cwd);
|
|
97
|
+
|
|
98
|
+
// The teeth: refuse a new round WHILE decideStop on the existing records is triage-required. Once
|
|
99
|
+
// the surviving blocking finding is classified (recordTriage), decideStop is no longer
|
|
100
|
+
// triage-required and the next round is permitted (a fixable-bug classification permits the fix
|
|
101
|
+
// round — no deadlock). triage-required is fingerprint/backend-independent, so a minimal decideStop
|
|
102
|
+
// suffices here.
|
|
103
|
+
const { records, malformed, malformedReasons, readError } = readLedger(ledgerPath, deps.readFile);
|
|
104
|
+
// Fail CLOSED before the teeth: a ledger the reader could not fully trust (an unreadable file, or
|
|
105
|
+
// malformed lines the reader dropped) could make decideStop miss a triage-required round and fail
|
|
106
|
+
// the teeth OPEN (codex R1). Refuse to append until the ledger is sound.
|
|
107
|
+
if (readError) throw stop(`cannot read the existing ledger (${readError}) — refusing to append (fail closed)`);
|
|
108
|
+
if (malformed > 0) throw stop(`the existing ledger has ${malformed} malformed line(s) — refusing to append until they are fixed (fail closed): ${malformedReasons.join('; ')}`);
|
|
109
|
+
const existingLoop = filterLoopRecords(records, { activity, loop });
|
|
110
|
+
// Sequence integrity + sequentiality (codex R2+R3): the EXISTING rounds must already be exactly
|
|
111
|
+
// 1..n (never trust a hand-corrupted [2]/[1,1]/[2,1] to compute "latest"), AND the incoming round
|
|
112
|
+
// must be exactly the next (n+1). A duplicate, decreasing, or gapped round would let a fabricated
|
|
113
|
+
// "later" round become the latest that decideStop reads, bypassing the "latest round" teeth.
|
|
114
|
+
const priorRounds = existingLoop.filter((r) => r.kind === 'round').map((r) => r.round);
|
|
115
|
+
if (!roundSequenceIntact(existingLoop)) {
|
|
116
|
+
throw stop(`refusing to append to loop "${loop}": its recorded round sequence is corrupt (${priorRounds.join(',') || 'empty'}, not 1..n) — fix the ledger by hand before recording another round`);
|
|
117
|
+
}
|
|
118
|
+
const nextRound = priorRounds.length + 1;
|
|
119
|
+
if (round !== nextRound) {
|
|
120
|
+
throw stop(`refusing to record round ${round}: rounds must be sequential — the next round for loop "${loop}" is ${nextRound} (a duplicate, out-of-order, or gapped round would corrupt the crossover computation)`);
|
|
121
|
+
}
|
|
122
|
+
const pre = decideStop(existingLoop, { cap: REVIEW_CAP });
|
|
123
|
+
if (pre.state === 'triage-required') {
|
|
124
|
+
throw stop(`refusing to record a new round while triage is required — ${pre.reason}. Classify the surviving blocking finding(s) with the "classify" command first (a fixable-bug classification permits the fix round).`);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const record = { schema: SCHEMA_VERSION, loop, activity, kind: 'round', round, fingerprint, origins, backends, findings, timestamp: timestamp ?? isoNow() };
|
|
128
|
+
const v = validateRecord(record);
|
|
129
|
+
if (!v.ok) throw stop(`refusing to record a malformed round: ${v.reason}`);
|
|
130
|
+
|
|
131
|
+
// Integrity binding: each NON-degraded backend needs a grounded code receipt for this tree — a
|
|
132
|
+
// round cannot be recorded for a tree no bridge reviewed. A degraded backend minted no receipt.
|
|
133
|
+
const receiptsPath = deps.receiptsPath ?? resolveReceiptsPath(cwd, env);
|
|
134
|
+
const { receipts } = receiptsPath ? readReceipts(receiptsPath, deps.readFile) : { receipts: [] };
|
|
135
|
+
for (const b of backends) {
|
|
136
|
+
if (b.degraded) continue;
|
|
137
|
+
const own = receipts.filter(
|
|
138
|
+
(r) => r.backend === b.backend && r.fingerprint === fingerprint && r.artifact === 'code' && r.fresh === true && r.grounded === true,
|
|
139
|
+
);
|
|
140
|
+
if (own.length === 0) {
|
|
141
|
+
throw stop(`refusing to record a round for ${b.backend}: no grounded code receipt for the current tree — run its review wrapper (codex-review code / agy-review code --facts @f) first, or mark the backend degraded with a reason`);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
return appendRecord(ledgerPath, record, deps);
|
|
146
|
+
};
|
|
147
|
+
|
|
148
|
+
// ── recordTriage (the deadlock-breaker — no teeth, no receipt binding) ──────────────────────────
|
|
149
|
+
|
|
150
|
+
// recordTriage({ cwd, env, loop, activity, round, classifications, timestamp }, deps) →
|
|
151
|
+
// { writtenPath, record }. Appends the classification of the round's surviving blocking findings.
|
|
152
|
+
export const recordTriage = (params, deps = {}) => {
|
|
153
|
+
const { cwd = process.cwd(), env = process.env, loop, activity = DEFAULT_ACTIVITY, round, classifications, timestamp } = params;
|
|
154
|
+
const ledgerPath = deps.ledgerPath ?? resolveLedgerPath(cwd, env);
|
|
155
|
+
if (ledgerPath == null) throw stop('cannot resolve the ledger path — not a git work tree and AW_REVIEW_LEDGER is unset');
|
|
156
|
+
if (!(Number.isInteger(round) && round >= 1)) throw stop(`round must be an integer >= 1 (got ${round})`);
|
|
157
|
+
const fingerprint = deps.computeFingerprint ? deps.computeFingerprint(cwd) : computeTreeFingerprint(cwd);
|
|
158
|
+
// Normalize each classification: an absent testId → null, an absent note → '' — an absent optional
|
|
159
|
+
// field is FILLED, never rejected as malformed (agy R3). Under schema v2 (M2/AD-046) a fixable-bug
|
|
160
|
+
// normalized to a null testId then FAILS validateRecord below (a typed STOP naming the rule + the
|
|
161
|
+
// red-test-first fix) — the test-per-fold binding rides the existing validate path. A non-array is
|
|
162
|
+
// left as-is for validateRecord to reject with a typed STOP (never a raw .map TypeError).
|
|
163
|
+
const normalized = Array.isArray(classifications) ? classifications.map((c) => ({ ...c, testId: c?.testId ?? null, note: c?.note ?? '' })) : classifications;
|
|
164
|
+
const record = { schema: SCHEMA_VERSION, loop, activity, kind: 'triage', round, fingerprint, classifications: normalized, timestamp: timestamp ?? isoNow() };
|
|
165
|
+
const v = validateRecord(record);
|
|
166
|
+
if (!v.ok) throw stop(`refusing to record a malformed triage: ${v.reason}`);
|
|
167
|
+
|
|
168
|
+
// Bind the triage to a REAL round: the referenced round must exist and every classified findingKey
|
|
169
|
+
// must be a surviving blocking finding (blocker or major) of THAT round — a classification for a
|
|
170
|
+
// nonexistent/future round, or for a key the round never raised, must not satisfy resolved-residual
|
|
171
|
+
// downstream (codex R1). Fail CLOSED on an unreadable / malformed ledger.
|
|
172
|
+
const { records, malformed, malformedReasons, readError } = readLedger(ledgerPath, deps.readFile);
|
|
173
|
+
if (readError) throw stop(`cannot read the existing ledger (${readError}) — refusing to append (fail closed)`);
|
|
174
|
+
if (malformed > 0) throw stop(`the existing ledger has ${malformed} malformed line(s) — refusing to append until they are fixed (fail closed): ${malformedReasons.join('; ')}`);
|
|
175
|
+
const loopRecords = filterLoopRecords(records, { activity, loop });
|
|
176
|
+
if (!roundSequenceIntact(loopRecords)) throw stop(`refusing to classify loop "${loop}": its recorded round sequence is corrupt (not 1..n) — fix the ledger by hand first`);
|
|
177
|
+
const targetRound = loopRecords.find((r) => r.kind === 'round' && r.round === round);
|
|
178
|
+
if (!targetRound) throw stop(`refusing to classify round ${round} of loop "${loop}": no such recorded round — classify a round that exists`);
|
|
179
|
+
const survivingKeys = new Set(targetRound.findings.filter((f) => f.severity === 'blocker' || f.severity === 'major').map((f) => f.findingKey));
|
|
180
|
+
for (const c of classifications) {
|
|
181
|
+
if (!survivingKeys.has(c.findingKey)) throw stop(`refusing to classify "${c.findingKey}": it is not a surviving blocking finding of round ${round} (classify only that round's blockers/majors)`);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
return appendRecord(ledgerPath, record, deps);
|
|
185
|
+
};
|
|
186
|
+
|
|
187
|
+
// ── CLI (record / classify) ──────────────────────────────────────────────────────────────────────
|
|
188
|
+
|
|
189
|
+
const HELP = `review-ledger-write — the review-round ledger WRITER (agent-workflow family, AD-045).
|
|
190
|
+
|
|
191
|
+
Usage:
|
|
192
|
+
node review-ledger-write.mjs record --json '<round-payload>' [--cwd <dir>]
|
|
193
|
+
node review-ledger-write.mjs classify --json '<triage-payload>' [--cwd <dir>]
|
|
194
|
+
|
|
195
|
+
record appends one review round. The JSON payload carries { loop, round, origins, backends,
|
|
196
|
+
findings } (activity defaults to plan-execution; timestamp defaults to now). REFUSES while
|
|
197
|
+
triage is required, beyond the hard-max ceiling of ${HARD_MAX} rounds, or when a non-degraded
|
|
198
|
+
backend lacks a grounded code receipt for the current tree.
|
|
199
|
+
classify appends one triage record. The JSON payload carries { loop, round, classifications } (each
|
|
200
|
+
{ findingKey, class, accepted, testId, note }). A fixable-bug REQUIRES a testId — the
|
|
201
|
+
red→green test that pins the fold, formatted "<test-file>#<test-name-pattern>" (write it
|
|
202
|
+
first); inherent-layer-residual / escalate may omit it. This is what permits the next round.
|
|
203
|
+
|
|
204
|
+
The read-only checker is a SEPARATE tool: node review-ledger.mjs --check / --status / --json.
|
|
205
|
+
Exit codes: 0 written; 1 a typed STOP (teeth / malformed / missing receipt / fs error); 2 usage.`;
|
|
206
|
+
|
|
207
|
+
const parseArgs = (argv) => {
|
|
208
|
+
const opts = { cwd: undefined, json: undefined };
|
|
209
|
+
for (let i = 0; i < argv.length; i += 1) {
|
|
210
|
+
const a = argv[i];
|
|
211
|
+
if (a === '--cwd') {
|
|
212
|
+
opts.cwd = argv[i + 1];
|
|
213
|
+
if (opts.cwd === undefined) throw usageFail('--cwd needs a directory');
|
|
214
|
+
i += 1;
|
|
215
|
+
} else if (a === '--json') {
|
|
216
|
+
opts.json = argv[i + 1];
|
|
217
|
+
if (opts.json === undefined) throw usageFail('--json needs a JSON payload');
|
|
218
|
+
i += 1;
|
|
219
|
+
} else {
|
|
220
|
+
throw usageFail(`unknown argument: ${a}`);
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
return opts;
|
|
224
|
+
};
|
|
225
|
+
|
|
226
|
+
const parsePayload = (json) => {
|
|
227
|
+
if (json === undefined) throw usageFail('a --json payload is required');
|
|
228
|
+
try {
|
|
229
|
+
return JSON.parse(json);
|
|
230
|
+
} catch (err) {
|
|
231
|
+
throw usageFail(`--json is not valid JSON (${err.message})`);
|
|
232
|
+
}
|
|
233
|
+
};
|
|
234
|
+
|
|
235
|
+
export const main = (argv, ctx = {}) => {
|
|
236
|
+
const cwd0 = ctx.cwd ?? process.cwd();
|
|
237
|
+
const env = ctx.env ?? process.env;
|
|
238
|
+
try {
|
|
239
|
+
if (argv.includes('--help') || argv.includes('-h') || argv.length === 0) return { code: argv.length === 0 ? 2 : 0, stdout: HELP, stderr: '' };
|
|
240
|
+
const sub = argv[0];
|
|
241
|
+
if (sub !== 'record' && sub !== 'classify') throw usageFail(`unknown subcommand "${sub}" (expected: record | classify)`);
|
|
242
|
+
const opts = parseArgs(argv.slice(1));
|
|
243
|
+
const payload = parsePayload(opts.json);
|
|
244
|
+
const cwd = opts.cwd ?? cwd0;
|
|
245
|
+
const result =
|
|
246
|
+
sub === 'record'
|
|
247
|
+
? recordRound({ cwd, env, ...payload })
|
|
248
|
+
: recordTriage({ cwd, env, ...payload });
|
|
249
|
+
return { code: 0, stdout: `review-ledger-write: recorded a ${result.record.kind} for loop "${result.record.loop}" round ${result.record.round} → ${result.writtenPath}`, stderr: '' };
|
|
250
|
+
} catch (err) {
|
|
251
|
+
return { code: err.exitCode ?? 1, stdout: '', stderr: `review-ledger-write: ${err.message}` };
|
|
252
|
+
}
|
|
253
|
+
};
|
|
254
|
+
|
|
255
|
+
const isDirectRun = process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href;
|
|
256
|
+
if (isDirectRun) {
|
|
257
|
+
const r = main(process.argv.slice(2));
|
|
258
|
+
if (r.stdout) process.stdout.write(r.stdout.endsWith('\n') ? r.stdout : `${r.stdout}\n`);
|
|
259
|
+
if (r.stderr) process.stderr.write(r.stderr.endsWith('\n') ? r.stderr : `${r.stderr}\n`);
|
|
260
|
+
process.exitCode = r.code;
|
|
261
|
+
}
|