@sabaiway/agent-workflow-kit 5.2.0 → 5.4.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 +110 -0
- package/README.md +1 -1
- package/SKILL.md +1 -1
- package/bridges/codex-cli-bridge/SKILL.md +11 -3
- package/bridges/codex-cli-bridge/bin/codex-exec.sh +154 -35
- package/bridges/codex-cli-bridge/bin/codex-exec.test.mjs +257 -4
- package/bridges/codex-cli-bridge/bin/codex-review.sh +1 -1
- package/bridges/codex-cli-bridge/capability.json +3 -2
- package/bridges/codex-cli-bridge/references/driving-codex.md +5 -3
- package/bridges/codex-cli-bridge/references/sandbox-and-flags.md +33 -15
- 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/gates.md +7 -2
- package/references/modes/recommendations.md +3 -1
- package/references/modes/upgrade.md +1 -1
- package/references/modes/velocity.md +5 -1
- package/references/scripts/archive-decisions.mjs +340 -15
- package/references/scripts/archive-decisions.test.mjs +522 -2
- package/references/scripts/migrate-gates.mjs +102 -10
- package/references/scripts/migrate-gates.test.mjs +37 -0
- package/tools/core-evidence.mjs +42 -2
- 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 +1 -0
- package/tools/dispatch-record.mjs +926 -0
- package/tools/doc-parity.mjs +19 -4
- package/tools/flow-check.mjs +48 -12
- package/tools/gates-declaration.mjs +49 -0
- package/tools/gates-init.mjs +83 -6
- package/tools/recommendations.mjs +63 -19
- package/tools/run-gates.mjs +111 -32
- 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/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 });
|
|
@@ -670,6 +674,17 @@ export const validateEvidenceRecord = (record) => {
|
|
|
670
674
|
if (record.lcovSha256 !== null && (typeof record.lcovSha256 !== 'string' || !HEX64_RE.test(record.lcovSha256))) {
|
|
671
675
|
return { ok: false, reason: 'final: lcovSha256 must be a 64-hex sha256 of the consumed lcov file, or null when none was produced' };
|
|
672
676
|
}
|
|
677
|
+
// The run's coverage token is ADDITIVE (absent = a pre-token receipt, still valid) and CLOSED.
|
|
678
|
+
// `certified` is cross-checked against the bound digest — a run that certified a verdict read
|
|
679
|
+
// bytes, so the two fields can never disagree in the store.
|
|
680
|
+
if ('coverage' in record) {
|
|
681
|
+
if (!FINAL_COVERAGE_STATES.includes(record.coverage)) {
|
|
682
|
+
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)` };
|
|
683
|
+
}
|
|
684
|
+
if (record.coverage === COVERAGE.certified && record.lcovSha256 === null) {
|
|
685
|
+
return { ok: false, reason: 'final: coverage "certified" requires a bound lcovSha256 — a certified verdict was issued over lcov bytes that were read' };
|
|
686
|
+
}
|
|
687
|
+
}
|
|
673
688
|
if (!isNonEmptyString(record.timestamp)) return { ok: false, reason: 'final: timestamp must be a non-empty string' };
|
|
674
689
|
return { ok: true };
|
|
675
690
|
}
|
|
@@ -1037,6 +1052,24 @@ const verdictLine = ({ backend, summary }) => {
|
|
|
1037
1052
|
return `${backend}: no attesting receipt for the current tree (stale or missing)`;
|
|
1038
1053
|
};
|
|
1039
1054
|
|
|
1055
|
+
// Why the run issued no verdict, keyed by the token IT recorded. `certified` maps to nothing —
|
|
1056
|
+
// there is no absence to name. A legacy receipt predates the token: the only honest thing to say
|
|
1057
|
+
// is what the RECORD carries, which is true by construction. Exported as a test seam (keyFor).
|
|
1058
|
+
const COVERAGE_QUALIFIER = Object.freeze({
|
|
1059
|
+
[COVERAGE.notRun]: 'the recorded run issued no coverage verdict',
|
|
1060
|
+
[COVERAGE.unknown]: 'the recorded run produced no readable coverage signal',
|
|
1061
|
+
});
|
|
1062
|
+
const LEGACY_COVERAGE_QUALIFIER = `coverage=${COVERAGE.unknown}: this legacy receipt carries no coverage token and binds no lcov digest`;
|
|
1063
|
+
|
|
1064
|
+
export const coverageQualifierFor = (finalRun) => {
|
|
1065
|
+
if (!finalRun) return '';
|
|
1066
|
+
if (typeof finalRun.coverage === 'string') {
|
|
1067
|
+
const why = COVERAGE_QUALIFIER[finalRun.coverage];
|
|
1068
|
+
return why ? ` — coverage=${finalRun.coverage}: ${why}` : '';
|
|
1069
|
+
}
|
|
1070
|
+
return finalRun.lcovSha256 == null ? ` — ${LEGACY_COVERAGE_QUALIFIER}` : '';
|
|
1071
|
+
};
|
|
1072
|
+
|
|
1040
1073
|
export const renderSummary = (s) => {
|
|
1041
1074
|
const short = (hex) => (typeof hex === 'string' ? `${hex.slice(0, 12)}…` : String(hex));
|
|
1042
1075
|
const evidenceSections = s.evidenceUnavailable
|
|
@@ -1053,10 +1086,17 @@ export const renderSummary = (s) => {
|
|
|
1053
1086
|
' review verdicts (current tree):',
|
|
1054
1087
|
...(s.verdicts.length ? s.verdicts.map((v) => ` ${verdictLine(v)}`) : [' (no receipts)']),
|
|
1055
1088
|
];
|
|
1089
|
+
// The withheld coverage verdict travels here too: an unqualified GREEN repeats, one surface
|
|
1090
|
+
// further on, the false reassurance the checker's own attested=no exists to close. It is read
|
|
1091
|
+
// from the token the RUN recorded — `lcovSha256` says what the receipt binds, never whether a
|
|
1092
|
+
// verdict was issued — and it rides RED as well: an absent verdict is a property of the run, not
|
|
1093
|
+
// of its colour. A LEGACY receipt (no token) is named as exactly that, never as a claim about
|
|
1094
|
+
// what it read. DETAIL beside an unchanged status word.
|
|
1095
|
+
const coverageQualifier = coverageQualifierFor(s.finalRun);
|
|
1056
1096
|
const finalLine = s.evidenceUnavailable
|
|
1057
1097
|
? null
|
|
1058
1098
|
: 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})`
|
|
1099
|
+
? ` 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
1100
|
: ' final gate run: (none recorded for the current tree)';
|
|
1061
1101
|
const lines = [
|
|
1062
1102
|
'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
|
+
};
|
|
@@ -86,6 +86,7 @@ const RAW_BACKENDS = [
|
|
|
86
86
|
'threat model: the sidecar byte and grammar screens detect corrupted input under a trusted parent environment. A hostile parent environment — including exported shell functions or PATH substitution of core/backend commands — is outside the threat model and can substitute the backend itself. Targeted shadow-proof resolution protects banner/dispatch honesty from accidental shadowing; it is not an environment security boundary',
|
|
87
87
|
'the exec posture banner appends a banner-only timeout=<duration|uncapped> field — exactly the duration handed to timeout(1), uncapped when no timeout/gtimeout binary caps the run; INFORMATIONAL only: it is never persisted in a receipt or session sidecar',
|
|
88
88
|
'quote the posture banner verbatim when labeling this dispatch — the banner is the machine-stated posture; a prose re-type drifts',
|
|
89
|
+
'every-run nested-sandbox scan (DUAL policy, deliberately two different rules): the scan runs on EVERY completed run, not only a failed one, because a run that SURVIVES the nested-sandbox failure exits 0 with an ungrounded answer and nothing said so. Failed run (rc != 0): the existing loose whole-trace combination rule prints the recovery hint. Successful run (rc == 0): a warning fires ONLY on precise per-item evidence — both a sandbox-mechanism token AND a permission/read-only failure token inside the aggregated_output of ONE command_execution item whose failure is PROVEN (a nonzero exit_code, or the serialized status "failed"); a null exit_code is never failure by itself, tokens split across two items never fire, and a successful command\'s output never fires. The answer is printed FIRST on stdout, then the warning on stderr. HONEST RESIDUAL: the exit status does NOT change on that lane (a distinct nonzero exit would give a heuristic scan DENY polarity, refusing real work whenever the scan over-warns), so an orchestrator keying on exit status alone can still bank an ungrounded answer — the stderr warning is the signal',
|
|
89
90
|
],
|
|
90
91
|
},
|
|
91
92
|
review: {
|