@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,364 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// fold-completeness.mjs — the read-only FOLD-COMPLETENESS checker behind `/agent-workflow-kit
|
|
3
|
+
// fold-completeness` (M3, AD-046, DEBT-TEST-COMPLETENESS). It is the sibling gate of
|
|
4
|
+
// review-ledger.mjs: the ledger computes the review-loop crossover-stop; this tool computes whether
|
|
5
|
+
// the tests that the loop's folds bind to ACTUALLY pin the changed code. It reads the result ledger
|
|
6
|
+
// the runner writes (fold-completeness-run.mjs — the SOLE tree-toucher + result writer), recomputes
|
|
7
|
+
// the canonical uncommitted-state fingerprint, resolves the plan-execution.review recipe + the
|
|
8
|
+
// in-flight plan, and decides `--check` fail-closed. It NEVER runs tests, NEVER mutates, and NEVER
|
|
9
|
+
// imports the runner — an import-split test pins that structural invariant (the read half owns the
|
|
10
|
+
// result SCHEMA + reader; the runner imports them the other direction, mirroring review-ledger /
|
|
11
|
+
// review-ledger-write).
|
|
12
|
+
//
|
|
13
|
+
// Normative `--check` exit contract (the single home of this list — SKILL.md / the mode file point
|
|
14
|
+
// here). The gate is plan-EXECUTION-scoped, filtered to the in-flight plan's filename stem:
|
|
15
|
+
// exit 0 when the resolved plan-execution.review recipe is solo (configured, or degraded there);
|
|
16
|
+
// when no plan is in flight; when the tree is clean; when the cwd is not a git work tree;
|
|
17
|
+
// and when the in-flight plan-execution loop has a CURRENT run record whose BOTH bindings
|
|
18
|
+
// match — the tree fingerprint AND the sorted fixable-bug testId set recorded in the run —
|
|
19
|
+
// with every bound testId resolvable + baseline-green, 0 uncovered changed lines, 0 changed
|
|
20
|
+
// unsupported-source files, and the reserved EMPTY mutation shape (v1 ships no mutation).
|
|
21
|
+
// exit 1 for any DIRTY in-flight plan-execution loop lacking such a current run record — including
|
|
22
|
+
// the stale-fingerprint case (a tree edit moves the fingerprint) and the same-fingerprint/
|
|
23
|
+
// new-testId case (a triage recorded after the run moves the bound-testId set, Decision 9) —
|
|
24
|
+
// or a run naming an unresolvable/red-baseline bound test, an uncovered changed line, a
|
|
25
|
+
// changed unsupported-source file, or ANY mutation data (v1 ships no mutation — such a record
|
|
26
|
+
// was not produced by this runner version; fail closed); when MORE THAN ONE plan is in
|
|
27
|
+
// flight (ambiguous loop id). Fail-CLOSED (unknown state, never a fail-open pass) on a
|
|
28
|
+
// detector failure, an unreadable/malformed result or review ledger, or a corrupt run set —
|
|
29
|
+
// the only detector-independent green is an EXPLICIT configured solo. Changed OUT-OF-DOMAIN
|
|
30
|
+
// files (docs/config the suite does not execute) are LOUDLY listed but never gate-blocking
|
|
31
|
+
// (Decision 5): guarding what the tool cannot assess with a red gate is a pretend-mechanism.
|
|
32
|
+
//
|
|
33
|
+
// HONEST residuals (accepted, documented — exactly like review-state's / review-ledger's): coverage
|
|
34
|
+
// proves execution, not assertion; NO mutation signal ships in v1 — the researched mutation half was
|
|
35
|
+
// shelved, the record's `mutation` field is a reserved empty shape (a record carrying ANY mutation
|
|
36
|
+
// data fails the check closed); the result records, testIds, and the ledger are forgeable
|
|
37
|
+
// (`git commit --no-verify`, file editing) — a self-discipline mechanism against silent process
|
|
38
|
+
// drift, NOT a security boundary. TS/JSX source is out of scope v1 (a changed unsupported-source
|
|
39
|
+
// file fails the gate closed rather than be vouched for).
|
|
40
|
+
//
|
|
41
|
+
// Read-only: never writes, never commits, never runs a subscription CLI. It DOES spawn read-only
|
|
42
|
+
// `git` queries (via the reused review-state fingerprint reader + a git-dir resolver). Dependency-
|
|
43
|
+
// free, Node >= 18. No side effects on import (the isDirectRun idiom).
|
|
44
|
+
|
|
45
|
+
import { readFileSync } from 'node:fs';
|
|
46
|
+
import { join } from 'node:path';
|
|
47
|
+
import { pathToFileURL } from 'node:url';
|
|
48
|
+
import { spawnSync } from 'node:child_process';
|
|
49
|
+
import { detectBackends } from './detect-backends.mjs';
|
|
50
|
+
import { resolveActivityRecipe, planRecipe, DISPLAY_ALIASES } from './recipes.mjs';
|
|
51
|
+
import { CONFIG_REL, fail, loadConfig } from './orchestration-config.mjs';
|
|
52
|
+
import { computeTreeFingerprint, isTreeClean, plansInFlight } from './review-state.mjs';
|
|
53
|
+
import { resolveLedgerPath, readLedger, filterLoopRecords } from './review-ledger.mjs';
|
|
54
|
+
|
|
55
|
+
export const RESULTS_BASENAME = 'agent-workflow-fold-completeness.jsonl';
|
|
56
|
+
export const RESULT_SCHEMA_VERSION = 1;
|
|
57
|
+
const ACTIVITY = 'plan-execution';
|
|
58
|
+
const SLOT = 'review';
|
|
59
|
+
|
|
60
|
+
// ── git-dir resolution (read-only queries; the ledger lives in the git dir, uncommittable) ──────
|
|
61
|
+
|
|
62
|
+
const gitLine = (args, cwd) => {
|
|
63
|
+
const r = spawnSync('git', args, { cwd, windowsHide: true });
|
|
64
|
+
if (r.error || r.status !== 0) return null;
|
|
65
|
+
return r.stdout.toString('utf8').replace(/\r?\n$/, '');
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
const gitRoot = (cwd) => gitLine(['rev-parse', '--show-toplevel'], cwd);
|
|
69
|
+
|
|
70
|
+
// The result-ledger path: AW_FOLD_RESULTS overrides (mirrors AW_REVIEW_LEDGER); else <git dir>/basename.
|
|
71
|
+
export const resolveResultsPath = (cwd, env = process.env) => {
|
|
72
|
+
if (env.AW_FOLD_RESULTS) return env.AW_FOLD_RESULTS;
|
|
73
|
+
const gitDir = gitLine(['rev-parse', '--absolute-git-dir'], cwd);
|
|
74
|
+
return gitDir == null ? null : join(gitDir, RESULTS_BASENAME);
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
// ── the bound fixable-bug testId set (the SINGLE source of truth — runner + checker share it) ────
|
|
78
|
+
|
|
79
|
+
// collectBoundTestIds(reviewRecords, { activity, loop }) → the sorted, de-duplicated testIds of the
|
|
80
|
+
// loop's fixable-bug classifications. Pure over review-ledger records. Both the runner (which records
|
|
81
|
+
// the set as a binding) and the checker (which recomputes it for the staleness check) call THIS, so
|
|
82
|
+
// the two can never drift (Decision 9: same-fingerprint/new-testId is stale).
|
|
83
|
+
export const collectBoundTestIds = (reviewRecords, { activity = ACTIVITY, loop } = {}) => {
|
|
84
|
+
const ids = new Set();
|
|
85
|
+
for (const r of filterLoopRecords(reviewRecords, { activity, loop })) {
|
|
86
|
+
if (r.kind !== 'triage') continue;
|
|
87
|
+
for (const c of r.classifications) if (c.class === 'fixable-bug' && typeof c.testId === 'string' && c.testId.length > 0) ids.add(c.testId);
|
|
88
|
+
}
|
|
89
|
+
return [...ids].sort();
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
// ── result-record schema (machine fields only; the runner refuses to write a malformed record) ───
|
|
93
|
+
|
|
94
|
+
const isPlainObject = (v) => v !== null && typeof v === 'object' && !Array.isArray(v);
|
|
95
|
+
const isNonEmptyString = (v) => typeof v === 'string' && v.length > 0;
|
|
96
|
+
const isNonNegInt = (v) => Number.isInteger(v) && v >= 0;
|
|
97
|
+
const isStringArray = (v) => Array.isArray(v) && v.every((x) => typeof x === 'string');
|
|
98
|
+
|
|
99
|
+
// validateRunRecord(obj) → { ok, reason }. The `reason` names the exact failed check so the
|
|
100
|
+
// malformed-line surface and the per-check named tests can assert it. Mutation is reserved and must
|
|
101
|
+
// stay the empty shape in v1 (the mutation half is shelved); the schema carries the fields so a
|
|
102
|
+
// record validates uniformly and decideCheck enforces the exact reserved shape.
|
|
103
|
+
export const validateRunRecord = (obj) => {
|
|
104
|
+
if (!isPlainObject(obj)) return { ok: false, reason: 'not an object' };
|
|
105
|
+
if (obj.schema !== RESULT_SCHEMA_VERSION) return { ok: false, reason: `schema must be ${RESULT_SCHEMA_VERSION}` };
|
|
106
|
+
if (!isNonEmptyString(obj.loop)) return { ok: false, reason: 'missing loop' };
|
|
107
|
+
if (!(obj.fingerprint === null || isNonEmptyString(obj.fingerprint))) return { ok: false, reason: 'fingerprint must be null or a non-empty string' };
|
|
108
|
+
if (!isStringArray(obj.boundTestIds)) return { ok: false, reason: 'boundTestIds must be an array of strings' };
|
|
109
|
+
if (!Array.isArray(obj.testIds)) return { ok: false, reason: 'testIds must be an array' };
|
|
110
|
+
for (const t of obj.testIds) {
|
|
111
|
+
if (!isPlainObject(t) || !isNonEmptyString(t.id)) return { ok: false, reason: 'each testId entry needs an id' };
|
|
112
|
+
if (typeof t.resolvable !== 'boolean' || typeof t.baselineGreen !== 'boolean') return { ok: false, reason: `testId ${t.id} needs boolean resolvable + baselineGreen` };
|
|
113
|
+
if (!isNonNegInt(t.executed)) return { ok: false, reason: `testId ${t.id} executed must be a non-negative integer` };
|
|
114
|
+
}
|
|
115
|
+
if (!isStringArray(obj.unsupported)) return { ok: false, reason: 'unsupported must be an array of strings' };
|
|
116
|
+
if (!isStringArray(obj.outOfDomain)) return { ok: false, reason: 'outOfDomain must be an array of strings' };
|
|
117
|
+
if (!isPlainObject(obj.coverage) || !Array.isArray(obj.coverage.uncoveredChanged)) return { ok: false, reason: 'coverage.uncoveredChanged must be an array' };
|
|
118
|
+
for (const u of obj.coverage.uncoveredChanged) {
|
|
119
|
+
if (!isPlainObject(u) || !isNonEmptyString(u.file)) return { ok: false, reason: 'each uncoveredChanged entry needs a file' };
|
|
120
|
+
if (!(u.line === null || (Number.isInteger(u.line) && u.line >= 1))) return { ok: false, reason: `uncoveredChanged ${u.file} line must be null or an integer >= 1` };
|
|
121
|
+
}
|
|
122
|
+
const m = obj.mutation;
|
|
123
|
+
if (!isPlainObject(m) || !isNonNegInt(m.total) || !isNonNegInt(m.killed) || !Array.isArray(m.survived) || !isNonNegInt(m.skipped)) {
|
|
124
|
+
return { ok: false, reason: 'mutation must be { total, killed, survived[], skipped, killSetBasis }' };
|
|
125
|
+
}
|
|
126
|
+
if (!(m.killSetBasis === null || isNonEmptyString(m.killSetBasis))) return { ok: false, reason: 'mutation.killSetBasis must be null or a non-empty string' };
|
|
127
|
+
if (!isPlainObject(obj.budgets)) return { ok: false, reason: 'budgets must be an object' };
|
|
128
|
+
if (!isNonEmptyString(obj.timestamp)) return { ok: false, reason: 'missing timestamp' };
|
|
129
|
+
return { ok: true };
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
// readResults(path) → { records, malformed, malformedReasons, readError }. Absent file → empty (no run
|
|
133
|
+
// yet). A non-ENOENT read error surfaces as readError so callers fail CLOSED (mirrors readLedger).
|
|
134
|
+
export const readResults = (path, readFile = readFileSync) => {
|
|
135
|
+
let raw;
|
|
136
|
+
try {
|
|
137
|
+
raw = readFile(path, 'utf8');
|
|
138
|
+
} catch (err) {
|
|
139
|
+
if (err && err.code === 'ENOENT') return { records: [], malformed: 0, malformedReasons: [] };
|
|
140
|
+
return { records: [], malformed: 0, malformedReasons: [], readError: (err && err.code) || (err && err.message) || 'read failed' };
|
|
141
|
+
}
|
|
142
|
+
const records = [];
|
|
143
|
+
const malformedReasons = [];
|
|
144
|
+
for (const line of raw.split('\n')) {
|
|
145
|
+
if (line.trim() === '') continue;
|
|
146
|
+
let parsed;
|
|
147
|
+
try {
|
|
148
|
+
parsed = JSON.parse(line);
|
|
149
|
+
} catch (err) {
|
|
150
|
+
malformedReasons.push(`unparseable JSON (${err.message})`);
|
|
151
|
+
continue;
|
|
152
|
+
}
|
|
153
|
+
const v = validateRunRecord(parsed);
|
|
154
|
+
if (v.ok) records.push(parsed);
|
|
155
|
+
else malformedReasons.push(v.reason);
|
|
156
|
+
}
|
|
157
|
+
return { records, malformed: malformedReasons.length, malformedReasons };
|
|
158
|
+
};
|
|
159
|
+
|
|
160
|
+
// filterLoopResults(records, loop) → the run records of ONE loop, order preserved (latest is last).
|
|
161
|
+
export const filterLoopResults = (records, loop) => records.filter((r) => r.loop === loop);
|
|
162
|
+
|
|
163
|
+
// ── the check + report core ─────────────────────────────────────────────────────────────────────
|
|
164
|
+
|
|
165
|
+
// buildFoldState({ cwd, env, detect }) → everything both renders need. Pure I/O at the edges; every
|
|
166
|
+
// project-relative read anchors at the git work-tree ROOT when one exists (the fingerprint is
|
|
167
|
+
// root-anchored — the same discipline review-state / review-ledger use).
|
|
168
|
+
export const buildFoldState = ({ cwd, env = process.env, detect = detectBackends } = {}) => {
|
|
169
|
+
const root = gitRoot(cwd) ?? cwd;
|
|
170
|
+
const { config, source: configSource } = loadConfig(root);
|
|
171
|
+
let detection = [];
|
|
172
|
+
let detectionWarning = null;
|
|
173
|
+
try {
|
|
174
|
+
detection = detect();
|
|
175
|
+
} catch (err) {
|
|
176
|
+
detectionWarning = `backend detection failed (${(err && err.message) || err}) — treating all backends as not ready; the review recipe floors at solo.`;
|
|
177
|
+
}
|
|
178
|
+
const resolved = resolveActivityRecipe({ config: config ?? {}, readiness: detection, activity: ACTIVITY, slot: SLOT });
|
|
179
|
+
const { dispatch } = planRecipe(resolved.recipe, detection);
|
|
180
|
+
const requiredBackends = dispatch.map((d) => DISPLAY_ALIASES[d.backend] ?? d.backend);
|
|
181
|
+
const plans = plansInFlight(root);
|
|
182
|
+
const fingerprint = computeTreeFingerprint(cwd);
|
|
183
|
+
const clean = fingerprint == null ? null : isTreeClean(cwd);
|
|
184
|
+
const resultsPath = resolveResultsPath(cwd, env);
|
|
185
|
+
const resultRead = resultsPath ? readResults(resultsPath) : { records: [], malformed: 0, malformedReasons: [] };
|
|
186
|
+
const reviewPath = resolveLedgerPath(cwd, env);
|
|
187
|
+
const reviewRead = reviewPath ? readLedger(reviewPath) : { records: [], malformed: 0, malformedReasons: [] };
|
|
188
|
+
return {
|
|
189
|
+
resolved,
|
|
190
|
+
configSource,
|
|
191
|
+
requiredBackends,
|
|
192
|
+
plans,
|
|
193
|
+
fingerprint,
|
|
194
|
+
clean,
|
|
195
|
+
resultsPath,
|
|
196
|
+
resultRecords: resultRead.records,
|
|
197
|
+
resultMalformed: resultRead.malformed,
|
|
198
|
+
resultReadError: resultRead.readError,
|
|
199
|
+
reviewPath,
|
|
200
|
+
reviewRecords: reviewRead.records,
|
|
201
|
+
reviewMalformed: reviewRead.malformed,
|
|
202
|
+
reviewReadError: reviewRead.readError,
|
|
203
|
+
detectionWarning,
|
|
204
|
+
};
|
|
205
|
+
};
|
|
206
|
+
|
|
207
|
+
const sameSet = (a, b) => a.length === b.length && a.every((x, i) => x === b[i]);
|
|
208
|
+
// The reserved (and, in v1, only legal) mutation key set — sorted for the sameSet comparison.
|
|
209
|
+
const RESERVED_MUTATION_KEYS = ['killSetBasis', 'killed', 'skipped', 'survived', 'total'];
|
|
210
|
+
const renderUncovered = (u) => (u.line === null ? `${u.file} (absent from coverage)` : `${u.file}:${u.line}`);
|
|
211
|
+
|
|
212
|
+
// The normative --check decision (the header contract, in order) → { code, reason }. The checker is
|
|
213
|
+
// PURE over (record, current fingerprint, review ledger) — it recomputes NO coverage/mutation.
|
|
214
|
+
export const decideCheck = (state) => {
|
|
215
|
+
// A detector failure is UNKNOWN state, not "no reviewer ready" — fail closed (like review-ledger).
|
|
216
|
+
const explicitSolo = state.resolved.recipe === 'solo' && state.resolved.source === 'config' && !state.resolved.degradedFrom;
|
|
217
|
+
if (state.detectionWarning && !explicitSolo) return { code: 1, reason: `cannot verify fold-completeness — ${state.detectionWarning}` };
|
|
218
|
+
if (state.resolved.recipe === 'solo') {
|
|
219
|
+
const why = state.resolved.degradedFrom ? `resolved ${ACTIVITY}.${SLOT} recipe degrades to solo here (${state.resolved.reason})` : `resolved ${ACTIVITY}.${SLOT} recipe is solo`;
|
|
220
|
+
return { code: 0, reason: `${why} — no fold-completeness run required` };
|
|
221
|
+
}
|
|
222
|
+
if (state.plans.length === 0) return { code: 0, reason: 'no plan in flight (docs/plans/ holds no active plan) — no fold-completeness run required' };
|
|
223
|
+
if (state.plans.length > 1) return { code: 1, reason: `more than one plan in flight (${state.plans.join(', ')}) — ambiguous loop id; resolve to one active plan` };
|
|
224
|
+
if (state.fingerprint == null) return { code: 0, reason: 'not a git work tree — nothing to assess' };
|
|
225
|
+
if (state.clean === true) return { code: 0, reason: 'the working tree is clean — nothing to assess' };
|
|
226
|
+
// Fail CLOSED on any ledger the reader could not fully trust (a dropped line could hide a defect).
|
|
227
|
+
if (state.resultReadError) return { code: 1, reason: `cannot read the result ledger (${state.resultReadError}) — failing closed; inspect ${state.resultsPath}` };
|
|
228
|
+
if (state.resultMalformed > 0) return { code: 1, reason: `the result ledger has ${state.resultMalformed} malformed line(s) — failing closed; inspect ${state.resultsPath}` };
|
|
229
|
+
if (state.reviewReadError) return { code: 1, reason: `cannot read the review ledger (${state.reviewReadError}) — failing closed; inspect ${state.reviewPath}` };
|
|
230
|
+
if (state.reviewMalformed > 0) return { code: 1, reason: `the review ledger has ${state.reviewMalformed} malformed line(s) — failing closed; inspect ${state.reviewPath}` };
|
|
231
|
+
|
|
232
|
+
const loop = state.plans[0].replace(/\.md$/, '');
|
|
233
|
+
const runs = filterLoopResults(state.resultRecords, loop);
|
|
234
|
+
if (runs.length === 0) return { code: 1, reason: `dirty plan-execution loop "${loop}" but no fold-completeness run recorded — run fold-completeness-run.mjs` };
|
|
235
|
+
const latest = runs[runs.length - 1];
|
|
236
|
+
|
|
237
|
+
// Decision 9 — the double binding. A tree edit moves the fingerprint; a new fixable-bug triage moves
|
|
238
|
+
// the bound-testId set. Either mismatch is STALE (the run no longer describes the committable tree).
|
|
239
|
+
if (latest.fingerprint !== state.fingerprint) return { code: 1, reason: `no fold-completeness run for the current tree (the tree was edited after the run) — re-run fold-completeness-run.mjs after the last edit` };
|
|
240
|
+
const currentBound = collectBoundTestIds(state.reviewRecords, { activity: ACTIVITY, loop });
|
|
241
|
+
if (!sameSet(latest.boundTestIds, currentBound)) return { code: 1, reason: `a fixable-bug testId was triaged after the run (the bound-testId set changed) — re-run fold-completeness-run.mjs` };
|
|
242
|
+
// Fail CLOSED if the run's probe set does not cover its bound-testId set EXACTLY. A well-formed
|
|
243
|
+
// runner record always probes every bound testId (one testIds[] entry per boundTestId), but a record
|
|
244
|
+
// with missing/extra/duplicate probes is untrustworthy — proving "0 unresolvable, 0 red" over an
|
|
245
|
+
// EMPTY (or partial) probe set proves nothing about the bound tests (codex R1).
|
|
246
|
+
const probedIds = latest.testIds.map((t) => t.id).sort();
|
|
247
|
+
if (!sameSet(probedIds, latest.boundTestIds)) return { code: 1, reason: `the run's probe set does not match its bound-testId set (${probedIds.length} probe(s) for ${latest.boundTestIds.length} bound testId(s)) — failing closed; re-run fold-completeness-run.mjs` };
|
|
248
|
+
|
|
249
|
+
// A changed unsupported-source file → fail closed: the signal never vouches for JS-family source it
|
|
250
|
+
// cannot assess (TS/JSX out of scope v1, Decision 5).
|
|
251
|
+
if (latest.unsupported.length > 0) return { code: 1, reason: `changed unsupported-source file(s) the signal cannot assess (TS/JSX out of scope v1): ${latest.unsupported.join(', ')}` };
|
|
252
|
+
const unresolvable = latest.testIds.filter((t) => !t.resolvable).map((t) => t.id);
|
|
253
|
+
if (unresolvable.length > 0) return { code: 1, reason: `unresolvable bound testId(s) — the pattern selects no test: ${unresolvable.join(', ')}` };
|
|
254
|
+
const redBaseline = latest.testIds.filter((t) => t.resolvable && !t.baselineGreen).map((t) => t.id);
|
|
255
|
+
if (redBaseline.length > 0) return { code: 1, reason: `bound test(s) with a red baseline (the fold is not complete): ${redBaseline.join(', ')}` };
|
|
256
|
+
if (latest.coverage.uncoveredChanged.length > 0) return { code: 1, reason: `uncovered changed line(s) — changed code no test executed: ${latest.coverage.uncoveredChanged.map(renderUncovered).join(', ')}` };
|
|
257
|
+
// v1 ships NO mutation (the mutation half was shelved): the shipped runner only ever writes the
|
|
258
|
+
// reserved empty shape, so a record carrying ANY mutation data was not produced by this runner
|
|
259
|
+
// version (forged, or a version-skewed ledger — e.g. an older placed checker reading a newer
|
|
260
|
+
// runner's ledger) — fail closed rather than vouch for a signal v1 cannot have computed. The rule
|
|
261
|
+
// is the EXACT reserved shape (key set + empty values): an extra key would smuggle mutation data
|
|
262
|
+
// past a known-fields-only check.
|
|
263
|
+
const m = latest.mutation;
|
|
264
|
+
const emptyReservedShape =
|
|
265
|
+
sameSet(Object.keys(m).sort(), RESERVED_MUTATION_KEYS) &&
|
|
266
|
+
m.total === 0 && m.killed === 0 && m.skipped === 0 && m.survived.length === 0 && m.killSetBasis === null;
|
|
267
|
+
if (!emptyReservedShape) {
|
|
268
|
+
return { code: 1, reason: `the run record's mutation field is not the reserved empty shape (${m.total} total / ${m.killed} killed / ${m.survived?.length ?? '?'} survived / ${m.skipped} skipped) but v1 ships no mutation — not a record this runner version produced; re-run fold-completeness-run.mjs` };
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
const notes = [];
|
|
272
|
+
if (latest.outOfDomain.length > 0) notes.push(`out-of-domain changes not assessed (non-blocking): ${latest.outOfDomain.join(', ')}`);
|
|
273
|
+
return { code: 0, reason: `fold-completeness verified for loop "${loop}" (fingerprint + bound-testId set current, coverage + baseline green)${notes.length ? ` — ${notes.join('; ')}` : ''}` };
|
|
274
|
+
};
|
|
275
|
+
|
|
276
|
+
// ── rendering ─────────────────────────────────────────────────────────────────────────────────
|
|
277
|
+
|
|
278
|
+
const runLine = (r) => {
|
|
279
|
+
const tests = r.testIds.length ? r.testIds.map((t) => `${t.id}${t.resolvable ? (t.baselineGreen ? '✓' : ' red-baseline') : ' unresolvable'}`).join(', ') : '(no bound tests)';
|
|
280
|
+
const uncov = r.coverage.uncoveredChanged.length ? r.coverage.uncoveredChanged.map(renderUncovered).join(', ') : 'none';
|
|
281
|
+
return [
|
|
282
|
+
` latest run — fingerprint ${r.fingerprint}`,
|
|
283
|
+
` bound testIds: ${tests}`,
|
|
284
|
+
` uncovered changed: ${uncov}`,
|
|
285
|
+
` unsupported: ${r.unsupported.length ? r.unsupported.join(', ') : 'none'} · out-of-domain: ${r.outOfDomain.length ? r.outOfDomain.join(', ') : 'none'}`,
|
|
286
|
+
` mutation: ${r.mutation.total} total / ${r.mutation.killed} killed / ${r.mutation.survived.length} survived / ${r.mutation.skipped} skipped`,
|
|
287
|
+
].join('\n');
|
|
288
|
+
};
|
|
289
|
+
|
|
290
|
+
const formatHuman = (state, check) => {
|
|
291
|
+
const src = state.configSource === 'config' ? `from ${CONFIG_REL}` : 'computed default';
|
|
292
|
+
const lines = [
|
|
293
|
+
`fold-completeness — ${ACTIVITY}.${SLOT} = ${state.resolved.recipe} (${src})${state.requiredBackends.length ? ` → ${state.requiredBackends.join(' + ')}` : ''}`,
|
|
294
|
+
];
|
|
295
|
+
if (state.detectionWarning) lines.push(` ⚠ ${state.detectionWarning}`);
|
|
296
|
+
lines.push(` plan in flight: ${state.plans.length ? state.plans.join(', ') : '(none)'}`);
|
|
297
|
+
if (state.fingerprint == null) lines.push(' tree: not a git work tree');
|
|
298
|
+
else if (state.clean === true) lines.push(' tree: clean (nothing to assess)');
|
|
299
|
+
else lines.push(` tree fingerprint: ${state.fingerprint}`);
|
|
300
|
+
lines.push(` result ledger: ${state.resultsPath ?? '(unresolvable — no git dir)'} (${state.resultRecords.length} record(s)${state.resultMalformed ? `, ${state.resultMalformed} malformed — inspect the file` : ''})`);
|
|
301
|
+
if (state.plans.length === 1) {
|
|
302
|
+
const loop = state.plans[0].replace(/\.md$/, '');
|
|
303
|
+
const runs = filterLoopResults(state.resultRecords, loop);
|
|
304
|
+
if (runs.length > 0) lines.push(runLine(runs[runs.length - 1]));
|
|
305
|
+
}
|
|
306
|
+
lines.push(` check: ${check.code === 0 ? 'PASS' : 'FAIL'} — ${check.reason}`);
|
|
307
|
+
return lines.join('\n');
|
|
308
|
+
};
|
|
309
|
+
|
|
310
|
+
const HELP = `fold-completeness — read-only FOLD-COMPLETENESS checker (agent-workflow family, AD-046).
|
|
311
|
+
|
|
312
|
+
Usage:
|
|
313
|
+
node fold-completeness.mjs [--check | --status | --json]
|
|
314
|
+
|
|
315
|
+
Reads the result ledger the runner writes (<git dir>/${RESULTS_BASENAME}; AW_FOLD_RESULTS overrides),
|
|
316
|
+
resolves the effective ${ACTIVITY}.${SLOT} recipe, recomputes the canonical uncommitted-state
|
|
317
|
+
fingerprint, and decides whether the in-flight plan-execution loop's changed code is pinned by tests.
|
|
318
|
+
|
|
319
|
+
--status (default) → the human report: resolved recipe, plan-in-flight, the latest run summary, verdict.
|
|
320
|
+
--check → the gate exit code. The normative exit contract lives in the tool header (the single home):
|
|
321
|
+
exit 0 for solo / no plan in flight / a clean tree / not-a-git-tree / a CURRENT run whose fingerprint
|
|
322
|
+
AND bound-testId set both match, with resolvable+green bound tests, 0 uncovered changed lines, 0
|
|
323
|
+
changed unsupported source, and the reserved empty mutation shape; exit 1 otherwise (stale/missing
|
|
324
|
+
run, an unresolvable/red bound test, an uncovered line, changed TS/JSX, any mutation data — v1 ships
|
|
325
|
+
no mutation, >1 plan, an unreadable/malformed ledger, or a detector failure). Out-of-domain changes
|
|
326
|
+
are listed, never blocking.
|
|
327
|
+
--json → the structured state + decision.
|
|
328
|
+
|
|
329
|
+
The runner is a SEPARATE tool (fold-completeness-run.mjs) — this read-only checker never imports it.
|
|
330
|
+
Human residual: git commit --no-verify and ledger editing remain possible — a self-discipline mechanism.
|
|
331
|
+
|
|
332
|
+
Exit codes: 0 pass (or plain report); 1 check failed or config error (loud); 2 usage.`;
|
|
333
|
+
|
|
334
|
+
const KNOWN_ARGS = new Set(['--help', '-h', '--check', '--status', '--json']);
|
|
335
|
+
|
|
336
|
+
export const main = (argv, ctx = {}) => {
|
|
337
|
+
const cwd = ctx.cwd ?? process.cwd();
|
|
338
|
+
const env = ctx.env ?? process.env;
|
|
339
|
+
const detect = ctx.detect ?? detectBackends;
|
|
340
|
+
try {
|
|
341
|
+
if (argv.includes('--help') || argv.includes('-h')) return { code: 0, stdout: HELP, stderr: '' };
|
|
342
|
+
const unknown = argv.find((a) => !KNOWN_ARGS.has(a));
|
|
343
|
+
if (unknown !== undefined) throw fail(2, `unknown argument: ${unknown}`);
|
|
344
|
+
const state = buildFoldState({ cwd, env, detect });
|
|
345
|
+
const check = decideCheck(state);
|
|
346
|
+
if (argv.includes('--json')) {
|
|
347
|
+
return { code: argv.includes('--check') ? check.code : 0, stdout: JSON.stringify({ ...state, check }, null, 2), stderr: '' };
|
|
348
|
+
}
|
|
349
|
+
if (argv.includes('--check')) {
|
|
350
|
+
return { code: check.code, stdout: `fold-completeness check: ${check.code === 0 ? 'PASS' : 'FAIL'} — ${check.reason}`, stderr: '' };
|
|
351
|
+
}
|
|
352
|
+
return { code: 0, stdout: formatHuman(state, check), stderr: '' };
|
|
353
|
+
} catch (err) {
|
|
354
|
+
return { code: err.exitCode ?? 1, stdout: '', stderr: `fold-completeness: ${err.message}` };
|
|
355
|
+
}
|
|
356
|
+
};
|
|
357
|
+
|
|
358
|
+
const isDirectRun = process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href;
|
|
359
|
+
if (isDirectRun) {
|
|
360
|
+
const r = main(process.argv.slice(2));
|
|
361
|
+
if (r.stdout) process.stdout.write(r.stdout.endsWith('\n') ? r.stdout : `${r.stdout}\n`);
|
|
362
|
+
if (r.stderr) process.stderr.write(r.stderr.endsWith('\n') ? r.stderr : `${r.stderr}\n`);
|
|
363
|
+
process.exitCode = r.code;
|
|
364
|
+
}
|
package/tools/procedures.mjs
CHANGED
|
@@ -198,7 +198,10 @@ const backendSetLabel = (backends) =>
|
|
|
198
198
|
// REQUIRED per-round structured emission {round N · finding-origin tally · per-backend verdict}. Only a
|
|
199
199
|
// review slot can resolve reviewed|council (execute floors at solo|delegated), so gate on the recipe.
|
|
200
200
|
const REVIEW_RECIPES = new Set(['reviewed', 'council']);
|
|
201
|
-
|
|
201
|
+
// activity-aware (AD-046): the triage classification vocabulary rides EVERY review-backed activity;
|
|
202
|
+
// the LEDGER pointer renders ONLY for plan-execution — the ledger is plan-execution-scoped (AD-045),
|
|
203
|
+
// and pointing plan-authoring at it would send rounds of the wrong activity into the code loop's gate.
|
|
204
|
+
const reviewLoopAdvice = (slots, activity) =>
|
|
202
205
|
slots.some((s) => REVIEW_RECIPES.has(s.recipe))
|
|
203
206
|
? [
|
|
204
207
|
'Review-loop economics (planning.md §9 · orchestration.md §4) — the review this recipe runs:',
|
|
@@ -206,6 +209,12 @@ const reviewLoopAdvice = (slots) =>
|
|
|
206
209
|
' • Backend divergence (one backend grounded-ships while another keeps revising mechanics) IS the crossover stop.',
|
|
207
210
|
' • Route an all-mechanics/CI or prose-only artifact to a thin plan + diff-review; run a self-consistency read before every re-review.',
|
|
208
211
|
' • Each round MUST emit {round N · finding-origin tally (first-draft / fold-induced / mechanics) · per-backend verdict} so the crossover is a computed signal.',
|
|
212
|
+
' • At the cap, classify every surviving blocking finding: fixable-bug (fold ONCE as a red→green test, re-review) / inherent-layer-residual (document + raise to an acceptance criterion) / escalate (the maintainer decides); a minor never forces triage.',
|
|
213
|
+
...(activity === 'plan-execution'
|
|
214
|
+
? [
|
|
215
|
+
' • The computed instrument for THIS loop: record each round + triage via review-ledger (record / classify); read the stop with review-ledger --status (its render replaces the hand-composed tally); gate the commit with review-ledger --check.',
|
|
216
|
+
]
|
|
217
|
+
: []),
|
|
209
218
|
]
|
|
210
219
|
: [];
|
|
211
220
|
|
|
@@ -302,7 +311,7 @@ const formatHuman = ({ activity, section, slots, warnings, plans }) => {
|
|
|
302
311
|
}
|
|
303
312
|
const grounding = groundingPreStepAdvice(activity, slots, plans);
|
|
304
313
|
if (grounding.length) lines.push('', ...grounding);
|
|
305
|
-
const advice = reviewLoopAdvice(slots);
|
|
314
|
+
const advice = reviewLoopAdvice(slots, activity);
|
|
306
315
|
if (advice.length) lines.push('', ...advice);
|
|
307
316
|
lines.push('', ...costLanesAdvice());
|
|
308
317
|
if (warnings.length) {
|
|
@@ -320,7 +329,7 @@ const buildJson = ({ activity, section, slots, configSource, warnings, plans })
|
|
|
320
329
|
// `contracts` is the ADDITIVE per-dispatch driving-contract field (empty for solo).
|
|
321
330
|
slots.map((s) => [s.slot, { recipe: s.recipe, source: s.source, degradedFrom: s.degradedFrom, reason: s.reason, backends: s.backends, contracts: s.contracts }]),
|
|
322
331
|
),
|
|
323
|
-
reviewLoop: reviewLoopAdvice(slots),
|
|
332
|
+
reviewLoop: reviewLoopAdvice(slots, activity),
|
|
324
333
|
// ADDITIVE (AD-038): the populated grounding pre-step, structured (empty when agy is not dispatched).
|
|
325
334
|
groundingPreStep: groundingPreStepAdvice(activity, slots, plans),
|
|
326
335
|
// ADDITIVE (cost-tiered execution): the unconditional cost-lane advisory, structured.
|