@sabaiway/agent-workflow-kit 5.3.0 → 5.5.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 +138 -0
- package/README.md +2 -1
- package/SKILL.md +5 -1
- package/bridges/antigravity-cli-bridge/SKILL.md +1 -1
- package/bridges/antigravity-cli-bridge/bin/agy-review.sh +1 -1
- package/bridges/antigravity-cli-bridge/capability.json +1 -1
- package/bridges/codex-cli-bridge/SKILL.md +53 -5
- package/bridges/codex-cli-bridge/bin/codex-exec.sh +622 -30
- package/bridges/codex-cli-bridge/bin/codex-exec.test.mjs +731 -3
- package/bridges/codex-cli-bridge/bin/codex-review.sh +1 -1
- package/bridges/codex-cli-bridge/capability.json +15 -10
- package/bridges/codex-cli-bridge/references/sandbox-and-flags.md +16 -12
- package/capability.json +1 -1
- package/package.json +1 -1
- package/references/modes/core-evidence.md +1 -1
- package/references/modes/coverage-check.md +1 -1
- package/references/modes/dispatch.md +29 -0
- package/references/modes/gates.md +7 -2
- package/references/modes/receipt-deadline.md +3 -3
- package/references/modes/recommendations.md +3 -1
- package/references/modes/upgrade.md +1 -1
- package/references/modes/velocity.md +5 -1
- package/references/scripts/migrate-gates.mjs +102 -10
- package/references/scripts/migrate-gates.test.mjs +37 -0
- package/tools/commands.mjs +7 -0
- package/tools/core-evidence.mjs +79 -5
- package/tools/coverage-check.mjs +23 -7
- package/tools/coverage-producer.mjs +68 -0
- package/tools/coverage-state.mjs +24 -0
- package/tools/declared-paths.mjs +32 -0
- package/tools/detect-backends.mjs +5 -4
- package/tools/dispatch-record.mjs +10 -3
- package/tools/dispatch-store.mjs +392 -0
- package/tools/dispatch.mjs +1779 -0
- package/tools/doc-parity.mjs +27 -4
- package/tools/exec-producer.mjs +483 -0
- package/tools/exec-receipt.mjs +263 -0
- package/tools/flow-store.mjs +111 -462
- package/tools/gates-declaration.mjs +49 -0
- package/tools/gates-init.mjs +83 -6
- package/tools/receipt-deadline.mjs +25 -3
- package/tools/recommendations.mjs +63 -19
- package/tools/release-scan.mjs +33 -0
- package/tools/run-gates.mjs +111 -32
- package/tools/store-append.mjs +444 -0
- package/tools/velocity-profile.mjs +102 -23
|
@@ -5,7 +5,9 @@
|
|
|
5
5
|
// COMPLETELY in one consented step: it REMOVES the canonical legacy entries, EXTENDS the
|
|
6
6
|
// canonical `unit-tests` cmd with the built-in lcov reporters (the D3(d) coverage source), and
|
|
7
7
|
// ADDS the coverage-check gate LAST (removal alone is not a migration — the result must satisfy
|
|
8
|
-
// `run-gates --final` and carry a working commit path).
|
|
8
|
+
// `run-gates --final` and carry a working commit path). The checker is added ONLY over a
|
|
9
|
+
// declaration that PRODUCES the lcov it reads; with no producer it is WITHHELD with a loud
|
|
10
|
+
// warning, because a checker with no producer passes (`skipped-no-lcov`) while verifying nothing.
|
|
9
11
|
//
|
|
10
12
|
// Matching is by the DOCUMENTED cmd forms ONLY (hand-wired history included — never by seed
|
|
11
13
|
// provenance): a legacy entry is `node <path>/review-ledger.mjs --check` or
|
|
@@ -45,12 +47,69 @@ export const LEGACY_FORMS = Object.freeze([
|
|
|
45
47
|
{ name: 'fold-completeness', re: legacyRe('fold-completeness\\.mjs') },
|
|
46
48
|
]);
|
|
47
49
|
|
|
48
|
-
//
|
|
49
|
-
//
|
|
50
|
-
//
|
|
50
|
+
// coverage-producer canon >>> BEGIN drift-guarded region
|
|
51
|
+
// Authored TWICE, byte-identically: in the memory substrate's references/scripts/migrate-gates.mjs
|
|
52
|
+
// and in the composition root's tools/coverage-producer.mjs. Neither side imports the other — the
|
|
53
|
+
// substrate is standalone and must not depend on the root, and the root must not import mirrored
|
|
54
|
+
// bytes — so a TEXT drift guard beside the root's copy holds them equal. Edit BOTH, then re-run the
|
|
55
|
+
// mirror sync.
|
|
56
|
+
//
|
|
57
|
+
// The destination is written against AW_GIT_DIR, which run-gates exports to every gate child on a
|
|
58
|
+
// plain run AND on --final (AW_LCOV_FILE is --final only), so one cmd survives the unmet
|
|
59
|
+
// producer-variable preflight in both modes. The explicit stdout reporter is not decoration:
|
|
60
|
+
// without it the lcov reporter swallows the human TAP/spec stream.
|
|
51
61
|
export const UNIT_TESTS_COVERAGE_FLAGS =
|
|
52
62
|
'--experimental-test-coverage --test-reporter=lcov --test-reporter-destination="$AW_GIT_DIR/agent-workflow-lcov.info" --test-reporter=spec --test-reporter-destination=stdout';
|
|
53
63
|
|
|
64
|
+
// The ONE suite body that produces that lcov with no extra dependency.
|
|
65
|
+
export const COVERAGE_PRODUCER_BODY = `node --test ${UNIT_TESTS_COVERAGE_FLAGS}`;
|
|
66
|
+
|
|
67
|
+
// The per-PM exec wrappers a fill offer puts that body behind. Recognition must cover every form
|
|
68
|
+
// the kit has EMITTED, so the prefixes are matched literally; gates-init's execCmdFor stays the one
|
|
69
|
+
// EMITTER and is bound to this list by a named acceptance test, never by a second grammar.
|
|
70
|
+
const PRODUCER_EXEC_PREFIXES = Object.freeze([
|
|
71
|
+
'COREPACK_ENABLE_NETWORK=0 npm exec --offline --script-shell /bin/sh -- ',
|
|
72
|
+
'COREPACK_ENABLE_NETWORK=0 pnpm exec -- ',
|
|
73
|
+
'COREPACK_ENABLE_NETWORK=0 yarn exec -- ',
|
|
74
|
+
]);
|
|
75
|
+
|
|
76
|
+
// A trailing suffix is the project's own test paths; a leading one would mean the body is not what
|
|
77
|
+
// this cmd runs. The tail passes a POSITIVE closed grammar — every whitespace-separated token must
|
|
78
|
+
// be path-shaped — never an operator blocklist: `node --test <flags> && rm -f <lcov>` runs the
|
|
79
|
+
// suite and then DELETES the file, so an open-ended tail would certify a producer that leaves
|
|
80
|
+
// nothing behind, and scanning for operator BYTES would put the incomplete-scan failure on the
|
|
81
|
+
// unsafe side (a missed operator is a dead pair) instead of the mild one (an unrecognised
|
|
82
|
+
// legitimate tail merely withholds the offer — add the gate by hand).
|
|
83
|
+
// The token set is everything that appears in a PATH or a glob and can never sequence, redirect or
|
|
84
|
+
// substitute a command — quoting and `~` included, `( ) $ ` ; & | < > # \` excluded. SCOPE, stated
|
|
85
|
+
// exactly: the screen judges each token's SOURCE bytes. Quote removal adds none, but brace SEQUENCE
|
|
86
|
+
// expansion does — `{Y..a}` yields ``[ \ ] ^ _ ` `` (probed) — so "no new bytes" would be a false
|
|
87
|
+
// claim. What holds is the property that matters: bash does not re-scan an expansion result as
|
|
88
|
+
// syntax, so a byte arriving that way is literal argument DATA, never an operator. The leading-`-`
|
|
89
|
+
// exclusion is weaker still — a FIRST-ORDER screen only, defeated by `'--flag'` and
|
|
90
|
+
// `{path,--flag}`. It is kept because the tail is the project's test PATHS and it costs only a loud
|
|
91
|
+
// withhold. Deciding an argument's post-expansion identity needs a shell lexer, which this family
|
|
92
|
+
// deliberately has NOWHERE (AD-079). So the claim is "configured with the reporters", never "the
|
|
93
|
+
// lcov survives the command"; a run that produces none is caught honestly at runtime as
|
|
94
|
+
// `skipped-no-lcov`.
|
|
95
|
+
const PRODUCER_PATH_TOKEN = /^(?!-)[A-Za-z0-9_./*{},:@+=~?[\]!'"-]+$/;
|
|
96
|
+
const pathShapedTail = (tail) => tail === '' || tail.split(/[ \t]+/).every((token) => PRODUCER_PATH_TOKEN.test(token));
|
|
97
|
+
const carriesProducerBody = (text) =>
|
|
98
|
+
text === COVERAGE_PRODUCER_BODY ||
|
|
99
|
+
(text.startsWith(`${COVERAGE_PRODUCER_BODY} `) && pathShapedTail(text.slice(COVERAGE_PRODUCER_BODY.length).trim()));
|
|
100
|
+
|
|
101
|
+
// matchesCoverageProducer(cmd) → CLOSED-WORLD over the full command forms the kit emits, never a
|
|
102
|
+
// substring probe: `echo "$AW_GIT_DIR/agent-workflow-lcov.info"`, a half-written reporter flag set,
|
|
103
|
+
// or the path as a bare substring must all read as NOT a producer — otherwise the checker is
|
|
104
|
+
// declared over a gate that writes nothing and then PASSES while certifying nothing.
|
|
105
|
+
export const matchesCoverageProducer = (cmd) => {
|
|
106
|
+
if (typeof cmd !== 'string') return false;
|
|
107
|
+
const trimmed = cmd.trim();
|
|
108
|
+
if (carriesProducerBody(trimmed)) return true;
|
|
109
|
+
return PRODUCER_EXEC_PREFIXES.some((prefix) => trimmed.startsWith(prefix) && carriesProducerBody(trimmed.slice(prefix.length)));
|
|
110
|
+
};
|
|
111
|
+
// coverage-producer canon <<< END drift-guarded region
|
|
112
|
+
|
|
54
113
|
// The RETIRED kit-owned git-dir stores the deleted machinery wrote — dead data a consumer's
|
|
55
114
|
// upgrade would otherwise strand forever. The migration cleans them (consented via the preview;
|
|
56
115
|
// ENOENT is a silent no-op; any other unlink error is reported loudly but never fails the
|
|
@@ -147,13 +206,20 @@ export const buildMigrationPlan = (gates, kitToolsDir) => {
|
|
|
147
206
|
plan.push({ action: 'keep', entry: gate, reason: null });
|
|
148
207
|
}
|
|
149
208
|
const kept = plan.filter((r) => r.action === 'keep' || r.action === 'extend');
|
|
209
|
+
// The checker READS an lcov; something has to WRITE it. Adding the checker over a declaration
|
|
210
|
+
// with no producer creates the dead pair — the gate PASSES (`skipped-no-lcov`) and certifies
|
|
211
|
+
// nothing, so the migration withholds it and says why instead.
|
|
212
|
+
const hasProducer = kept.some((r) => matchesCoverageProducer(r.entry.cmd));
|
|
150
213
|
let collision = null;
|
|
214
|
+
let checkerWithheld = false;
|
|
151
215
|
if (checkerRow === null) {
|
|
152
216
|
// A surviving NON-canonical entry already holding the checker's id blocks the add — two
|
|
153
217
|
// `coverage-check` rows would be ambiguous; the customized entry must be resolved by hand
|
|
154
218
|
// FIRST (the caller turns this into a loud STOP on preview and apply alike).
|
|
155
219
|
if (kept.some((r) => r.entry.id === 'coverage-check')) {
|
|
156
220
|
collision = 'coverage-check';
|
|
221
|
+
} else if (!hasProducer) {
|
|
222
|
+
checkerWithheld = true;
|
|
157
223
|
} else {
|
|
158
224
|
plan.push({
|
|
159
225
|
action: 'add',
|
|
@@ -165,8 +231,23 @@ export const buildMigrationPlan = (gates, kitToolsDir) => {
|
|
|
165
231
|
checkerRow.action = 'move';
|
|
166
232
|
checkerRow.reason = 'the canonical checker must be the LAST declared gate (nothing may run after it consumed the lcov)';
|
|
167
233
|
}
|
|
234
|
+
// An ALREADY-declared checker over no producer is the same dead pair the withhold prevents — an
|
|
235
|
+
// earlier deployment could have created it. The migration removes no declared gate, so it reports
|
|
236
|
+
// the inertness and refuses to call the result final-run-capable.
|
|
237
|
+
const checkerInert = checkerRow !== null && !hasProducer;
|
|
168
238
|
const reviewStateCandidate = `{ "id": "review-state", "title": "Review receipts converged (D3(b))", "cmd": "node \\"${join(kitToolsDir, 'review-state.mjs')}\\" --check" }`;
|
|
169
|
-
return {
|
|
239
|
+
return {
|
|
240
|
+
plan,
|
|
241
|
+
customized,
|
|
242
|
+
unitTestsExtended,
|
|
243
|
+
finalCapable: hasReviewState && !checkerWithheld && !checkerInert,
|
|
244
|
+
hasProducer,
|
|
245
|
+
hasReviewState,
|
|
246
|
+
checkerWithheld,
|
|
247
|
+
checkerInert,
|
|
248
|
+
reviewStateCandidate,
|
|
249
|
+
collision,
|
|
250
|
+
};
|
|
170
251
|
};
|
|
171
252
|
|
|
172
253
|
export const resultingGates = (plan) => {
|
|
@@ -184,7 +265,7 @@ const customizedRecovery = (gate) =>
|
|
|
184
265
|
? `declare the canonical suite gate by hand so the coverage contract is verifiable: node --test ${UNIT_TESTS_COVERAGE_FLAGS} <your test paths>`
|
|
185
266
|
: 'remove the entry, or repoint it at a living check — the review-ledger / fold-completeness tools no longer exist.';
|
|
186
267
|
|
|
187
|
-
const warningLines = ({ customized, finalCapable, reviewStateCandidate }) => {
|
|
268
|
+
const warningLines = ({ customized, finalCapable, hasReviewState = finalCapable, checkerWithheld = false, checkerInert = false, reviewStateCandidate }) => {
|
|
188
269
|
const lines = [];
|
|
189
270
|
for (const gate of customized) {
|
|
190
271
|
lines.push(` CUSTOMIZED (untouched): ${gate.id}: ${gate.cmd}`);
|
|
@@ -193,7 +274,15 @@ const warningLines = ({ customized, finalCapable, reviewStateCandidate }) => {
|
|
|
193
274
|
if (customized.length) {
|
|
194
275
|
lines.push(' IMPORTANT: do NOT install the commit guard until every customized entry above is resolved — a declaration that cannot pass run-gates --final would block every commit.');
|
|
195
276
|
}
|
|
196
|
-
if (
|
|
277
|
+
if (checkerWithheld) {
|
|
278
|
+
lines.push(' WARNING: the canonical coverage-check gate was NOT added — no declared gate would PRODUCE the lcov it reads, and a checker with no producer passes while verifying nothing. Declare the suite gate first, then re-run this migration:');
|
|
279
|
+
lines.push(` node --test ${UNIT_TESTS_COVERAGE_FLAGS} <your test paths>`);
|
|
280
|
+
}
|
|
281
|
+
if (checkerInert) {
|
|
282
|
+
lines.push(' WARNING: the DECLARED coverage-check gate is INERT — no declared gate PRODUCES the lcov it reads, so it passes while verifying nothing. Nothing is removed for you; declare the suite gate:');
|
|
283
|
+
lines.push(` node --test ${UNIT_TESTS_COVERAGE_FLAGS} <your test paths>`);
|
|
284
|
+
}
|
|
285
|
+
if (!hasReviewState) {
|
|
197
286
|
lines.push(' WARNING: the result is NOT final-run-capable — no canonical review-state check is declared. Add it (paste-ready), then run-gates --final can mint the receipt:');
|
|
198
287
|
lines.push(` ${reviewStateCandidate}`);
|
|
199
288
|
}
|
|
@@ -201,7 +290,7 @@ const warningLines = ({ customized, finalCapable, reviewStateCandidate }) => {
|
|
|
201
290
|
};
|
|
202
291
|
|
|
203
292
|
export const formatPreview = (analysis, applyHint) => {
|
|
204
|
-
const { plan, unitTestsExtended, finalCapable, retiredStores = [] } = analysis;
|
|
293
|
+
const { plan, unitTestsExtended, finalCapable, hasProducer = unitTestsExtended, retiredStores = [] } = analysis;
|
|
205
294
|
const lines = ['[agent-workflow] legacy gates.json migration preview (dry-run — nothing was written):'];
|
|
206
295
|
const acted = plan.filter((r) => r.action !== 'keep');
|
|
207
296
|
for (const r of acted) {
|
|
@@ -217,7 +306,9 @@ export const formatPreview = (analysis, applyHint) => {
|
|
|
217
306
|
: ' nothing to migrate mechanically — no canonical legacy entries and no retired stores; the warnings below still need a hand.',
|
|
218
307
|
);
|
|
219
308
|
}
|
|
220
|
-
|
|
309
|
+
// Keyed on the ID, but a PRODUCER is recognized under any id — repeating this advice over a
|
|
310
|
+
// working producer sends the user to fix nothing.
|
|
311
|
+
if (!hasProducer && !unitTestsExtended && !plan.some((r) => r.entry.id === 'unit-tests')) {
|
|
221
312
|
lines.push(' note: no canonical `unit-tests` entry found — declare your suite gate with the lcov reporters by hand (the coverage-check gate reads the file it produces).');
|
|
222
313
|
}
|
|
223
314
|
lines.push(...warningLines(analysis));
|
|
@@ -317,7 +408,8 @@ Usage:
|
|
|
317
408
|
Default is a dry-run PREVIEW (writes nothing). --apply rewrites ${GATES_REL} atomically:
|
|
318
409
|
canonical legacy entries (review-ledger / fold-completeness --check, matched by their documented
|
|
319
410
|
single-invocation forms) are REMOVED; the canonical unit-tests cmd gains the built-in lcov
|
|
320
|
-
reporters; the coverage-check gate is ADDED last (resolved, QUOTED path)
|
|
411
|
+
reporters; the coverage-check gate is ADDED last (resolved, QUOTED path) — and WITHHELD, loudly,
|
|
412
|
+
when no declared gate produces the lcov it reads. Customized entries are
|
|
321
413
|
NEVER auto-touched — the preview names each with a paste-ready recovery, and the commit guard
|
|
322
414
|
must not be installed until they are resolved.`;
|
|
323
415
|
|
|
@@ -12,6 +12,7 @@ import { spawnSync } from 'node:child_process';
|
|
|
12
12
|
import {
|
|
13
13
|
LEGACY_FORMS,
|
|
14
14
|
UNIT_TESTS_COVERAGE_FLAGS,
|
|
15
|
+
COVERAGE_PRODUCER_BODY,
|
|
15
16
|
RETIRED_STORE_BASENAMES,
|
|
16
17
|
findRetiredStores,
|
|
17
18
|
buildMigrationPlan,
|
|
@@ -71,6 +72,42 @@ describe('migrate-gates — the pure migration plan', () => {
|
|
|
71
72
|
assert.ok(!again.plan.some((r) => r.action === 'add'), 'a declaration already carrying the checker gains no duplicate');
|
|
72
73
|
});
|
|
73
74
|
|
|
75
|
+
it('a declaration with NO producer never GAINS the checker — the pair is declared together or not at all', () => {
|
|
76
|
+
const npmSuite = { id: 'suite', title: 'S', cmd: 'npm test' };
|
|
77
|
+
const analysis = buildMigrationPlan([LEGACY_LEDGER, npmSuite], KIT_TOOLS);
|
|
78
|
+
assert.ok(!analysis.plan.some((r) => r.action === 'add'), 'no checker is added over a declaration that produces no lcov');
|
|
79
|
+
assert.deepEqual(resultingGates(analysis.plan).map((g) => g.id), ['suite'], 'the legacy entry still goes, nothing dead arrives');
|
|
80
|
+
assert.equal(analysis.finalCapable, false, 'a declaration with no checker is not final-run-capable');
|
|
81
|
+
const preview = formatPreview(analysis, 'APPLY');
|
|
82
|
+
assert.match(preview, /coverage-check/, 'the withheld checker is named');
|
|
83
|
+
assert.match(preview, /produce/i, 'the preview says WHY — no gate produces the lcov it would read');
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it('an ALREADY-declared checker over no producer is reported INERT, is never removed, and is not final-run-capable', () => {
|
|
87
|
+
const checker = { id: 'coverage-check', title: 'CC', cmd: `node "${join(KIT_TOOLS, 'coverage-check.mjs')}" --check` };
|
|
88
|
+
const reviewState = { id: 'review-state', title: 'RS', cmd: `node "${join(KIT_TOOLS, 'review-state.mjs')}" --check` };
|
|
89
|
+
const analysis = buildMigrationPlan([{ id: 'suite', title: 'S', cmd: 'npm test' }, reviewState, checker], KIT_TOOLS);
|
|
90
|
+
assert.equal(analysis.finalCapable, false, 'a review-state present must NOT make an inert pair read as final-run-capable');
|
|
91
|
+
assert.ok(resultingGates(analysis.plan).some((g) => g.id === 'coverage-check'), 'the declared checker is never removed');
|
|
92
|
+
const preview = formatPreview(analysis, 'APPLY');
|
|
93
|
+
assert.match(preview, /INERT/, 'the inert pair is named');
|
|
94
|
+
assert.match(preview, /--experimental-test-coverage/, 'the paste-ready suite cmd is carried');
|
|
95
|
+
assert.doesNotMatch(preview, /already final-run-capable/);
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
it('a gates-init-shaped producer (the exec-wrapped offer form) is recognized — the checker IS added over it', () => {
|
|
99
|
+
const offered = {
|
|
100
|
+
id: 'test',
|
|
101
|
+
title: 'T',
|
|
102
|
+
cmd: `COREPACK_ENABLE_NETWORK=0 npm exec --offline --script-shell /bin/sh -- ${COVERAGE_PRODUCER_BODY}`,
|
|
103
|
+
};
|
|
104
|
+
const analysis = buildMigrationPlan([offered], KIT_TOOLS);
|
|
105
|
+
assert.deepEqual(resultingGates(analysis.plan).map((g) => g.id), ['test', 'coverage-check']);
|
|
106
|
+
// The `no canonical unit-tests entry` advice is keyed on the ID, but a producer is recognized
|
|
107
|
+
// under ANY id — repeating the advice over a working producer sends the user to fix nothing.
|
|
108
|
+
assert.doesNotMatch(formatPreview(analysis, 'APPLY'), /declare your suite gate with the lcov reporters by hand/);
|
|
109
|
+
});
|
|
110
|
+
|
|
74
111
|
it('a CUSTOMIZED dead-tool reference (compound form) is kept untouched and reported', () => {
|
|
75
112
|
const analysis = buildMigrationPlan([CUSTOM], KIT_TOOLS);
|
|
76
113
|
assert.equal(analysis.plan.find((r) => r.entry.id === 'my-ledger-wrap').action, 'keep');
|
package/tools/commands.mjs
CHANGED
|
@@ -259,6 +259,13 @@ const CATALOG = [
|
|
|
259
259
|
kind: READ_ONLY,
|
|
260
260
|
oneLine: 'Check that the documented contract tokens still match the live code constants they describe — a read-only lint that fails closed on drift; --check turns it into a gate exit code.',
|
|
261
261
|
},
|
|
262
|
+
{
|
|
263
|
+
key: 'dispatch',
|
|
264
|
+
invocation: invocationOf('dispatch'),
|
|
265
|
+
group: 'Orchestrate',
|
|
266
|
+
kind: WRITER,
|
|
267
|
+
oneLine: 'Measure delegation: check a sub-task brief’s contract block (form only — never whether the task is genuinely bounded), pre-register an acceptance wave with its thresholds, record one observation, open a delegated thread from that brief, wait for that one dispatch to answer — a wait that ends without an answer says so and authorizes nothing — absorb the wrapper’s receipt back into the ledger, fold the returned work or close the thread with a recorded degrade, and print the per-class report of how much a delegated sub-task actually bought, derived from what was dispatched, returned and folded. Writes only its own ledger file beside the repo; never commits.',
|
|
268
|
+
},
|
|
262
269
|
{
|
|
263
270
|
// NEVER `guarded` — that kind promises dry-run-first, which these writers do not have; the
|
|
264
271
|
// honest strongest caution is `writer` with the destructive arm named in the line itself.
|
package/tools/core-evidence.mjs
CHANGED
|
@@ -24,7 +24,8 @@
|
|
|
24
24
|
// re-derived): safe repo-relative path resolution, no-follow real-path containment, shell-free
|
|
25
25
|
// argv, per-run timeout, N/N reruns, the quarantine lane (reruns 3, timeout 120s).
|
|
26
26
|
// Import posture: this module is the DAG BOTTOM — it imports only node built-ins + atomic-write +
|
|
27
|
-
// changed-surface
|
|
27
|
+
// changed-surface + the pure leaves (repo-lex, fs-read-nofollow, coverage-state), and OWNS the
|
|
28
|
+
// canonical review-domain primitives (tree fingerprint, receipt read
|
|
28
29
|
// path, attesting predicate, verdict vocabulary, testId format, base resolution). review-state
|
|
29
30
|
// RE-EXPORTS its historical public API from here, so its consumers (and the bash-twin parity
|
|
30
31
|
// tests) are unchanged while review-state can import the degrade reader without an import cycle.
|
|
@@ -42,6 +43,9 @@ import { writeContainedFileAtomic } from './atomic-write.mjs';
|
|
|
42
43
|
import { parsePositiveIntKnob, probeVerdict } from './changed-surface.mjs';
|
|
43
44
|
import { readRegularFileNoFollow } from './fs-read-nofollow.mjs';
|
|
44
45
|
import { lexicalRepoRelative } from './repo-lex.mjs';
|
|
46
|
+
// The coverage vocabulary leaf: run-gates RECORDS the token this validator checks, and run-gates
|
|
47
|
+
// imports THIS module (the sole-writer boundary), so their shared home sits below both.
|
|
48
|
+
import { COVERAGE, FINAL_COVERAGE_STATES } from './coverage-state.mjs';
|
|
45
49
|
|
|
46
50
|
export const CORE_EVIDENCE_STOP = 'CORE_EVIDENCE_STOP';
|
|
47
51
|
const stop = (message) => Object.assign(new Error(`[agent-workflow-kit] ${message}`), { name: 'CoreEvidenceStop', code: CORE_EVIDENCE_STOP });
|
|
@@ -52,7 +56,9 @@ const GIT_MAX_BUFFER = 256 * 1024 * 1024; // a full-tree diff / TAP stream can b
|
|
|
52
56
|
|
|
53
57
|
const gitRaw = (args, cwd) => spawnSync('git', args, { cwd, maxBuffer: GIT_MAX_BUFFER, windowsHide: true });
|
|
54
58
|
|
|
55
|
-
|
|
59
|
+
// Exported so a consumer computing over the SAME domain (the exec metric producer) reads git through
|
|
60
|
+
// this one runner instead of growing a second spawnSync wrapper with its own buffer bound.
|
|
61
|
+
export const gitBuf = (args, cwd) => {
|
|
56
62
|
const r = gitRaw(args, cwd);
|
|
57
63
|
if (r.error || r.status !== 0) return null;
|
|
58
64
|
return r.stdout;
|
|
@@ -670,6 +676,17 @@ export const validateEvidenceRecord = (record) => {
|
|
|
670
676
|
if (record.lcovSha256 !== null && (typeof record.lcovSha256 !== 'string' || !HEX64_RE.test(record.lcovSha256))) {
|
|
671
677
|
return { ok: false, reason: 'final: lcovSha256 must be a 64-hex sha256 of the consumed lcov file, or null when none was produced' };
|
|
672
678
|
}
|
|
679
|
+
// The run's coverage token is ADDITIVE (absent = a pre-token receipt, still valid) and CLOSED.
|
|
680
|
+
// `certified` is cross-checked against the bound digest — a run that certified a verdict read
|
|
681
|
+
// bytes, so the two fields can never disagree in the store.
|
|
682
|
+
if ('coverage' in record) {
|
|
683
|
+
if (!FINAL_COVERAGE_STATES.includes(record.coverage)) {
|
|
684
|
+
return { ok: false, reason: `final: coverage, when present, must be one of ${FINAL_COVERAGE_STATES.join(' | ')} (got ${JSON.stringify(record.coverage)}; a final run always selects the canonical checker, so "none" never rides a final receipt)` };
|
|
685
|
+
}
|
|
686
|
+
if (record.coverage === COVERAGE.certified && record.lcovSha256 === null) {
|
|
687
|
+
return { ok: false, reason: 'final: coverage "certified" requires a bound lcovSha256 — a certified verdict was issued over lcov bytes that were read' };
|
|
688
|
+
}
|
|
689
|
+
}
|
|
673
690
|
if (!isNonEmptyString(record.timestamp)) return { ok: false, reason: 'final: timestamp must be a non-empty string' };
|
|
674
691
|
return { ok: true };
|
|
675
692
|
}
|
|
@@ -997,6 +1014,26 @@ export const runDegrade = ({ cwd = process.cwd(), env = process.env, backend, re
|
|
|
997
1014
|
|
|
998
1015
|
// ── the summary verb (D6): ONE stateless render — receipts + evidence store, no ledger ────────────
|
|
999
1016
|
|
|
1017
|
+
// A red-proof binds the BYTES of the test file it was observed on, and the final-run checker refuses
|
|
1018
|
+
// when those bytes have moved (coverage-check.mjs, the hash-mismatch arm). This render used to print a
|
|
1019
|
+
// stale record byte-identically to a live one, so the only place staleness surfaced was that gate —
|
|
1020
|
+
// after both council dispatches, at the commit boundary, which is the most expensive discovery point
|
|
1021
|
+
// in the loop. The currency is therefore computed HERE, from the SAME two helpers the checker uses, so
|
|
1022
|
+
// the render and the gate can never disagree about what stale means.
|
|
1023
|
+
//
|
|
1024
|
+
// An unresolvable or unreadable file is STALE, not unknown: a proof nobody can re-check is not a proof
|
|
1025
|
+
// anyone should read as current.
|
|
1026
|
+
export const redProofCurrency = (rootTop, record) => {
|
|
1027
|
+
if (rootTop == null) return { state: 'stale', detail: 'the work-tree root is not resolvable' };
|
|
1028
|
+
const resolved = resolveTestFile(rootTop, record.file);
|
|
1029
|
+
if (!resolved.ok) return { state: 'stale', detail: resolved.reason };
|
|
1030
|
+
const current = hashFileBytes(resolved.abs);
|
|
1031
|
+
if (current == null) return { state: 'stale', detail: `cannot read "${record.file}"` };
|
|
1032
|
+
return current === record.fileHash
|
|
1033
|
+
? { state: 'current' }
|
|
1034
|
+
: { state: 'stale', detail: 'the bound test file changed after the mint' };
|
|
1035
|
+
};
|
|
1036
|
+
|
|
1000
1037
|
export const buildSummaryState = ({ cwd = process.cwd(), env = process.env } = {}) => {
|
|
1001
1038
|
const fingerprint = computeTreeFingerprint(cwd);
|
|
1002
1039
|
const base = resolveBase(cwd);
|
|
@@ -1011,7 +1048,10 @@ export const buildSummaryState = ({ cwd = process.cwd(), env = process.env } = {
|
|
|
1011
1048
|
backend: b,
|
|
1012
1049
|
summary: summarizeReviewReceiptsForTree(receipts.filter((r) => r.backend === b), fingerprint),
|
|
1013
1050
|
}));
|
|
1014
|
-
const
|
|
1051
|
+
const rootTop = gitLine(['rev-parse', '--show-toplevel'], cwd);
|
|
1052
|
+
const redProofs = authoritativeOfKind(store.records, 'red-proof')
|
|
1053
|
+
.filter((r) => r.base === base)
|
|
1054
|
+
.map((r) => ({ ...r, currency: redProofCurrency(rootTop, r) }));
|
|
1015
1055
|
const degrades = authoritativeOfKind(store.records, 'degrade').filter((r) => r.fingerprint === fingerprint);
|
|
1016
1056
|
const finalRun = authoritativeOfKind(store.records, 'final').find((r) => r.fingerprintBefore === fingerprint) ?? null;
|
|
1017
1057
|
// A malformed/unreadable store makes the AUTHORITATIVE selection untrustworthy (a dropped later
|
|
@@ -1037,13 +1077,40 @@ const verdictLine = ({ backend, summary }) => {
|
|
|
1037
1077
|
return `${backend}: no attesting receipt for the current tree (stale or missing)`;
|
|
1038
1078
|
};
|
|
1039
1079
|
|
|
1080
|
+
// Why the run issued no verdict, keyed by the token IT recorded. `certified` maps to nothing —
|
|
1081
|
+
// there is no absence to name. A legacy receipt predates the token: the only honest thing to say
|
|
1082
|
+
// is what the RECORD carries, which is true by construction. Exported as a test seam (keyFor).
|
|
1083
|
+
const COVERAGE_QUALIFIER = Object.freeze({
|
|
1084
|
+
[COVERAGE.notRun]: 'the recorded run issued no coverage verdict',
|
|
1085
|
+
[COVERAGE.unknown]: 'the recorded run produced no readable coverage signal',
|
|
1086
|
+
});
|
|
1087
|
+
const LEGACY_COVERAGE_QUALIFIER = `coverage=${COVERAGE.unknown}: this legacy receipt carries no coverage token and binds no lcov digest`;
|
|
1088
|
+
|
|
1089
|
+
export const coverageQualifierFor = (finalRun) => {
|
|
1090
|
+
if (!finalRun) return '';
|
|
1091
|
+
if (typeof finalRun.coverage === 'string') {
|
|
1092
|
+
const why = COVERAGE_QUALIFIER[finalRun.coverage];
|
|
1093
|
+
return why ? ` — coverage=${finalRun.coverage}: ${why}` : '';
|
|
1094
|
+
}
|
|
1095
|
+
return finalRun.lcovSha256 == null ? ` — ${LEGACY_COVERAGE_QUALIFIER}` : '';
|
|
1096
|
+
};
|
|
1097
|
+
|
|
1040
1098
|
export const renderSummary = (s) => {
|
|
1041
1099
|
const short = (hex) => (typeof hex === 'string' ? `${hex.slice(0, 12)}…` : String(hex));
|
|
1042
1100
|
const evidenceSections = s.evidenceUnavailable
|
|
1043
1101
|
? [` evidence sections WITHHELD — the store is unavailable (${s.storeMalformed} malformed line(s)${s.storeReadError ? `, read error: ${s.storeReadError}` : ''}); a dropped line could resurrect a superseded record — inspect ${s.storePath}`]
|
|
1044
1102
|
: [
|
|
1045
1103
|
` red-proof records (current base): ${s.redProofs.length ? '' : '(none)'}`,
|
|
1046
|
-
...s.redProofs.map((r) =>
|
|
1104
|
+
...s.redProofs.map((r) => {
|
|
1105
|
+
const stale = r.currency?.state === 'stale';
|
|
1106
|
+
const mark = stale ? `STALE (${r.currency.detail})` : 'CURRENT';
|
|
1107
|
+
return ` ${r.testId} — ${mark} · ${r.reds}/${r.runs} red, hash ${short(r.fileHash)}, pre-fix fingerprint ${short(r.fingerprint)}`;
|
|
1108
|
+
}),
|
|
1109
|
+
// The recovery, printed once and only when it is needed — it was written down nowhere, and
|
|
1110
|
+
// the loop paid for that three times.
|
|
1111
|
+
...(s.redProofs.some((r) => r.currency?.state === 'stale')
|
|
1112
|
+
? [' ↳ a STALE proof is refused by run-gates --final: park the fix so the bound test fails again, re-observe it with `core-evidence red-proof "<testId>"`, then restore the fix. Edit test files FIRST and re-observe ONCE per park — every proof bound to an edited file goes stale together.']
|
|
1113
|
+
: []),
|
|
1047
1114
|
` degrade records (current tree): ${s.degrades.length ? '' : '(none)'}`,
|
|
1048
1115
|
...s.degrades.map((d) => ` ${d.backend} — ${d.reason} (${d.timestamp})`),
|
|
1049
1116
|
];
|
|
@@ -1053,10 +1120,17 @@ export const renderSummary = (s) => {
|
|
|
1053
1120
|
' review verdicts (current tree):',
|
|
1054
1121
|
...(s.verdicts.length ? s.verdicts.map((v) => ` ${verdictLine(v)}`) : [' (no receipts)']),
|
|
1055
1122
|
];
|
|
1123
|
+
// The withheld coverage verdict travels here too: an unqualified GREEN repeats, one surface
|
|
1124
|
+
// further on, the false reassurance the checker's own attested=no exists to close. It is read
|
|
1125
|
+
// from the token the RUN recorded — `lcovSha256` says what the receipt binds, never whether a
|
|
1126
|
+
// verdict was issued — and it rides RED as well: an absent verdict is a property of the run, not
|
|
1127
|
+
// of its colour. A LEGACY receipt (no token) is named as exactly that, never as a claim about
|
|
1128
|
+
// what it read. DETAIL beside an unchanged status word.
|
|
1129
|
+
const coverageQualifier = coverageQualifierFor(s.finalRun);
|
|
1056
1130
|
const finalLine = s.evidenceUnavailable
|
|
1057
1131
|
? null
|
|
1058
1132
|
: s.finalRun
|
|
1059
|
-
? ` final gate run: ${s.finalRun.status === 'green' ? 'GREEN' : 'RED'} (${s.finalRun.results.filter((r) => r.ok).length}/${s.finalRun.results.length} gates, ${s.finalRun.timestamp})`
|
|
1133
|
+
? ` final gate run: ${s.finalRun.status === 'green' ? 'GREEN' : 'RED'}${coverageQualifier} (${s.finalRun.results.filter((r) => r.ok).length}/${s.finalRun.results.length} gates, ${s.finalRun.timestamp})`
|
|
1060
1134
|
: ' final gate run: (none recorded for the current tree)';
|
|
1061
1135
|
const lines = [
|
|
1062
1136
|
'core-evidence summary — stateless render (review receipts + evidence store; no ledger, no rounds)',
|
package/tools/coverage-check.mjs
CHANGED
|
@@ -179,6 +179,11 @@ export const attestationState = ({ env, records, fingerprint, base }) => {
|
|
|
179
179
|
return { attesting: true };
|
|
180
180
|
};
|
|
181
181
|
|
|
182
|
+
// The withheld-verdict reason for the one case a VALID context cannot rescue: the run owned the
|
|
183
|
+
// artifact's lifetime and read NO bytes — an absent file and a refused path alike, which is why
|
|
184
|
+
// this states the bytes rather than the skip.
|
|
185
|
+
const NO_BYTES_NO_VERDICT = 'no lcov bytes were read at the checked path, so no verdict could be issued — a run that read nothing certifies nothing; if no gate produces the file, declare a coverage PRODUCER beside the checker (references/modes/gates.md names the exact form)';
|
|
186
|
+
|
|
182
187
|
// ── the red-proof verification arm (D3(c)) ────────────────────────────────────────────────────────
|
|
183
188
|
|
|
184
189
|
// verifyRedProofs({ rootTop, cwd, env }) → { failures: [...], verified: n } | { storeFailure }.
|
|
@@ -276,7 +281,13 @@ export const runCheck = ({ cwd = process.cwd(), env = process.env } = {}) => {
|
|
|
276
281
|
const identityAfter = { fingerprint: computeTreeFingerprint(cwd), base: resolveBase(cwd) };
|
|
277
282
|
const attestAfter = attestationState({ env, records: storeRecords, ...identityAfter });
|
|
278
283
|
const attestation = attestBefore.refusal ? attestBefore : attestAfter;
|
|
279
|
-
const
|
|
284
|
+
const contextValid = attestation.attesting === true && attestBefore.attesting === true;
|
|
285
|
+
// `attested=` states whether a coverage VERDICT was ISSUED — pass OR fail — never that coverage
|
|
286
|
+
// passed: a valid handshake over uncovered lines still reads yes and still exits 1. What no
|
|
287
|
+
// context can rescue is an EMPTY read, and the honest predicate is the BYTES CONSUMED, not the
|
|
288
|
+
// skip flag: an absent lcov and a refused path (a symlink) both read nothing, and attesting over
|
|
289
|
+
// either is the same false green this whole arm exists to close, one layer up.
|
|
290
|
+
const attesting = contextValid && cov.lcovSha256 !== null;
|
|
280
291
|
// EXACTLY ONE fully anchored machine line, the lcov-sha256 contract's sibling — the runner binds
|
|
281
292
|
// it, so a missing/duplicated/injected one is an integrity failure rather than a silent green.
|
|
282
293
|
lines.push(`coverage-check: attested=${attesting ? 'yes' : 'no'}`);
|
|
@@ -284,11 +295,11 @@ export const runCheck = ({ cwd = process.cwd(), env = process.env } = {}) => {
|
|
|
284
295
|
failed = true;
|
|
285
296
|
lines.push(`coverage-check: REFUSED — ${attestation.refusal}`);
|
|
286
297
|
} else if (!attesting) {
|
|
287
|
-
lines.push(`coverage-check: NO VERDICT — ${attestation.reason}`);
|
|
298
|
+
lines.push(`coverage-check: NO VERDICT — ${contextValid ? NO_BYTES_NO_VERDICT : attestation.reason}`);
|
|
288
299
|
}
|
|
289
300
|
// The attestation gates ONLY the coverage claim. Every pre-existing fail-closed refusal above
|
|
290
301
|
// (symlinked lcov, malformed evidence store, unmet red-proof obligation) keeps its own exit 1.
|
|
291
|
-
if (attesting && !failed &&
|
|
302
|
+
if (attesting && !failed && cov.failures.length === 0) {
|
|
292
303
|
lines.push('coverage-check: PASS — every changed Node line is covered');
|
|
293
304
|
}
|
|
294
305
|
return { code: failed ? 1 : 0, lines };
|
|
@@ -314,10 +325,15 @@ store fails (exit 1).
|
|
|
314
325
|
A coverage VERDICT is issued ONLY inside the run that owns the artifact's lifetime: run-gates
|
|
315
326
|
--final deletes the lcov before any gate spawns and hands this checker an attestation context
|
|
316
327
|
(a nonce whose one-way commitment over {nonce, fingerprint, base} is the final-start attempt id).
|
|
317
|
-
One anchored machine line rides every run: coverage-check: attested=<yes|no>.
|
|
318
|
-
|
|
319
|
-
attested=
|
|
320
|
-
|
|
328
|
+
One anchored machine line rides every run: coverage-check: attested=<yes|no>. It states whether a
|
|
329
|
+
coverage VERDICT was ISSUED — pass OR fail — never that coverage passed.
|
|
330
|
+
attested=yes → a verdict was issued over the lcov BYTES this run read (a failing one still
|
|
331
|
+
exits 1); the predicate is the consumed digest, never the skip flag.
|
|
332
|
+
attested=no → NO VERDICT: no handshake, or no lcov bytes were read at all (an absent file or a
|
|
333
|
+
refused path — nothing read, nothing certified). Withholding a verdict never
|
|
334
|
+
changes an exit code: an absent lcov stays exit 0, a refused non-regular path
|
|
335
|
+
keeps its own fail-closed exit 1, and uncovered lines still exit 1. Findings are
|
|
336
|
+
still printed; run-gates carries the withheld verdict as coverage=not-run.
|
|
321
337
|
REFUSED (exit 1) → the context describes another tree, or matches no recorded attempt.
|
|
322
338
|
Residual, stated: ownership of the fixed path is CONVENTION, not enforcement — a concurrent writer
|
|
323
339
|
to it can still place foreign evidence (queued as LCOV-EXCLUSIVE-OWNERSHIP).
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
// coverage-producer.mjs — the kit's half of the shared coverage-producer vocabulary: the reporter
|
|
2
|
+
// flags the canonical suite gate carries, and the CLOSED predicate deciding whether a declared gate
|
|
3
|
+
// cmd actually WRITES the lcov the canonical checker reads. A LEAF — imports nothing, so every
|
|
4
|
+
// consumer (gates-init, gates-declaration, the advisor) decides through ONE predicate.
|
|
5
|
+
// Dependency-free, Node >= 22. No side effects on import.
|
|
6
|
+
|
|
7
|
+
// coverage-producer canon >>> BEGIN drift-guarded region
|
|
8
|
+
// Authored TWICE, byte-identically: in the memory substrate's references/scripts/migrate-gates.mjs
|
|
9
|
+
// and in the composition root's tools/coverage-producer.mjs. Neither side imports the other — the
|
|
10
|
+
// substrate is standalone and must not depend on the root, and the root must not import mirrored
|
|
11
|
+
// bytes — so a TEXT drift guard beside the root's copy holds them equal. Edit BOTH, then re-run the
|
|
12
|
+
// mirror sync.
|
|
13
|
+
//
|
|
14
|
+
// The destination is written against AW_GIT_DIR, which run-gates exports to every gate child on a
|
|
15
|
+
// plain run AND on --final (AW_LCOV_FILE is --final only), so one cmd survives the unmet
|
|
16
|
+
// producer-variable preflight in both modes. The explicit stdout reporter is not decoration:
|
|
17
|
+
// without it the lcov reporter swallows the human TAP/spec stream.
|
|
18
|
+
export const UNIT_TESTS_COVERAGE_FLAGS =
|
|
19
|
+
'--experimental-test-coverage --test-reporter=lcov --test-reporter-destination="$AW_GIT_DIR/agent-workflow-lcov.info" --test-reporter=spec --test-reporter-destination=stdout';
|
|
20
|
+
|
|
21
|
+
// The ONE suite body that produces that lcov with no extra dependency.
|
|
22
|
+
export const COVERAGE_PRODUCER_BODY = `node --test ${UNIT_TESTS_COVERAGE_FLAGS}`;
|
|
23
|
+
|
|
24
|
+
// The per-PM exec wrappers a fill offer puts that body behind. Recognition must cover every form
|
|
25
|
+
// the kit has EMITTED, so the prefixes are matched literally; gates-init's execCmdFor stays the one
|
|
26
|
+
// EMITTER and is bound to this list by a named acceptance test, never by a second grammar.
|
|
27
|
+
const PRODUCER_EXEC_PREFIXES = Object.freeze([
|
|
28
|
+
'COREPACK_ENABLE_NETWORK=0 npm exec --offline --script-shell /bin/sh -- ',
|
|
29
|
+
'COREPACK_ENABLE_NETWORK=0 pnpm exec -- ',
|
|
30
|
+
'COREPACK_ENABLE_NETWORK=0 yarn exec -- ',
|
|
31
|
+
]);
|
|
32
|
+
|
|
33
|
+
// A trailing suffix is the project's own test paths; a leading one would mean the body is not what
|
|
34
|
+
// this cmd runs. The tail passes a POSITIVE closed grammar — every whitespace-separated token must
|
|
35
|
+
// be path-shaped — never an operator blocklist: `node --test <flags> && rm -f <lcov>` runs the
|
|
36
|
+
// suite and then DELETES the file, so an open-ended tail would certify a producer that leaves
|
|
37
|
+
// nothing behind, and scanning for operator BYTES would put the incomplete-scan failure on the
|
|
38
|
+
// unsafe side (a missed operator is a dead pair) instead of the mild one (an unrecognised
|
|
39
|
+
// legitimate tail merely withholds the offer — add the gate by hand).
|
|
40
|
+
// The token set is everything that appears in a PATH or a glob and can never sequence, redirect or
|
|
41
|
+
// substitute a command — quoting and `~` included, `( ) $ ` ; & | < > # \` excluded. SCOPE, stated
|
|
42
|
+
// exactly: the screen judges each token's SOURCE bytes. Quote removal adds none, but brace SEQUENCE
|
|
43
|
+
// expansion does — `{Y..a}` yields ``[ \ ] ^ _ ` `` (probed) — so "no new bytes" would be a false
|
|
44
|
+
// claim. What holds is the property that matters: bash does not re-scan an expansion result as
|
|
45
|
+
// syntax, so a byte arriving that way is literal argument DATA, never an operator. The leading-`-`
|
|
46
|
+
// exclusion is weaker still — a FIRST-ORDER screen only, defeated by `'--flag'` and
|
|
47
|
+
// `{path,--flag}`. It is kept because the tail is the project's test PATHS and it costs only a loud
|
|
48
|
+
// withhold. Deciding an argument's post-expansion identity needs a shell lexer, which this family
|
|
49
|
+
// deliberately has NOWHERE (AD-079). So the claim is "configured with the reporters", never "the
|
|
50
|
+
// lcov survives the command"; a run that produces none is caught honestly at runtime as
|
|
51
|
+
// `skipped-no-lcov`.
|
|
52
|
+
const PRODUCER_PATH_TOKEN = /^(?!-)[A-Za-z0-9_./*{},:@+=~?[\]!'"-]+$/;
|
|
53
|
+
const pathShapedTail = (tail) => tail === '' || tail.split(/[ \t]+/).every((token) => PRODUCER_PATH_TOKEN.test(token));
|
|
54
|
+
const carriesProducerBody = (text) =>
|
|
55
|
+
text === COVERAGE_PRODUCER_BODY ||
|
|
56
|
+
(text.startsWith(`${COVERAGE_PRODUCER_BODY} `) && pathShapedTail(text.slice(COVERAGE_PRODUCER_BODY.length).trim()));
|
|
57
|
+
|
|
58
|
+
// matchesCoverageProducer(cmd) → CLOSED-WORLD over the full command forms the kit emits, never a
|
|
59
|
+
// substring probe: `echo "$AW_GIT_DIR/agent-workflow-lcov.info"`, a half-written reporter flag set,
|
|
60
|
+
// or the path as a bare substring must all read as NOT a producer — otherwise the checker is
|
|
61
|
+
// declared over a gate that writes nothing and then PASSES while certifying nothing.
|
|
62
|
+
export const matchesCoverageProducer = (cmd) => {
|
|
63
|
+
if (typeof cmd !== 'string') return false;
|
|
64
|
+
const trimmed = cmd.trim();
|
|
65
|
+
if (carriesProducerBody(trimmed)) return true;
|
|
66
|
+
return PRODUCER_EXEC_PREFIXES.some((prefix) => trimmed.startsWith(prefix) && carriesProducerBody(trimmed.slice(prefix.length)));
|
|
67
|
+
};
|
|
68
|
+
// coverage-producer canon <<< END drift-guarded region
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
// coverage-state.mjs — the coverage vocabulary LEAF (kit-inert-gate Phase 2, Decision 8).
|
|
2
|
+
//
|
|
3
|
+
// Two consumers need the same closed value set and cannot import each other: run-gates.mjs REPORTS
|
|
4
|
+
// the token on its summary line and records it on the `--final` receipt, and core-evidence.mjs
|
|
5
|
+
// VALIDATES that recorded field — and run-gates already imports core-evidence (the sole-writer
|
|
6
|
+
// boundary), so the dependency can only run this way. ONE home, no drift guard needed: the
|
|
7
|
+
// repo-lex.mjs / gates-declaration.mjs leaf idiom.
|
|
8
|
+
//
|
|
9
|
+
// No imports, no CLI, no side effects. Dependency-free, Node >= 22.
|
|
10
|
+
|
|
11
|
+
// The CLOSED set, with ONE value defined for every run outcome. It is DETAIL, never a new state:
|
|
12
|
+
// no exit code, no `status=` token, no receipt status and no commit-guard disposition reads it.
|
|
13
|
+
export const COVERAGE = Object.freeze({
|
|
14
|
+
certified: 'certified', // the canonical checker consumed lcov bytes and ISSUED a verdict — pass OR fail
|
|
15
|
+
notRun: 'not-run', // the checker ran and issued NO verdict: nothing was read, or the run holds no attestation context
|
|
16
|
+
none: 'none', // no canonical checker ran here (an --only subset, the --pre-review derived subset)
|
|
17
|
+
unknown: 'unknown', // the run ended before the gates produced the signal, or the signal is unreadable
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
// What may ride a `final` receipt. `none` never can: --final REFUSES a declaration that does not
|
|
21
|
+
// carry the canonical checker last, so a final run always selects it (run-gates.mjs --final
|
|
22
|
+
// preflight) — a recorded `none` would mean the receipt describes a run that could not have
|
|
23
|
+
// happened, and the validator says so instead of storing it.
|
|
24
|
+
export const FINAL_COVERAGE_STATES = Object.freeze([COVERAGE.certified, COVERAGE.notRun, COVERAGE.unknown]);
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
// declared-paths.mjs — the shared resolution + containment leaf for a DECLARED settings path
|
|
2
|
+
// (`sandbox.filesystem.allowWrite`). Two consumers must never disagree about what such an entry
|
|
3
|
+
// MEANS: the advisor's worktrees-dir convergence lane asks "does a declared entry cover this dir?",
|
|
4
|
+
// and the autonomy render's allowWrite degrade asks "does this entry resolve outside the repo?" —
|
|
5
|
+
// the same resolution and the same containment rule, read from opposite ends. A second resolver
|
|
6
|
+
// would let those two answers drift, so the rule lives here once.
|
|
7
|
+
//
|
|
8
|
+
// A LEAF: imports node:path only (never the advisor, never the render — the advisor already imports
|
|
9
|
+
// the render). No side effects on import. Dependency-free, Node >= 22.
|
|
10
|
+
|
|
11
|
+
import { resolve, sep } from 'node:path';
|
|
12
|
+
|
|
13
|
+
// What counts as a RESOLVABLE declared entry, shared by both consumers so they cannot disagree about
|
|
14
|
+
// which entries are readable at all. A blank entry is rejected deliberately: it means nothing to a
|
|
15
|
+
// host, but it would resolve to the project root and thereby read as a grant on the whole repo.
|
|
16
|
+
export const isResolvableDeclaredEntry = (entry) => typeof entry === 'string' && entry.trim() !== '';
|
|
17
|
+
|
|
18
|
+
// Resolve a declared entry the way a host that honors the key resolves it: `~` and `~/…` against the
|
|
19
|
+
// resolved home, every other form (relative or absolute) against the project root.
|
|
20
|
+
export const resolveDeclaredDir = (entry, { home, root }) => {
|
|
21
|
+
if (entry === '~') return resolve(home);
|
|
22
|
+
if (entry.startsWith('~/')) return resolve(home, entry.slice(2));
|
|
23
|
+
return resolve(root, entry);
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
// Ancestor-or-equal containment on PATH SEGMENTS, never a raw string prefix — a grant on `<p>/farm`
|
|
27
|
+
// must never read as a grant on the sibling `<p>/farmhouse`. A grant on a DESCENDANT never covers
|
|
28
|
+
// its parent: the parent dir is the one a provision writes into.
|
|
29
|
+
export const dirCovers = (containerDir, candidateDir) => {
|
|
30
|
+
const base = containerDir.endsWith(sep) ? containerDir.slice(0, -sep.length) : containerDir;
|
|
31
|
+
return candidateDir === base || candidateDir.startsWith(`${base}${sep}`);
|
|
32
|
+
};
|
|
@@ -67,14 +67,15 @@ const RAW_BACKENDS = [
|
|
|
67
67
|
roleContracts: {
|
|
68
68
|
execute: {
|
|
69
69
|
invocations: [
|
|
70
|
-
'codex-exec <plan-file|->',
|
|
71
|
-
'codex-exec <plan-file|-> -- <extra codex flags...>',
|
|
70
|
+
'codex-exec [--nonce <n>] <plan-file|->',
|
|
71
|
+
'codex-exec [--nonce <n>] <plan-file|-> -- <extra codex flags...>',
|
|
72
72
|
],
|
|
73
73
|
grounding: "automatic — the root AGENTS.md (Hard Constraints) is auto-merged into codex's context and the wrapper prepends the orchestrator execution contract; no grounding flags",
|
|
74
74
|
continue: [
|
|
75
|
-
'codex-exec --resume-last <plan-file|->',
|
|
76
|
-
'codex-exec --resume <session-id> <plan-file|->',
|
|
75
|
+
'codex-exec --resume-last [--nonce <n>] <plan-file|->',
|
|
76
|
+
'codex-exec --resume <session-id> [--nonce <n>] <plan-file|->',
|
|
77
77
|
],
|
|
78
|
+
receipt: "side effect — a NONCED run mints ONE exec receipt beside the delegation store: the dispatch nonce seam is the AW_DISPATCH_NONCE environment value or its plain-argument equivalent --nonce <n>, recognised ONLY before the prompt operand (after the operand or a literal '--' it is passthrough payload, never a flag), under the safe grammar [A-Za-z0-9._-]{1,64} — anything else, a duplicate, or a flag disagreeing with a non-empty env value refuses PRE-SPEND. The store directory resolves exactly as the kit's delegation store does: the dirname of an ABSOLUTE AW_DELEGATION_STORE (a relative one, or one ending in a path separator, refuses), else the git common dir. The artifact is agent-workflow-exec-receipt-<backendLength>-<backend>-<nonce>.json in two states: 'reserved' is written atomically and NO-CLOBBER immediately before the CLI runs — that write IS the nonce reservation, so a second dispatch on the same nonce, or an already-taken report name, refuses BEFORE any spend — and 'terminal' replaces it in place at exit. A nonced run also refuses pre-spend when no timeout/gtimeout binary can cap it (an accounted dispatch that cannot be capped can never honour the terminal-exit rule; a nonce-LESS run still warns and runs uncapped), when node is missing (the mint core), and when the prompt rides on stdin instead of a contract FILE — contractDigest is computed BY THIS WRAPPER from the dispatch file it was actually handed, so the kit can refuse a run that executed a different contract than the one it opened. That digest is taken from the SAME bytes already read as the prompt, never a second open of the path: two reads leave a window in which the file can be swapped, and the run would then execute one contract while its receipt claimed the digest of another. The header's own nonce must EQUAL the dispatch nonce — 'dispatch open' copies the nonce FROM the header, so a disagreeing --nonce could only reserve an identity no return would ever absorb, and it refuses pre-spend. A contract file edited BETWEEN 'dispatch open' and the run is caught at ABSORB by the contractDigest comparison, not pre-spend: the wrapper never reads the ledger, and that boundary is what the whole lane rests on. At exit the wrapper FIRST re-reads its reservation and verifies its own opaque owner token — a foreign owner refuses having published NOTHING, neither report nor receipt — THEN writes the delegate's final message atomically to agent-workflow-exec-report-<backendLength>-<backend>-<nonce>.txt, THEN re-verifies the owner and REPLACES the reservation with the terminal receipt {schema, kind, state, backend, nonce, owner, contractDigest, wrapperVersion, posture {model, effort, tier}, capS, killGraceS, sessionId, exitStatus, outcome, reportDigest, reportLength, timestamp}: the report is complete on disk before any artifact says the run arrived. capS and killGraceS are the cap the run ACTUALLY applied. outcome is the wrapper's own SUBSET of the ledger's vocabulary — exit 0 with a session id -> success, exit 0 without one -> missing-identity, ANY nonzero exit including the timeout codes 124 and 137 -> transport-failure; every orchestrator judgment is recorded at absorb time, never claimed here. The session id is captured BEFORE outcome branching, so a FAILED run records one too; in resume mode it is the validated resume id. FAIL-CLOSED, deliberately NOT the review lane's warn-only receipt: a publication that cannot complete exits nonzero with a DISTINCT status, and the message states only what the run can still prove. 70: the reservation could not be verified BEFORE any publication — NOTHING was published, not the report and not the receipt, and because the artifact found there belongs to another run it is never a '--no-receipt' source. 71: a publication stopped after that point — either the report write failed (nothing beyond the reservation was published; the '--no-receipt' absorb then records reportLength 0, ineligible by the name empty-report) or the report IS on disk and the terminal receipt was not completed (the absorb reads it, report-if-present). The post-report lane never claims the reservation still stands, because after that point its fate is no longer something this run observed. Every lane names the tree as partial/dirtied rather than untouched. A nonce-LESS invocation is byte-unchanged: no reservation, no receipt, no artifact, no node.",
|
|
78
79
|
passthrough: {
|
|
79
80
|
policy: 'guarded',
|
|
80
81
|
blocked: ['-c*', '--config*', '-s*', '--sandbox*', '--dangerously-bypass-approvals-and-sandbox', '--dangerously-bypass-hook-trust', '--full-auto', '--oss', '--local-provider*', '-p*', '--profile*', '-m*', '--model*', '-o*', '--output-last-message*', '--json*', '--color*', '--output-schema*', '--ephemeral*'],
|