@sabaiway/agent-workflow-kit 2.1.0 → 3.1.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 +94 -1
- package/README.md +7 -5
- package/SKILL.md +16 -8
- package/bin/install.mjs +11 -2
- package/bridges/antigravity-cli-bridge/SKILL.md +8 -1
- package/bridges/antigravity-cli-bridge/bin/agy-review-honesty.test.mjs +163 -0
- package/bridges/antigravity-cli-bridge/bin/agy-review-model-screen.test.mjs +76 -0
- package/bridges/antigravity-cli-bridge/bin/agy-review.sh +80 -15
- package/bridges/antigravity-cli-bridge/bin/agy-review.test.mjs +171 -22
- package/bridges/antigravity-cli-bridge/bin/agy.test.mjs +59 -41
- package/bridges/antigravity-cli-bridge/capability.json +3 -2
- package/bridges/codex-cli-bridge/SKILL.md +8 -1
- package/bridges/codex-cli-bridge/bin/codex-exec.test.mjs +67 -22
- package/bridges/codex-cli-bridge/bin/codex-review-honesty.test.mjs +156 -0
- package/bridges/codex-cli-bridge/bin/codex-review.sh +71 -10
- package/bridges/codex-cli-bridge/bin/codex-review.test.mjs +158 -27
- package/bridges/codex-cli-bridge/capability.json +3 -2
- package/capability.json +1 -1
- package/migrations/3.0.0-hardened-core-loop.md +44 -0
- package/migrations/README.md +1 -1
- package/package.json +2 -2
- package/references/hooks/gate-approve.mjs +1 -1
- package/references/modes/bootstrap.md +3 -3
- package/references/modes/commit-guard.md +19 -0
- package/references/modes/core-evidence.md +15 -0
- package/references/modes/coverage-check.md +12 -0
- package/references/modes/doc-parity.md +7 -6
- package/references/modes/gates.md +7 -6
- package/references/modes/grounding.md +1 -2
- package/references/modes/hook.md +1 -1
- package/references/modes/recommendations.md +1 -0
- package/references/modes/review-state.md +3 -3
- package/references/modes/upgrade.md +6 -6
- package/references/modes/velocity.md +2 -2
- package/references/modes/worktrees.md +120 -0
- package/references/scripts/archive-decisions.mjs +3 -3
- package/references/scripts/archive-decisions.test.mjs +5 -5
- package/references/scripts/check-docs-size-cli.test.mjs +41 -0
- package/references/scripts/check-docs-size.mjs +46 -29
- package/references/scripts/install-git-hooks-repo-exec.test.mjs +82 -0
- package/references/scripts/install-git-hooks.mjs +90 -18
- package/references/scripts/install-git-hooks.test.mjs +102 -0
- package/references/scripts/migrate-gates-branches.test.mjs +157 -0
- package/references/scripts/migrate-gates.mjs +395 -0
- package/references/scripts/migrate-gates.test.mjs +284 -0
- package/references/templates/agent_rules.md +2 -2
- package/tools/ack-write.mjs +1 -1
- package/tools/atomic-write.mjs +2 -2
- package/tools/autonomy-config.mjs +1 -1
- package/tools/autonomy-doctor.mjs +2 -2
- package/tools/autonomy-write.mjs +1 -1
- package/tools/bridge-settings-read.mjs +1 -1
- package/tools/bridge-settings.mjs +1 -1
- package/tools/changed-surface.mjs +16 -77
- package/tools/cheap-agents.mjs +2 -2
- package/tools/commands.mjs +26 -10
- package/tools/commit-guard.mjs +167 -0
- package/tools/core-evidence.mjs +914 -0
- package/tools/coverage-check.mjs +260 -0
- package/tools/delegation.mjs +1 -1
- package/tools/detect-backends.mjs +3 -3
- package/tools/doc-parity.mjs +11 -27
- package/tools/engine-source.mjs +1 -1
- package/tools/family-members.mjs +1 -1
- package/tools/family-registry.mjs +1 -1
- package/tools/fs-safe.mjs +1 -1
- package/tools/gate-hook.mjs +1 -1
- package/tools/{seed-gates.mjs → gates-init.mjs} +58 -52
- package/tools/grounding.mjs +19 -96
- package/tools/hide-footprint.mjs +1 -1
- package/tools/inject-methodology.mjs +72 -41
- package/tools/known-footprint.mjs +1 -1
- package/tools/labels.mjs +1 -1
- package/tools/lcov.mjs +6 -10
- package/tools/lens-region.mjs +1 -1
- package/tools/manifest/validate.mjs +19 -0
- package/tools/migrate-adr-store.mjs +5 -5
- package/tools/orchestration-config.mjs +1 -1
- package/tools/orchestration-write.mjs +1 -1
- package/tools/presentation.mjs +1 -1
- package/tools/procedures.mjs +3 -3
- package/tools/recipes.mjs +62 -11
- package/tools/recommendations.mjs +97 -14
- package/tools/release-scan.mjs +10 -3
- package/tools/renderers.mjs +1 -1
- package/tools/review-state.mjs +291 -343
- package/tools/run-gates.mjs +216 -92
- package/tools/sandbox-masks.mjs +14 -14
- package/tools/semver-lite.mjs +1 -1
- package/tools/set-autonomy.mjs +1 -1
- package/tools/set-recipe.mjs +2 -2
- package/tools/setup-backends.mjs +1 -1
- package/tools/surface.mjs +1 -1
- package/tools/uninstall.mjs +1 -1
- package/tools/velocity-profile.mjs +9 -9
- package/tools/view-model.mjs +1 -1
- package/tools/worktrees.mjs +2292 -0
- package/references/modes/fold-completeness.md +0 -30
- package/references/modes/review-ledger.md +0 -34
- package/references/templates/verification-profile.json +0 -10
- package/tools/fold-completeness-run.mjs +0 -1120
- package/tools/fold-completeness.mjs +0 -672
- package/tools/review-ledger-core.mjs +0 -428
- package/tools/review-ledger-write.mjs +0 -647
- package/tools/review-ledger.mjs +0 -630
- package/tools/sarif.mjs +0 -52
- package/tools/verification-profile.mjs +0 -219
package/tools/run-gates.mjs
CHANGED
|
@@ -18,38 +18,31 @@
|
|
|
18
18
|
// Honest outcomes — each distinct, never a silent green (the exit-code table + the summary-line
|
|
19
19
|
// schema are pinned by run-gates.test.mjs):
|
|
20
20
|
// 0 ok · 1 gate failure · 2 usage · 3 missing declaration · 4 empty gates list ·
|
|
21
|
-
// 5 malformed/invalid declaration · 6 bash unavailable ·
|
|
22
|
-
//
|
|
23
|
-
//
|
|
24
|
-
// under another shell.
|
|
21
|
+
// 5 malformed/invalid declaration · 6 bash unavailable · 8 --final receipt not written.
|
|
22
|
+
// Gate `cmd` lines are BASH command lines (brace/glob expansion); a host without bash gets the
|
|
23
|
+
// loud exit-6 preflight error, never a silent reinterpretation under another shell.
|
|
25
24
|
//
|
|
26
|
-
// The runner itself WRITES NOTHING
|
|
27
|
-
//
|
|
28
|
-
//
|
|
29
|
-
// runner never opens the ledger file itself (an import/structure pin holds the boundary). The
|
|
30
|
-
// record carries the FULL declaration + exactly what ran (a --only subset records honestly as a
|
|
31
|
-
// subset — it never satisfies quality-green) + the tree fingerprint BEFORE and AFTER the run (a
|
|
32
|
-
// mutating gate attests no particular tree). Dependency-free, Node >= 18. No side effects on import.
|
|
25
|
+
// The runner itself WRITES NOTHING on a plain run. `--final` (D3(a)) is the ONE writing mode:
|
|
26
|
+
// every attempt lands in the core-evidence store via its sole writer. Dependency-free.
|
|
27
|
+
// No side effects on import.
|
|
33
28
|
|
|
34
|
-
import { readFileSync, lstatSync } from 'node:fs';
|
|
35
|
-
import { join } from 'node:path';
|
|
29
|
+
import { readFileSync, lstatSync, unlinkSync, realpathSync } from 'node:fs';
|
|
30
|
+
import { join, isAbsolute } from 'node:path';
|
|
36
31
|
import { spawnSync } from 'node:child_process';
|
|
37
|
-
import { pathToFileURL } from 'node:url';
|
|
32
|
+
import { pathToFileURL, fileURLToPath } from 'node:url';
|
|
33
|
+
import { createHash, randomUUID } from 'node:crypto';
|
|
38
34
|
import { computeTreeFingerprint } from './review-state.mjs';
|
|
39
|
-
|
|
40
|
-
//
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
import { foldSuiteCredit } from './fold-completeness.mjs';
|
|
44
|
-
|
|
45
|
-
// The gate id the (a) credit applies to — the SAME command the fold runner resolves as its suite.
|
|
46
|
-
export const UNIT_TESTS_GATE_ID = 'unit-tests';
|
|
35
|
+
// The D3(a) final receipt rides the core-evidence SOLE WRITER (the sole-writer boundary — this
|
|
36
|
+
// runner never opens the store itself) + the canonical per-kind serialization its hashes bind.
|
|
37
|
+
import { appendEvidenceRecord, resolveEvidencePath, readEvidence, canonicalKindSerialization, EVIDENCE_SCHEMA_VERSION } from './core-evidence.mjs';
|
|
38
|
+
import { LCOV_BASENAME } from './coverage-check.mjs';
|
|
47
39
|
|
|
48
40
|
// The per-project declaration (strict JSON, hand-editable). cwd-relative — errors show a path the
|
|
49
41
|
// user can open (the orchestration-config CONFIG_REL idiom).
|
|
50
42
|
export const GATES_REL = 'docs/ai/gates.json';
|
|
51
43
|
|
|
52
44
|
// The full exit-code table — one distinct code per honest outcome (never a silent green).
|
|
45
|
+
// 7 is RETIRED (the deleted --record arm's outcome) — never reused for a new meaning.
|
|
53
46
|
export const EXIT = Object.freeze({
|
|
54
47
|
ok: 0,
|
|
55
48
|
fail: 1,
|
|
@@ -58,10 +51,9 @@ export const EXIT = Object.freeze({
|
|
|
58
51
|
empty: 4,
|
|
59
52
|
malformed: 5,
|
|
60
53
|
noBash: 6,
|
|
61
|
-
// --
|
|
62
|
-
//
|
|
63
|
-
|
|
64
|
-
recordFailed: 7,
|
|
54
|
+
// --final could not write its receipt (a corrupt store, an fs refusal): green gates WITHOUT a
|
|
55
|
+
// written receipt never read as success (D3(a)).
|
|
56
|
+
finalFailed: 8,
|
|
65
57
|
});
|
|
66
58
|
|
|
67
59
|
// A tagged failure carrying its process exit code (the shared orchestration-config idiom).
|
|
@@ -74,21 +66,23 @@ const SPAWN_FAILED_CODE = -1;
|
|
|
74
66
|
const MAX_GATE_OUTPUT_BYTES = 64 * 1024 * 1024;
|
|
75
67
|
|
|
76
68
|
const USAGE = [
|
|
77
|
-
'usage: run-gates.mjs [--cwd <dir>] [--only <id>]... [--
|
|
69
|
+
'usage: run-gates.mjs [--cwd <dir>] [--only <id>]... [--final] [--help]',
|
|
70
|
+
'',
|
|
71
|
+
'--final runs the FULL declared matrix as the D3(a) final verification run: it refuses a',
|
|
72
|
+
'declaration lacking the canonical core checks (the review-state + coverage-check gates),',
|
|
73
|
+
'deletes the stale git-dir lcov first, exports AW_GIT_DIR to every gate cmd, records EVERY',
|
|
74
|
+
'attempt (start + completed green/red) in the core-evidence store, and binds the receipt to',
|
|
75
|
+
'{ fingerprint before/after, the full declaration, per-gate results, the canonical red-proof +',
|
|
76
|
+
'degrade evidence hashes, the lcov sha }. --final refuses --only (a subset never attests).',
|
|
78
77
|
'',
|
|
79
78
|
`Runs the gates declared in <cwd>/${GATES_REL} (one bash command line each, project root as cwd).`,
|
|
80
79
|
'Prints a per-gate PASS/FAIL table + one machine-readable summary line; exit 0 iff all green.',
|
|
81
|
-
'--record additionally mints ONE v4 gate-run record into the review ledger via its sole writer',
|
|
82
|
-
'(the D5 green-baseline receipt; needs a single in-flight plan): the full declaration + what ran',
|
|
83
|
-
'+ the pre/post tree fingerprints. A red run records honestly; a --only subset records as a subset.',
|
|
84
|
-
'In --record mode the unit-tests gate is CREDITED (not re-spawned) when it is the FIRST declared gate',
|
|
85
|
-
'AND the fold-completeness runner already ran that EXACT command green at the current tree',
|
|
86
|
-
'(fingerprint-bound + exit-0 + cmd-identity); positioned after another gate, it always re-spawns.',
|
|
87
80
|
'Sandbox-safe: the runner itself needs no network and writes only repo-local state — the D4 sandbox',
|
|
88
81
|
'lane; each DECLARED gate command is the project\'s own, so ITS sandbox-safety is command-shape',
|
|
89
82
|
'dependent (first try the sandbox-safe shape — cache under $TMPDIR, offline/notifier off).',
|
|
90
83
|
`Exit codes: 0 ok · 1 gate failure · 2 usage · 3 missing declaration · 4 empty gates list ·`,
|
|
91
|
-
'5 malformed/invalid declaration · 6 bash unavailable ·
|
|
84
|
+
'5 malformed/invalid declaration · 6 bash unavailable ·',
|
|
85
|
+
'8 --final asked but its receipt could not be written (green gates never read as success without it).',
|
|
92
86
|
].join('\n');
|
|
93
87
|
|
|
94
88
|
// ── declaration validation (malformed → exit 5, loud `path: reason`) ─────────────────
|
|
@@ -187,13 +181,11 @@ export const selectGates = (gates, onlyIds) => {
|
|
|
187
181
|
// Spawn one gate cmd via bash from the project root. `cmd` is a BASH command line by contract
|
|
188
182
|
// (the declaration's _README states it): this repo's own gate matrix needs brace+glob expansion,
|
|
189
183
|
// which /bin/sh does not perform — hence bash explicitly, never the platform default shell.
|
|
190
|
-
// NODE_TEST_CONTEXT is stripped
|
|
191
|
-
//
|
|
192
|
-
//
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
export const spawnGateViaBash = (cmd, cwd) => {
|
|
196
|
-
const env = { ...process.env };
|
|
184
|
+
// NODE_TEST_CONTEXT is stripped: a `node --test` gate spawned while run-gates is itself running
|
|
185
|
+
// under a parent test context would otherwise inherit it, hit Node's recursive-run guard, silently
|
|
186
|
+
// skip every file, and exit 0 — a vacuous false green.
|
|
187
|
+
export const spawnGateViaBash = (cmd, cwd, extraEnv = {}) => {
|
|
188
|
+
const env = { ...process.env, ...extraEnv };
|
|
197
189
|
delete env.NODE_TEST_CONTEXT;
|
|
198
190
|
return spawnSync('bash', ['-c', cmd], { cwd, env, encoding: 'utf8', maxBuffer: MAX_GATE_OUTPUT_BYTES });
|
|
199
191
|
};
|
|
@@ -209,26 +201,17 @@ const trimTrailingNewline = (text) => text.replace(/\n$/, '');
|
|
|
209
201
|
|
|
210
202
|
// Run the selected gates sequentially (declaration order). A green gate logs one PASS line; a
|
|
211
203
|
// failing gate logs FAIL + its captured stdout/stderr VERBATIM (triage without re-running).
|
|
212
|
-
export const runGates = (gates, { cwd, spawn = spawnGateViaBash, now = Date.now, log = console.log
|
|
204
|
+
export const runGates = (gates, { cwd, spawn = spawnGateViaBash, now = Date.now, log = console.log }) => {
|
|
213
205
|
const results = [];
|
|
214
206
|
for (const gate of gates) {
|
|
215
207
|
log(`── ${gate.id} — ${gate.title}`);
|
|
216
|
-
// (a) the one-suite-run credit: for the unit-tests gate, if the fold runner already ran this exact
|
|
217
|
-
// command green at the current tree, CREDIT it instead of re-spawning (no quality loss — same
|
|
218
|
-
// execution, same tree, recorded once). Any mismatch → credit is null → the normal spawn below.
|
|
219
|
-
const credited = credit ? credit(gate) : null;
|
|
220
|
-
if (credited) {
|
|
221
|
-
log(' PASS (credited from the fold-completeness suite run — no re-spawn)');
|
|
222
|
-
results.push({ id: gate.id, title: gate.title, ok: true, code: 0, elapsedMs: 0, credited: true });
|
|
223
|
-
continue;
|
|
224
|
-
}
|
|
225
208
|
const startedAt = now();
|
|
226
209
|
const res = spawn(gate.cmd, cwd);
|
|
227
210
|
const elapsedMs = now() - startedAt;
|
|
228
211
|
const spawnError = res.error ? `spawn error: ${res.error.code || res.error.message}` : null;
|
|
229
212
|
const ok = spawnError === null && res.status === 0;
|
|
230
213
|
const code = spawnError === null ? res.status : SPAWN_FAILED_CODE;
|
|
231
|
-
results.push({ id: gate.id, title: gate.title, ok, code, elapsedMs });
|
|
214
|
+
results.push({ id: gate.id, title: gate.title, ok, code, elapsedMs, stdout: res.stdout ? String(res.stdout) : '' });
|
|
232
215
|
if (ok) {
|
|
233
216
|
log(` PASS (${formatSeconds(elapsedMs)})`);
|
|
234
217
|
} else {
|
|
@@ -264,7 +247,7 @@ export const composeSummaryLine = ({ status, results = [] }) => {
|
|
|
264
247
|
// ── CLI ───────────────────────────────────────────────────────────────────────────────
|
|
265
248
|
|
|
266
249
|
const parseArgs = (argv) => {
|
|
267
|
-
const opts = { cwd: null, only: [],
|
|
250
|
+
const opts = { cwd: null, only: [], final: false, help: false };
|
|
268
251
|
for (let i = 0; i < argv.length; i += 1) {
|
|
269
252
|
const arg = argv[i];
|
|
270
253
|
if (arg === '--help' || arg === '-h') {
|
|
@@ -277,15 +260,53 @@ const parseArgs = (argv) => {
|
|
|
277
260
|
i += 1;
|
|
278
261
|
if (argv[i] === undefined) throw fail(EXIT.usage, '--only requires a gate id argument');
|
|
279
262
|
opts.only.push(argv[i]);
|
|
280
|
-
} else if (arg === '--
|
|
281
|
-
opts.
|
|
263
|
+
} else if (arg === '--final') {
|
|
264
|
+
opts.final = true;
|
|
282
265
|
} else {
|
|
283
266
|
throw fail(EXIT.usage, `unknown argument "${arg}"\n${USAGE}`);
|
|
284
267
|
}
|
|
285
268
|
}
|
|
269
|
+
if (opts.final && opts.only.length > 0) {
|
|
270
|
+
throw fail(EXIT.usage, '--final refuses --only — a subset never attests (the D3(a) receipt binds the FULL declaration)');
|
|
271
|
+
}
|
|
286
272
|
return opts;
|
|
287
273
|
};
|
|
288
274
|
|
|
275
|
+
// The canonical core checks a --final declaration must carry (D3(a)), matched as STRICT FULL
|
|
276
|
+
// commands: `node` + ONE (quoted or bare) path token + the exact tool basename + ` --check` +
|
|
277
|
+
// END — and the path token must REALPATH-RESOLVE to the kit's OWN tool (the canonical sibling of
|
|
278
|
+
// this runner). Masked forms (`--check --help`, `--check || true`, prefix commands) never match
|
|
279
|
+
// the shape; a lookalike file that merely carries the basename — whatever it prints — never
|
|
280
|
+
// resolves to the canonical tool. Any form that DOES resolve (bare, relative, absolute, quoted)
|
|
281
|
+
// is accepted, so the anchor adds no false refusals.
|
|
282
|
+
const coreCheckRe = (basename) => new RegExp(`^node\\s+(?:"((?:[^"]*[/\\\\])?${basename})"|((?:[^\\s"]*[/\\\\])?${basename}))\\s+--check$`);
|
|
283
|
+
const FINAL_CORE_CHECKS = [
|
|
284
|
+
{ name: 'review-state', re: coreCheckRe('review-state\\.mjs'), canonical: fileURLToPath(new URL('./review-state.mjs', import.meta.url)) },
|
|
285
|
+
{ name: 'coverage-check', re: coreCheckRe('coverage-check\\.mjs'), canonical: fileURLToPath(new URL('./coverage-check.mjs', import.meta.url)) },
|
|
286
|
+
];
|
|
287
|
+
const matchesCanonicalCheck = (check, cmd, projectDir) => {
|
|
288
|
+
const m = check.re.exec(cmd.trim());
|
|
289
|
+
if (!m) return false;
|
|
290
|
+
const token = m[1] ?? m[2];
|
|
291
|
+
const abs = isAbsolute(token) ? token : join(projectDir, token);
|
|
292
|
+
try {
|
|
293
|
+
return realpathSync(abs) === realpathSync(check.canonical);
|
|
294
|
+
} catch {
|
|
295
|
+
return false; // unresolvable → never canonical (fail closed)
|
|
296
|
+
}
|
|
297
|
+
};
|
|
298
|
+
|
|
299
|
+
// isFinalCapableDeclaration(gates, projectDir) → whether --final would accept this declaration
|
|
300
|
+
// (every canonical core check present + the checker LAST) — the ONE home consumers (the
|
|
301
|
+
// recommendations guard-install probe) read instead of re-deriving the rule.
|
|
302
|
+
export const isFinalCapableDeclaration = (gates, projectDir) => {
|
|
303
|
+
if (!Array.isArray(gates) || gates.length === 0) return false;
|
|
304
|
+
const missing = FINAL_CORE_CHECKS.filter((c) => !gates.some((g) => matchesCanonicalCheck(c, g.cmd, projectDir)));
|
|
305
|
+
if (missing.length > 0) return false;
|
|
306
|
+
return matchesCanonicalCheck(FINAL_CORE_CHECKS[1], gates[gates.length - 1].cmd, projectDir);
|
|
307
|
+
};
|
|
308
|
+
const sha256Hex = (data) => createHash('sha256').update(data).digest('hex');
|
|
309
|
+
|
|
289
310
|
// The full CLI, dependency-injected for hermetic tests. Returns the process exit code; the two
|
|
290
311
|
// output sinks split human-facing report (log) from error channel (logError). The summary line is
|
|
291
312
|
// emitted via `log` as the final line of every non-usage outcome.
|
|
@@ -299,9 +320,7 @@ export const runCli = (argv, deps = {}) => {
|
|
|
299
320
|
readFile,
|
|
300
321
|
lstat,
|
|
301
322
|
now,
|
|
302
|
-
record = recordGateRun,
|
|
303
323
|
fingerprint = computeTreeFingerprint,
|
|
304
|
-
foldCredit = foldSuiteCredit,
|
|
305
324
|
} = deps;
|
|
306
325
|
try {
|
|
307
326
|
const opts = parseArgs(argv);
|
|
@@ -326,6 +345,39 @@ export const runCli = (argv, deps = {}) => {
|
|
|
326
345
|
return EXIT.empty;
|
|
327
346
|
}
|
|
328
347
|
const selected = selectGates(declaration.gates, opts.only);
|
|
348
|
+
// AW_GIT_DIR rides EVERY gate child inside a git tree (plain and --only alike): declared
|
|
349
|
+
// cmds reference fixed git-dir artifacts (the unit-tests lcov destination) — a plain red-run
|
|
350
|
+
// must exercise the SAME cmd line --final will, never a broken-only-outside---final variant.
|
|
351
|
+
const dirRes = spawnSync('git', ['rev-parse', '--absolute-git-dir'], { cwd: projectDir, windowsHide: true });
|
|
352
|
+
const gitDir = dirRes.error || dirRes.status !== 0 ? null : dirRes.stdout.toString('utf8').replace(/\r?\n$/, '');
|
|
353
|
+
let gateSpawn = gitDir === null ? spawn : (cmd, cwd2) => spawn(cmd, cwd2, { AW_GIT_DIR: gitDir });
|
|
354
|
+
// ── --final preflight (D3(a)): the declaration must carry the canonical core checks; the TRUE
|
|
355
|
+
// git dir resolves AW_GIT_DIR; the stale lcov dies BEFORE the suite so it is never consumed.
|
|
356
|
+
if (opts.final) {
|
|
357
|
+
const missing = FINAL_CORE_CHECKS.filter((c) => !declaration.gates.some((g) => matchesCanonicalCheck(c, g.cmd, projectDir)));
|
|
358
|
+
if (missing.length > 0) {
|
|
359
|
+
throw fail(EXIT.malformed, `--final refuses a weakened declaration — missing the canonical core check(s): ${missing.map((c) => c.name).join(', ')} (each must be ONE plain --check invocation of the kit's OWN tool in ${GATES_REL} — a masked form, a compound, or a lookalike path never counts)`);
|
|
360
|
+
}
|
|
361
|
+
const lastGate = declaration.gates[declaration.gates.length - 1];
|
|
362
|
+
if (!matchesCanonicalCheck(FINAL_CORE_CHECKS[1], lastGate.cmd, projectDir)) {
|
|
363
|
+
throw fail(EXIT.malformed, `--final refuses the declaration — the CANONICAL coverage-check gate must be the LAST declared gate (nothing may run after the checker consumed the lcov; "${lastGate.id}" is declared last)`);
|
|
364
|
+
}
|
|
365
|
+
if (gitDir === null) throw fail(EXIT.fail, '--final needs a git work tree (cannot resolve the git dir)');
|
|
366
|
+
try {
|
|
367
|
+
unlinkSync(join(gitDir, LCOV_BASENAME)); // a stale lcov is never consumed
|
|
368
|
+
} catch (err) {
|
|
369
|
+
if (err.code !== 'ENOENT') {
|
|
370
|
+
// Anything but "absent" leaves a readable stale artifact behind — fail closed BEFORE
|
|
371
|
+
// the attempt starts (a swallowed delete error could fake coverage).
|
|
372
|
+
logError(`[run-gates] --final could not delete the stale lcov at ${join(gitDir, LCOV_BASENAME)}: ${err.message} (fail closed)`);
|
|
373
|
+
log(composeSummaryLine({ status: 'fail' }));
|
|
374
|
+
return EXIT.finalFailed;
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
// ONE lcov path for delete/check/hash/guard: the fixed git-dir file is FORCED into every
|
|
378
|
+
// gate child env — a stray host AW_LCOV_FILE can never desync the checker from the receipt.
|
|
379
|
+
gateSpawn = (cmd, cwd2) => spawn(cmd, cwd2, { AW_GIT_DIR: gitDir, AW_LCOV_FILE: join(gitDir, LCOV_BASENAME) });
|
|
380
|
+
}
|
|
329
381
|
const probe = spawn(BASH_PROBE_CMD, projectDir);
|
|
330
382
|
if (probe.error && probe.error.code === 'ENOENT') {
|
|
331
383
|
logError(
|
|
@@ -335,51 +387,123 @@ export const runCli = (argv, deps = {}) => {
|
|
|
335
387
|
log(composeSummaryLine({ status: 'no-bash' }));
|
|
336
388
|
return EXIT.noBash;
|
|
337
389
|
}
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
390
|
+
// --final needs the pre-run fingerprint (the receipt binds before == after == current).
|
|
391
|
+
const finalFingerprintBefore = opts.final ? fingerprint(projectDir) : null;
|
|
392
|
+
const finalAttempt = opts.final ? randomUUID() : null;
|
|
393
|
+
let finalError = null;
|
|
394
|
+
let startEvidenceHashes = null;
|
|
395
|
+
if (opts.final) {
|
|
396
|
+
try {
|
|
397
|
+
appendEvidenceRecord({
|
|
398
|
+
path: resolveEvidencePath(projectDir, env),
|
|
399
|
+
record: { schema: EVIDENCE_SCHEMA_VERSION, kind: 'final-start', fingerprint: finalFingerprintBefore, attempt: finalAttempt, timestamp: new Date().toISOString() },
|
|
400
|
+
});
|
|
401
|
+
// The drift tooth's anchor: the canonical red-proof/degrade serializations BEFORE any
|
|
402
|
+
// gate runs — no legitimate writer appends those kinds DURING a final run, so any change
|
|
403
|
+
// by receipt time is an integrity failure, never a green.
|
|
404
|
+
const { records } = readEvidence(resolveEvidencePath(projectDir, env));
|
|
405
|
+
startEvidenceHashes = {
|
|
406
|
+
redProof: sha256Hex(canonicalKindSerialization(records, 'red-proof')),
|
|
407
|
+
degrade: sha256Hex(canonicalKindSerialization(records, 'degrade')),
|
|
408
|
+
};
|
|
409
|
+
} catch (err) {
|
|
410
|
+
// EVERY attempt is recorded — an unwritable store refuses the whole attempt up front
|
|
411
|
+
// (green gates without a written receipt never read as success).
|
|
412
|
+
logError(`[run-gates] --final could not record the attempt start: ${err.message}`);
|
|
413
|
+
log(composeSummaryLine({ status: 'fail' }));
|
|
414
|
+
return EXIT.finalFailed;
|
|
415
|
+
}
|
|
349
416
|
}
|
|
350
|
-
const results = runGates(selected, { cwd: projectDir, spawn, log, now
|
|
417
|
+
const results = runGates(selected, { cwd: projectDir, spawn: gateSpawn, log, now });
|
|
351
418
|
for (const line of formatTable(results)) log(line);
|
|
352
419
|
const allGreen = results.every((result) => result.ok);
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
420
|
+
if (opts.final) {
|
|
421
|
+
// The checker's verbatim diagnostics surface even on green — skipped-no-lcov and the
|
|
422
|
+
// out-of-domain/unsupported lists must never vanish into a suppressed green stdout.
|
|
423
|
+
const checkerRow = results[results.length - 1];
|
|
424
|
+
if (checkerRow?.ok && checkerRow.stdout) {
|
|
425
|
+
log('── coverage-check diagnostics (verbatim)');
|
|
426
|
+
log(trimTrailingNewline(checkerRow.stdout));
|
|
427
|
+
}
|
|
428
|
+
// The completed attempt (D3(a)): status green/red DERIVED from results + integrity, the
|
|
429
|
+
// FULL declaration, the per-gate results, the evidence hashes over the CANONICAL
|
|
430
|
+
// authoritative serializations, and the lcov sha the CHECKER printed for the bytes it
|
|
431
|
+
// actually read — with an end re-hash agreement check (the checker's own children are the
|
|
432
|
+
// one write window that survives "coverage-check runs last").
|
|
359
433
|
try {
|
|
360
|
-
const
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
434
|
+
const storePath = resolveEvidencePath(projectDir, env);
|
|
435
|
+
const endRead = readEvidence(storePath);
|
|
436
|
+
const endBroken = Boolean(endRead.readError) || (endRead.malformed ?? 0) > 0;
|
|
437
|
+
let integrityFailure = null;
|
|
438
|
+
if (endBroken) {
|
|
439
|
+
integrityFailure = 'the evidence store became unreadable under the final run';
|
|
440
|
+
} else {
|
|
441
|
+
const endHashes = {
|
|
442
|
+
redProof: sha256Hex(canonicalKindSerialization(endRead.records, 'red-proof')),
|
|
443
|
+
degrade: sha256Hex(canonicalKindSerialization(endRead.records, 'degrade')),
|
|
444
|
+
};
|
|
445
|
+
if (endHashes.redProof !== startEvidenceHashes.redProof || endHashes.degrade !== startEvidenceHashes.degrade) {
|
|
446
|
+
integrityFailure = 'the evidence store moved under the final run (the canonical red-proof/degrade serialization changed)';
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
// Exactly ONE full machine line binds the receipt — an unanchored first-match would let
|
|
450
|
+
// an injected/duplicated line shadow the real one and skip the end re-hash.
|
|
451
|
+
const shaLineRe = /^coverage-check: lcov-sha256=([0-9a-f]{64}|none)$/;
|
|
452
|
+
const shaLines = String(checkerRow?.stdout ?? '').split(/\r?\n/).filter((l) => shaLineRe.test(l));
|
|
453
|
+
const shaValue = shaLines.length === 1 ? shaLineRe.exec(shaLines[0])[1] : null;
|
|
454
|
+
const lcovSha256 = shaValue !== null && shaValue !== 'none' ? shaValue : null;
|
|
455
|
+
if (allGreen && integrityFailure === null) {
|
|
456
|
+
if (shaLines.length !== 1) {
|
|
457
|
+
integrityFailure = shaLines.length === 0
|
|
458
|
+
? 'the coverage-check gate printed no lcov-sha256 line — the consumed lcov is unknowable (fail closed)'
|
|
459
|
+
: `the coverage-check gate printed ${shaLines.length} lcov-sha256 lines — exactly ONE full machine line binds the receipt`;
|
|
460
|
+
} else if (lcovSha256 !== null) {
|
|
461
|
+
let endSha = null;
|
|
462
|
+
try {
|
|
463
|
+
const st = lstatSync(join(gitDir, LCOV_BASENAME));
|
|
464
|
+
if (st.isFile()) endSha = sha256Hex(readFileSync(join(gitDir, LCOV_BASENAME)));
|
|
465
|
+
} catch { /* vanished → disagreement below */ }
|
|
466
|
+
if (endSha !== lcovSha256) integrityFailure = 'the lcov moved under the checker (the end re-hash differs from the checker-read bytes)';
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
const status = allGreen && integrityFailure === null ? 'green' : 'red';
|
|
470
|
+
appendEvidenceRecord({
|
|
471
|
+
path: storePath,
|
|
472
|
+
record: {
|
|
473
|
+
schema: EVIDENCE_SCHEMA_VERSION,
|
|
474
|
+
kind: 'final',
|
|
475
|
+
status,
|
|
476
|
+
attempt: finalAttempt,
|
|
477
|
+
fingerprintBefore: finalFingerprintBefore,
|
|
478
|
+
fingerprintAfter: fingerprint(projectDir),
|
|
479
|
+
declared: declaration.gates.map(({ id, cmd }) => ({ id, cmd })),
|
|
480
|
+
results: results.map(({ id, ok, code }) => ({ id, ok, code })),
|
|
481
|
+
evidenceHashes: endBroken
|
|
482
|
+
? startEvidenceHashes
|
|
483
|
+
: {
|
|
484
|
+
redProof: sha256Hex(canonicalKindSerialization(endRead.records, 'red-proof')),
|
|
485
|
+
degrade: sha256Hex(canonicalKindSerialization(endRead.records, 'degrade')),
|
|
486
|
+
},
|
|
487
|
+
lcovSha256,
|
|
488
|
+
integrityFailure,
|
|
489
|
+
timestamp: new Date().toISOString(),
|
|
371
490
|
},
|
|
372
|
-
fingerprintBefore,
|
|
373
|
-
fingerprintAfter: fingerprint(projectDir),
|
|
374
491
|
});
|
|
375
|
-
|
|
492
|
+
if (integrityFailure) {
|
|
493
|
+
finalError = new Error(integrityFailure);
|
|
494
|
+
logError(`[run-gates] --final integrity failure: ${integrityFailure} — the receipt is RED`);
|
|
495
|
+
}
|
|
496
|
+
log(`[run-gates] final receipt recorded (${status}) → ${storePath}`);
|
|
497
|
+
if (status === 'green' && lcovSha256 === null) {
|
|
498
|
+
logError(`[run-gates] --final consumed NO lcov — the coverage arm ran skipped-no-lcov; the receipt records lcovSha256:null (the declared unit-tests gate must produce <git-dir>/${LCOV_BASENAME})`);
|
|
499
|
+
}
|
|
376
500
|
} catch (err) {
|
|
377
|
-
|
|
378
|
-
logError(`[run-gates] --
|
|
501
|
+
finalError = err;
|
|
502
|
+
logError(`[run-gates] --final could not write its receipt: ${err.message}`);
|
|
379
503
|
}
|
|
380
504
|
}
|
|
381
505
|
log(composeSummaryLine({ status: allGreen ? 'ok' : 'fail', results }));
|
|
382
|
-
if (
|
|
506
|
+
if (finalError) return EXIT.finalFailed;
|
|
383
507
|
return allGreen ? EXIT.ok : EXIT.fail;
|
|
384
508
|
} catch (err) {
|
|
385
509
|
logError(`[run-gates] ${err.message}`);
|
package/tools/sandbox-masks.mjs
CHANGED
|
@@ -31,7 +31,7 @@
|
|
|
31
31
|
// from the hidden-mode reconcile's block (hide-footprint.mjs — that lane noops on visible
|
|
32
32
|
// deployments; this one serves them too). A malformed fence (start without end / duplicated
|
|
33
33
|
// markers) fails CLOSED: loud report, file untouched. Patterns are root-anchored (`/rel`),
|
|
34
|
-
// glob-metacharacters escaped, NUL-safe walk. Dependency-free, Node >=
|
|
34
|
+
// glob-metacharacters escaped, NUL-safe walk. Dependency-free, Node >= 22; no side effects on
|
|
35
35
|
// import (the isDirectRun idiom).
|
|
36
36
|
|
|
37
37
|
import { lstatSync, readFileSync, writeFileSync, mkdirSync } from 'node:fs';
|
|
@@ -69,7 +69,7 @@ const listUntrackedUnfilteredZ = (root) => {
|
|
|
69
69
|
// name can never shadow a deeper real path), glob metacharacters + leading-! / leading-# handled
|
|
70
70
|
// by the anchor, trailing whitespace escaped (gitignore trims it otherwise).
|
|
71
71
|
// Trailing SPACES are escapable in gitignore (`\ `); a trailing TAB is not expressible — such a
|
|
72
|
-
// name is classified unrenderable by the derivation
|
|
72
|
+
// name is classified unrenderable by the derivation, so it never reaches this renderer.
|
|
73
73
|
export const toExcludePattern = (rel) => {
|
|
74
74
|
const escaped = rel.replace(/([\\*?[\]])/g, '\\$1').replace(/ +$/, (m) => '\\ '.repeat(m.length));
|
|
75
75
|
return `/${escaped}`;
|
|
@@ -79,7 +79,7 @@ export const toExcludePattern = (rel) => {
|
|
|
79
79
|
// lstat class is never-committable (char/block/FIFO/socket). Everything else is refused by
|
|
80
80
|
// construction — tracked paths never appear in an --others walk; regular/dir/symlink/missing fail
|
|
81
81
|
// the predicate. A CR/LF-carrying mask NAME cannot be expressed as ONE gitignore rule — rendering
|
|
82
|
-
// it would split into several rules and could hide unrelated committable paths
|
|
82
|
+
// it would split into several rules and could hide unrelated committable paths — so it
|
|
83
83
|
// is a LOUD unrenderable skip, never written (the review domain already ignores the mask itself).
|
|
84
84
|
export const deriveMasks = ({ root, lstat = lstatSync, listUntracked = listUntrackedUnfilteredZ } = {}) => {
|
|
85
85
|
const entries = listUntracked(root);
|
|
@@ -95,7 +95,7 @@ export const deriveMasks = ({ root, lstat = lstatSync, listUntracked = listUntra
|
|
|
95
95
|
}
|
|
96
96
|
if (!isNeverCommittableStat(stat)) continue;
|
|
97
97
|
// CR/LF anywhere, or a trailing TAB (inexpressible in gitignore — only trailing SPACES escape
|
|
98
|
-
// as `\
|
|
98
|
+
// as `\ `), make the name unrenderable as ONE exclude rule.
|
|
99
99
|
if (/[\r\n]/.test(rel) || /\t$/.test(rel)) unrenderable.push(rel);
|
|
100
100
|
else masks.push(rel);
|
|
101
101
|
}
|
|
@@ -115,11 +115,11 @@ export const findMasksFence = (lines) => {
|
|
|
115
115
|
|
|
116
116
|
// A fenced pattern back to its relative path (the derivation writes only `/rel` root-anchored
|
|
117
117
|
// escaped forms; unescape is its exact inverse — the escaped-space form `\ ` included, so a
|
|
118
|
-
// trailing-space mask round-trips
|
|
118
|
+
// trailing-space mask round-trips). Non-pattern lines (blank/comment) are skipped;
|
|
119
119
|
// only UNESCAPED trailing whitespace is insignificant (gitignore semantics).
|
|
120
120
|
const patternToRel = (line) => {
|
|
121
121
|
// A trailing CR (a CRLF-saved exclude file) strips FIRST — it would otherwise ride into the rel
|
|
122
|
-
// name and false-ENOENT the revalidation lstat
|
|
122
|
+
// name and false-ENOENT the revalidation lstat; then only UNESCAPED trailing
|
|
123
123
|
// whitespace is insignificant (gitignore semantics — an escaped `\ ` survives).
|
|
124
124
|
const t = line.replace(/\r$/, '').replace(/^[ \t]+/, '').replace(/(?<!\\)[ \t]+$/, '');
|
|
125
125
|
if (t === '' || t.startsWith('#') || !t.startsWith('/')) return null;
|
|
@@ -156,13 +156,13 @@ export const probeSandboxMasks = ({ cwd = process.cwd(), lstat = lstatSync, list
|
|
|
156
156
|
const commonDirRaw = gitLine(['rev-parse', '--git-common-dir'], cwd);
|
|
157
157
|
if (gitPathRaw == null || commonDirRaw == null) return null;
|
|
158
158
|
const excludeFile = resolve(cwd, gitPathRaw);
|
|
159
|
-
// The write-containment root
|
|
159
|
+
// The write-containment root: info/exclude lives in the COMMON git dir (worktree-safe),
|
|
160
160
|
// and the apply must never write through a symlinked component or a non-regular leaf.
|
|
161
161
|
const gitCommonDir = resolve(cwd, commonDirRaw);
|
|
162
162
|
const derived = deriveMasks({ root, lstat, listUntracked });
|
|
163
163
|
if (derived == null) return null;
|
|
164
164
|
const { masks, unrenderable } = derived;
|
|
165
|
-
// The WHOLE chain is guarded BEFORE any read
|
|
165
|
+
// The WHOLE chain is guarded BEFORE any read: a symlinked exclude leaf OR a
|
|
166
166
|
// symlinked parent (.git/info) must never be read through, and a FIFO/socket leaf would HANG
|
|
167
167
|
// the read-only probe — parent-chain containment + leaf lstat first, fail closed on everything
|
|
168
168
|
// but a plain absent file, then read the existing REGULAR content. The apply re-checks
|
|
@@ -190,7 +190,7 @@ export const probeSandboxMasks = ({ cwd = process.cwd(), lstat = lstatSync, list
|
|
|
190
190
|
content = readFile(excludeFile, 'utf8');
|
|
191
191
|
} catch (err) {
|
|
192
192
|
// The file existed a moment ago — ANY read failure now fails CLOSED (an apply over a misread
|
|
193
|
-
// "empty" file would overwrite hand content outside the managed block
|
|
193
|
+
// "empty" file would overwrite hand content outside the managed block).
|
|
194
194
|
throw fail(1, `cannot read ${excludeFile} (${err?.code ?? err?.message ?? err}) — refusing to treat an unreadable exclude file as empty`);
|
|
195
195
|
}
|
|
196
196
|
}
|
|
@@ -222,7 +222,7 @@ export const planApply = (probe, { clear = false } = {}) => {
|
|
|
222
222
|
}
|
|
223
223
|
// --clear takes PRECEDENCE over the derivation (codex, Segment-A receipt): it means "remove the
|
|
224
224
|
// managed block", even while masks are visible — re-writing the block instead of the requested
|
|
225
|
-
// clear would be a silent divergence. An EMPTY fence (markers, no entries) clears too
|
|
225
|
+
// clear would be a silent divergence. An EMPTY fence (markers, no entries) clears too;
|
|
226
226
|
// no fence at all is a stated no-op.
|
|
227
227
|
if (clear && probe.fence.state !== 'ok') {
|
|
228
228
|
return { action: 'noop', reason: 'no managed block present — nothing to clear' };
|
|
@@ -236,7 +236,7 @@ export const planApply = (probe, { clear = false } = {}) => {
|
|
|
236
236
|
}
|
|
237
237
|
const masks = clear ? [] : probe.masks;
|
|
238
238
|
const block = masks.length === 0 ? [] : [MASKS_FENCE_START, ...masks.map(toExcludePattern), MASKS_FENCE_END];
|
|
239
|
-
// EVERY hand byte outside the managed fence is preserved EXACTLY
|
|
239
|
+
// EVERY hand byte outside the managed fence is preserved EXACTLY: the first-apply
|
|
240
240
|
// append adds at most ONE separator newline, and the existing-block replace splices via RAW
|
|
241
241
|
// string offsets around the fence lines — a split/join('\n') rebuild would silently normalize
|
|
242
242
|
// CRLF hand content outside the block. Only the fenced block itself is ours (always LF).
|
|
@@ -325,18 +325,18 @@ export const main = (argv, ctx = {}) => {
|
|
|
325
325
|
return { code: 0, stdout: formatProbe(probe), stderr: '' };
|
|
326
326
|
}
|
|
327
327
|
const plan = planApply(probe, { clear: argv.includes('--clear') });
|
|
328
|
-
// The unrenderable skips stay LOUD on EVERY apply path
|
|
328
|
+
// The unrenderable skips stay LOUD on EVERY apply path — the same warning the
|
|
329
329
|
// probe prints; the renderable subset still applies (a refusal would block the useful masks).
|
|
330
330
|
const skipWarnings = probe.unrenderable
|
|
331
331
|
.map((rel) => `sandbox-masks: ⚠ mask name carries a newline or ends with a tab and cannot be expressed as ONE exclude rule — NOT written (the review domain already ignores the mask itself): ${JSON.stringify(rel)}`)
|
|
332
332
|
.join('\n');
|
|
333
|
-
// A refusal carries the unrenderable warnings too
|
|
333
|
+
// A refusal carries the unrenderable warnings too — they are never discarded.
|
|
334
334
|
if (plan.action === 'refuse') throw fail(1, skipWarnings ? `${plan.reason}\n${skipWarnings}` : plan.reason);
|
|
335
335
|
if (plan.action === 'noop') return { code: 0, stdout: `sandbox-masks: ${plan.reason}`, stderr: skipWarnings };
|
|
336
336
|
const writeFile = ctx.deps?.writeFile ?? writeFileSync;
|
|
337
337
|
const mkdir = ctx.deps?.mkdir ?? mkdirSync;
|
|
338
338
|
const lstat = ctx.deps?.lstat ?? lstatSync;
|
|
339
|
-
// The write never follows a symlink or clobbers a non-regular leaf
|
|
339
|
+
// The write never follows a symlink or clobbers a non-regular leaf: every component
|
|
340
340
|
// from the common git dir down is guarded, and an existing exclude leaf must be a regular file.
|
|
341
341
|
try {
|
|
342
342
|
assertContainedRealPath(probe.gitCommonDir, probe.excludeFile, { lstat });
|
package/tools/semver-lite.mjs
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
// freshness probe that cannot parse a version must degrade to "unknown" — never to a false ordering
|
|
8
8
|
// claim in either direction (INV-B). No `let`: a small functional comparison (AGENTS.md §2.3).
|
|
9
9
|
//
|
|
10
|
-
// Pure, no imports, no side effects, Node >=
|
|
10
|
+
// Pure, no imports, no side effects, Node >= 22.
|
|
11
11
|
|
|
12
12
|
export const parseSemver = (str) => {
|
|
13
13
|
const m = typeof str === 'string' ? str.trim().match(/^(\d+)\.(\d+)\.(\d+)/) : null;
|
package/tools/set-autonomy.mjs
CHANGED
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
// (malformed/unreadable policy) or a write STOP (no deployment / symlinked leaf). main(argv, ctx) →
|
|
20
20
|
// { code, stdout, stderr }; cwd / fs are injectable for host-independent tests.
|
|
21
21
|
//
|
|
22
|
-
// Dependency-free, Node >=
|
|
22
|
+
// Dependency-free, Node >= 22. No side effects on import (the isDirectRun idiom).
|
|
23
23
|
|
|
24
24
|
import { readFileSync, lstatSync } from 'node:fs';
|
|
25
25
|
import { pathToFileURL } from 'node:url';
|
package/tools/set-recipe.mjs
CHANGED
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
// or a write STOP (no deployment / symlinked leaf). main(argv, ctx) → { code, stdout, stderr }; cwd /
|
|
20
20
|
// env / home / detect / fs are injectable for host-independent tests.
|
|
21
21
|
//
|
|
22
|
-
// Dependency-free, Node >=
|
|
22
|
+
// Dependency-free, Node >= 22. No side effects on import (the isDirectRun idiom).
|
|
23
23
|
|
|
24
24
|
import { readFileSync, lstatSync } from 'node:fs';
|
|
25
25
|
import { homedir } from 'node:os';
|
|
@@ -210,7 +210,7 @@ export const main = (argv, ctx = {}) => {
|
|
|
210
210
|
const { writtenPath } = writeConfig(cwd, after, ctx);
|
|
211
211
|
const fileBody = serializeConfig(after);
|
|
212
212
|
// The echoed handover line carries the SAME autonomy levels recipes --active-line renders —
|
|
213
|
-
// sync facts (no render-check needed for the cells;
|
|
213
|
+
// sync facts (no render-check needed for the cells; Segment B). A malformed policy
|
|
214
214
|
// surfaces loudly through the line's own MALFORMED segment.
|
|
215
215
|
const autonomyFacts = (() => {
|
|
216
216
|
try {
|
package/tools/setup-backends.mjs
CHANGED
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
// wrappers are POSIX .sh — report unsupported / use WSL and mutate nothing. Every fs primitive is
|
|
22
22
|
// injectable (deps.*) so the whole module is unit-testable without touching the real filesystem.
|
|
23
23
|
//
|
|
24
|
-
// Dependency-free, Node >=
|
|
24
|
+
// Dependency-free, Node >= 22.
|
|
25
25
|
|
|
26
26
|
import {
|
|
27
27
|
existsSync, lstatSync, statSync, readdirSync, readlinkSync, symlinkSync, mkdirSync, copyFileSync,
|
package/tools/surface.mjs
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
// `/agent-workflow-kit status` surface always consumes `--json` and localizes itself.
|
|
7
7
|
//
|
|
8
8
|
// Pure + fully injectable (argv / env / isTTY / columns / platform are inputs), no side effects on
|
|
9
|
-
// import, Node >=
|
|
9
|
+
// import, Node >= 22. Tested in isolation against the §4.5 table.
|
|
10
10
|
|
|
11
11
|
export const FORMATS = Object.freeze(['auto', 'plain', 'ansi', 'json']);
|
|
12
12
|
export const FORMAT_ENV = 'AGENT_WORKFLOW_FORMAT';
|
package/tools/uninstall.mjs
CHANGED
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
// untouched and reported, never removed.
|
|
20
20
|
//
|
|
21
21
|
// Pure planner (buildPlan) + guarded executor (executePlan), both dependency-injectable so the whole
|
|
22
|
-
// module is unit-testable without the real filesystem. Dependency-free, Node >=
|
|
22
|
+
// module is unit-testable without the real filesystem. Dependency-free, Node >= 22. No side effects on
|
|
23
23
|
// import (the isDirectRun idiom).
|
|
24
24
|
|
|
25
25
|
import { existsSync, statSync, lstatSync, readlinkSync, readFileSync, readdirSync, realpathSync } from 'node:fs';
|