@sabaiway/agent-workflow-kit 5.10.0 → 5.11.1
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 +152 -0
- package/README.md +2 -2
- package/SKILL.md +1 -1
- package/capability.json +1 -1
- package/package.json +1 -1
- package/references/hooks/gate-approve.mjs +13 -2
- package/references/hooks/state-block-guard.mjs +14 -2
- package/references/modes/commit-guard.md +11 -8
- package/references/modes/core-evidence.md +1 -1
- package/references/modes/dispatch.md +32 -10
- package/references/modes/worktrees.md +47 -3
- package/references/scripts/archive-changelog.mjs +14 -3
- package/references/scripts/archive-decisions.mjs +14 -3
- package/references/scripts/archive-issues.mjs +14 -3
- package/references/scripts/check-docs-size.mjs +14 -3
- package/references/scripts/migrate-gates.mjs +13 -2
- package/tools/ack-write.mjs +3 -3
- package/tools/advisor-matrix.mjs +165 -0
- package/tools/autonomy-doctor.mjs +2 -3
- package/tools/bridge-settings.mjs +2 -3
- package/tools/cheap-agents.mjs +3 -3
- package/tools/commands.mjs +4 -5
- package/tools/commit-guard.mjs +77 -20
- package/tools/core-evidence.mjs +12 -3
- package/tools/coverage-check.mjs +2 -3
- package/tools/delegation.mjs +2 -3
- package/tools/detect-backends.mjs +2 -3
- package/tools/dispatch-advisor.mjs +323 -0
- package/tools/dispatch.mjs +174 -109
- package/tools/doc-parity.mjs +69 -16
- package/tools/family-registry.mjs +3 -3
- package/tools/flow-adoption-mint.mjs +70 -0
- package/tools/flow-append.mjs +309 -0
- package/tools/flow-chain-state.mjs +91 -0
- package/tools/flow-check-cores.mjs +35 -6
- package/tools/flow-check-rungs.mjs +20 -2
- package/tools/flow-check.mjs +22 -8
- package/tools/flow-delta-proof.mjs +307 -0
- package/tools/flow-record.mjs +1 -1
- package/tools/flow-store-read.mjs +3 -3
- package/tools/flow-store.mjs +35 -812
- package/tools/flow-subset-budget.mjs +81 -0
- package/tools/flow-writer.mjs +3 -3
- package/tools/gate-hook.mjs +3 -3
- package/tools/gates-init.mjs +3 -3
- package/tools/grounding.mjs +2 -3
- package/tools/hide-footprint.mjs +2 -3
- package/tools/inject-methodology.mjs +2 -3
- package/tools/lens-region.mjs +2 -3
- package/tools/manifest/validate.mjs +2 -3
- package/tools/migrate-adr-store.mjs +3 -3
- package/tools/observation-builder.mjs +123 -0
- package/tools/path-inventory.mjs +2 -3
- package/tools/procedures.mjs +3 -3
- package/tools/receipt-deadline.mjs +2 -3
- package/tools/recipes.mjs +2 -3
- package/tools/recommendations.mjs +3 -3
- package/tools/release-scan.mjs +2 -3
- package/tools/repo-search.mjs +2 -3
- package/tools/review-state.mjs +3 -3
- package/tools/run-gates.mjs +2 -3
- package/tools/sandbox-masks.mjs +3 -3
- package/tools/satellite-locator.mjs +179 -0
- package/tools/set-autonomy.mjs +2 -3
- package/tools/set-flow.mjs +3 -3
- package/tools/set-recipe.mjs +2 -3
- package/tools/setup-backends.mjs +3 -3
- package/tools/store-append.mjs +2 -2
- package/tools/uninstall.mjs +2 -3
- package/tools/velocity-profile.mjs +3 -3
- package/tools/worktree-handoff-return.mjs +369 -0
- package/tools/worktree-prompt.mjs +190 -0
- package/tools/worktrees-record.mjs +171 -0
- package/tools/worktrees.mjs +311 -300
|
@@ -32,9 +32,9 @@
|
|
|
32
32
|
// --warm-days=N (default 30)
|
|
33
33
|
// --today=YYYY-MM-DD (default today UTC) — useful for tests / reproducible runs
|
|
34
34
|
|
|
35
|
-
import { readFileSync, writeFileSync, mkdirSync, readdirSync, existsSync } from 'node:fs';
|
|
35
|
+
import { readFileSync, writeFileSync, mkdirSync, readdirSync, existsSync, realpathSync } from 'node:fs';
|
|
36
36
|
import { dirname, resolve, basename } from 'node:path';
|
|
37
|
-
import { fileURLToPath
|
|
37
|
+
import { fileURLToPath } from 'node:url';
|
|
38
38
|
import { tokenizeMarkdown, findParagraphBreak, fail } from './markdown-blocks.mjs';
|
|
39
39
|
|
|
40
40
|
const __filename = fileURLToPath(import.meta.url);
|
|
@@ -542,5 +542,16 @@ export const runCli = (argv, deps = {}) => {
|
|
|
542
542
|
}
|
|
543
543
|
};
|
|
544
544
|
|
|
545
|
-
|
|
545
|
+
// Run main() only when executed directly, never on import. Compare by REAL path: an entry point
|
|
546
|
+
// reached through a symlink resolves to its target, so a raw string compare reads the two as
|
|
547
|
+
// different and the CLI never runs. realpathSync collapses the link so both sides match.
|
|
548
|
+
const isDirectRun = (() => {
|
|
549
|
+
const invoked = process.argv[1];
|
|
550
|
+
if (!invoked) return false;
|
|
551
|
+
try {
|
|
552
|
+
return realpathSync(invoked) === realpathSync(fileURLToPath(import.meta.url));
|
|
553
|
+
} catch {
|
|
554
|
+
return false;
|
|
555
|
+
}
|
|
556
|
+
})();
|
|
546
557
|
if (isDirectRun) process.exitCode = runCli(process.argv.slice(2));
|
|
@@ -75,9 +75,9 @@
|
|
|
75
75
|
//
|
|
76
76
|
// Dependency-free, Node >= 22. Deployed into a consumer's scripts/ like its siblings.
|
|
77
77
|
|
|
78
|
-
import { readFileSync, writeFileSync, existsSync, mkdirSync, readdirSync, rmSync, statSync } from 'node:fs';
|
|
78
|
+
import { readFileSync, writeFileSync, existsSync, mkdirSync, readdirSync, rmSync, statSync, realpathSync } from 'node:fs';
|
|
79
79
|
import { dirname, resolve, join, posix } from 'node:path';
|
|
80
|
-
import { fileURLToPath
|
|
80
|
+
import { fileURLToPath } from 'node:url';
|
|
81
81
|
import { spawnSync } from 'node:child_process';
|
|
82
82
|
import { createHash } from 'node:crypto';
|
|
83
83
|
import { tmpdir } from 'node:os';
|
|
@@ -1195,5 +1195,16 @@ export const runCli = (argv, deps = {}) => {
|
|
|
1195
1195
|
}
|
|
1196
1196
|
};
|
|
1197
1197
|
|
|
1198
|
-
|
|
1198
|
+
// Run main() only when executed directly, never on import. Compare by REAL path: an entry point
|
|
1199
|
+
// reached through a symlink resolves to its target, so a raw string compare reads the two as
|
|
1200
|
+
// different and the CLI never runs. realpathSync collapses the link so both sides match.
|
|
1201
|
+
const isDirectRun = (() => {
|
|
1202
|
+
const invoked = process.argv[1];
|
|
1203
|
+
if (!invoked) return false;
|
|
1204
|
+
try {
|
|
1205
|
+
return realpathSync(invoked) === realpathSync(fileURLToPath(import.meta.url));
|
|
1206
|
+
} catch {
|
|
1207
|
+
return false;
|
|
1208
|
+
}
|
|
1209
|
+
})();
|
|
1199
1210
|
if (isDirectRun) process.exitCode = runCli(process.argv.slice(2));
|
|
@@ -31,9 +31,9 @@
|
|
|
31
31
|
// --cutoff-days=N (default 14)
|
|
32
32
|
// --today=YYYY-MM-DD (default UTC today)
|
|
33
33
|
|
|
34
|
-
import { readFileSync, writeFileSync, mkdirSync, existsSync } from 'node:fs';
|
|
34
|
+
import { readFileSync, writeFileSync, mkdirSync, existsSync, realpathSync } from 'node:fs';
|
|
35
35
|
import { dirname, resolve, basename } from 'node:path';
|
|
36
|
-
import { fileURLToPath
|
|
36
|
+
import { fileURLToPath } from 'node:url';
|
|
37
37
|
import { tokenizeMarkdown, fail } from './markdown-blocks.mjs';
|
|
38
38
|
|
|
39
39
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
@@ -411,5 +411,16 @@ export const runCli = (argv, deps = {}) => {
|
|
|
411
411
|
}
|
|
412
412
|
};
|
|
413
413
|
|
|
414
|
-
|
|
414
|
+
// Run main() only when executed directly, never on import. Compare by REAL path: an entry point
|
|
415
|
+
// reached through a symlink resolves to its target, so a raw string compare reads the two as
|
|
416
|
+
// different and the CLI never runs. realpathSync collapses the link so both sides match.
|
|
417
|
+
const isDirectRun = (() => {
|
|
418
|
+
const invoked = process.argv[1];
|
|
419
|
+
if (!invoked) return false;
|
|
420
|
+
try {
|
|
421
|
+
return realpathSync(invoked) === realpathSync(fileURLToPath(import.meta.url));
|
|
422
|
+
} catch {
|
|
423
|
+
return false;
|
|
424
|
+
}
|
|
425
|
+
})();
|
|
415
426
|
if (isDirectRun) process.exitCode = runCli(process.argv.slice(2));
|
|
@@ -25,9 +25,9 @@
|
|
|
25
25
|
// --quiet print only failures (and final summary)
|
|
26
26
|
|
|
27
27
|
import { readFile, writeFile, readdir, stat, rename, rm } from 'node:fs/promises';
|
|
28
|
-
import { existsSync, lstatSync } from 'node:fs';
|
|
28
|
+
import { existsSync, lstatSync, realpathSync } from 'node:fs';
|
|
29
29
|
import { dirname, resolve, relative, join, basename, sep } from 'node:path';
|
|
30
|
-
import { fileURLToPath
|
|
30
|
+
import { fileURLToPath } from 'node:url';
|
|
31
31
|
import { randomBytes } from 'node:crypto';
|
|
32
32
|
|
|
33
33
|
const __filename = fileURLToPath(import.meta.url);
|
|
@@ -571,7 +571,18 @@ export const runCli = async (argv, deps = {}) => {
|
|
|
571
571
|
return result(errorCount > 0 && !flags.report ? 1 : 0);
|
|
572
572
|
};
|
|
573
573
|
|
|
574
|
-
|
|
574
|
+
// Run main() only when executed directly, never on import. Compare by REAL path: an entry point
|
|
575
|
+
// reached through a symlink resolves to its target, so a raw string compare reads the two as
|
|
576
|
+
// different and the CLI never runs. realpathSync collapses the link so both sides match.
|
|
577
|
+
const isDirectRun = (() => {
|
|
578
|
+
const invoked = process.argv[1];
|
|
579
|
+
if (!invoked) return false;
|
|
580
|
+
try {
|
|
581
|
+
return realpathSync(invoked) === realpathSync(fileURLToPath(import.meta.url));
|
|
582
|
+
} catch {
|
|
583
|
+
return false;
|
|
584
|
+
}
|
|
585
|
+
})();
|
|
575
586
|
if (isDirectRun) {
|
|
576
587
|
const { code, stdout, stderr } = await runCli(process.argv.slice(2));
|
|
577
588
|
if (stdout) process.stdout.write(stdout);
|
|
@@ -28,7 +28,7 @@
|
|
|
28
28
|
|
|
29
29
|
import { existsSync, lstatSync, readFileSync, writeFileSync, renameSync, unlinkSync, realpathSync } from 'node:fs';
|
|
30
30
|
import { join, resolve, isAbsolute } from 'node:path';
|
|
31
|
-
import {
|
|
31
|
+
import { fileURLToPath } from 'node:url';
|
|
32
32
|
import { randomBytes } from 'node:crypto';
|
|
33
33
|
import { spawnSync } from 'node:child_process';
|
|
34
34
|
|
|
@@ -718,5 +718,16 @@ export const main = (argv = process.argv.slice(2), io = {}) => {
|
|
|
718
718
|
}
|
|
719
719
|
};
|
|
720
720
|
|
|
721
|
-
|
|
721
|
+
// Run main() only when executed directly, never on import. Compare by REAL path: an entry point
|
|
722
|
+
// reached through a symlink resolves to its target, so a raw string compare reads the two as
|
|
723
|
+
// different and the CLI never runs. realpathSync collapses the link so both sides match.
|
|
724
|
+
const isDirectRun = (() => {
|
|
725
|
+
const invoked = process.argv[1];
|
|
726
|
+
if (!invoked) return false;
|
|
727
|
+
try {
|
|
728
|
+
return realpathSync(invoked) === realpathSync(fileURLToPath(import.meta.url));
|
|
729
|
+
} catch {
|
|
730
|
+
return false;
|
|
731
|
+
}
|
|
732
|
+
})();
|
|
722
733
|
if (isDirectRun) process.exitCode = main();
|
package/tools/ack-write.mjs
CHANGED
|
@@ -27,7 +27,8 @@
|
|
|
27
27
|
|
|
28
28
|
import { lstatSync, readFileSync } from 'node:fs';
|
|
29
29
|
import { dirname, join, resolve } from 'node:path';
|
|
30
|
-
import { fileURLToPath
|
|
30
|
+
import { fileURLToPath } from 'node:url';
|
|
31
|
+
import { isDirectRun } from './direct-run.mjs';
|
|
31
32
|
import { ACKS_FILE, ACK_LANES } from './recommendations.mjs';
|
|
32
33
|
import { assertDocsAiDeployment, writeDocsAiFileAtomic, lstatNoFollow } from './atomic-write.mjs';
|
|
33
34
|
import { shellQuoteArg } from './review-state.mjs';
|
|
@@ -207,5 +208,4 @@ export const main = (argv = process.argv.slice(2), deps = {}) => {
|
|
|
207
208
|
}
|
|
208
209
|
};
|
|
209
210
|
|
|
210
|
-
|
|
211
|
-
if (isDirectRun) process.exit(main(process.argv.slice(2)));
|
|
211
|
+
if (isDirectRun(import.meta.url)) process.exit(main(process.argv.slice(2)));
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
// advisor-matrix.mjs — the advisor-matrix STRUCTURE check (delegation Plan 3, Phase 1), as a leaf.
|
|
2
|
+
//
|
|
3
|
+
// A doc-parity BINDING proves a token is somewhere in a file. Correspondence is a different claim,
|
|
4
|
+
// and it is the one this table needs: the dispatch mode doc's routing matrix must carry one row per
|
|
5
|
+
// registry row, in registry order, with every CELL equal. A reorder, a duplicate, a dropped row, a
|
|
6
|
+
// mis-bound vehicle and a drifted availability or returns cell all leave every token present — so a
|
|
7
|
+
// token check passes every one of them, which is exactly why this exists beside the bindings rather
|
|
8
|
+
// than as more of them.
|
|
9
|
+
//
|
|
10
|
+
// Its own module rather than more lines in doc-parity.mjs: the lint's identity is "a closed registry
|
|
11
|
+
// of value bindings plus the runner over them", and a table parser with its own refusal vocabulary is
|
|
12
|
+
// a second thing. Split, each is a file you can hold whole — and the parser gets its own test file.
|
|
13
|
+
//
|
|
14
|
+
// Read-only: never writes, never commits, spawns nothing. Node built-ins plus the advisor registry
|
|
15
|
+
// only. No side effects on import; no CLI (it is reached through doc-parity).
|
|
16
|
+
|
|
17
|
+
import { readFileSync } from 'node:fs';
|
|
18
|
+
import { dirname, resolve } from 'node:path';
|
|
19
|
+
import { fileURLToPath } from 'node:url';
|
|
20
|
+
import { ADVISOR_ROWS, ADVISOR_MATRIX_HEADER, ADVISOR_MATRIX_COLUMNS, renderAdvisorMatrix } from './dispatch-advisor.mjs';
|
|
21
|
+
|
|
22
|
+
const KIT_ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..');
|
|
23
|
+
|
|
24
|
+
export const ADVISOR_MATRIX_DOC = 'references/modes/dispatch.md';
|
|
25
|
+
|
|
26
|
+
// The table is found through an ANCHORED surface, not by "the first header line anywhere". Header
|
|
27
|
+
// search alone is maskable in a way even an exactly-one rule does not close: a faithful copy of the
|
|
28
|
+
// table plus a canonical one whose HEADER drifted leaves exactly one matching header — the decoy's —
|
|
29
|
+
// and the check then reads the decoy and passes. The markers make the checked surface a property of
|
|
30
|
+
// the DOC, so a copy outside them can neither stand in for the table nor hide its drift, and a
|
|
31
|
+
// drifted header INSIDE them leaves the surface with zero headers and fails closed.
|
|
32
|
+
export const ADVISOR_MATRIX_BEGIN = '<!-- advisor-matrix:begin -->';
|
|
33
|
+
export const ADVISOR_MATRIX_END = '<!-- advisor-matrix:end -->';
|
|
34
|
+
|
|
35
|
+
export const readKitDoc = (rel) => readFileSync(resolve(KIT_ROOT, rel), 'utf8');
|
|
36
|
+
|
|
37
|
+
// Both line endings are ordinary here. Splitting on '\n' alone leaves a trailing '\r' on every line
|
|
38
|
+
// of a CRLF-authored doc, and the marker match survives it (it trims) while the exact header match
|
|
39
|
+
// does not — so the check would fail a CORRECT document while naming a drifted header. One split,
|
|
40
|
+
// before anything compares.
|
|
41
|
+
const splitLines = (text) => String(text).split(/\r?\n/);
|
|
42
|
+
|
|
43
|
+
const linesMatching = (lines, marker) => lines.flatMap((line, i) => (line.trim() === marker ? [i] : []));
|
|
44
|
+
|
|
45
|
+
// Only leading and trailing BLANK lines are dropped. Trimming the joined block with String.trim()
|
|
46
|
+
// would also eat significant edge whitespace INSIDE the first and last lines, which is drift the
|
|
47
|
+
// comparison is supposed to see.
|
|
48
|
+
const trimBlankEdges = (lines) => {
|
|
49
|
+
let start = 0;
|
|
50
|
+
let end = lines.length;
|
|
51
|
+
while (start < end && lines[start].trim() === '') start += 1;
|
|
52
|
+
while (end > start && lines[end - 1].trim() === '') end -= 1;
|
|
53
|
+
return lines.slice(start, end);
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
// parseAdvisorMatrix(text) → { ok: true, lines, rows } | { ok: false, reason }. `lines` is the whole
|
|
57
|
+
// anchored block; `rows` are its class rows, parsed for the DIAGNOSIS only — the verdict is the
|
|
58
|
+
// whole-block comparison in checkMatrixStructure, so a deleted alignment rule, a rewritten harness
|
|
59
|
+
// lane and an extra row are all caught, none of which a class-row walk would ever see.
|
|
60
|
+
export const parseAdvisorMatrix = (text) => {
|
|
61
|
+
const lines = splitLines(text);
|
|
62
|
+
const begins = linesMatching(lines, ADVISOR_MATRIX_BEGIN);
|
|
63
|
+
const ends = linesMatching(lines, ADVISOR_MATRIX_END);
|
|
64
|
+
if (begins.length !== 1 || ends.length !== 1) {
|
|
65
|
+
return { ok: false, reason: `the anchored matrix surface is not unique — found ${begins.length} "${ADVISOR_MATRIX_BEGIN}" and ${ends.length} "${ADVISOR_MATRIX_END}" (exactly one of each is required)` };
|
|
66
|
+
}
|
|
67
|
+
if (ends[0] < begins[0]) {
|
|
68
|
+
return { ok: false, reason: 'the matrix end marker precedes its begin marker — the anchored surface is inverted' };
|
|
69
|
+
}
|
|
70
|
+
const surface = trimBlankEdges(lines.slice(begins[0] + 1, ends[0]));
|
|
71
|
+
const headers = surface.filter((line) => line === ADVISOR_MATRIX_HEADER);
|
|
72
|
+
if (headers.length !== 1) {
|
|
73
|
+
return { ok: false, reason: `the anchored matrix surface carries ${headers.length} header line(s) equal to "${ADVISOR_MATRIX_HEADER}" — exactly one is required` };
|
|
74
|
+
}
|
|
75
|
+
const rows = [];
|
|
76
|
+
for (const line of surface.slice(surface.indexOf(ADVISOR_MATRIX_HEADER) + 1)) {
|
|
77
|
+
if (!line.startsWith('|')) continue;
|
|
78
|
+
const cells = line.split('|').slice(1, -1).map((c) => c.trim());
|
|
79
|
+
// Arity refuses OUTRIGHT rather than skipping the row: a row whose cell count disagrees with the
|
|
80
|
+
// header's is malformed whatever it says, and skipping it would report the drift as a MISSING
|
|
81
|
+
// class row — a true verdict reached through a misleading sentence.
|
|
82
|
+
if (cells.length !== ADVISOR_MATRIX_COLUMNS.length) {
|
|
83
|
+
return { ok: false, reason: `a matrix row carries ${cells.length} cell(s), the table has ${ADVISOR_MATRIX_COLUMNS.length} columns: ${line.trim()}` };
|
|
84
|
+
}
|
|
85
|
+
const classCell = /^`(.+)`$/.exec(cells[0]);
|
|
86
|
+
// A row whose first cell is not a backticked class joins no CLASS comparison — the harness lane,
|
|
87
|
+
// or an interloper. Neither escapes: the whole-block equality below sees every line.
|
|
88
|
+
if (classCell === null) continue;
|
|
89
|
+
rows.push(Object.fromEntries(ADVISOR_MATRIX_COLUMNS.map(({ key }, i) => [key, i === 0 ? classCell[1] : cells[i]])));
|
|
90
|
+
}
|
|
91
|
+
return { ok: true, lines: surface, rows };
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
const quoted = (classes) => classes.map((c) => `\`${c}\``).join(', ');
|
|
95
|
+
|
|
96
|
+
// The DIAGNOSIS over the class rows, and the ORDER of its questions is the point. A POSITIONAL walk
|
|
97
|
+
// reads a deleted middle row as a corrupted step-class cell in the row that slid up behind it —
|
|
98
|
+
// technically a difference at that index, and a useless pointer for whoever has to fix the doc. So
|
|
99
|
+
// membership is settled first (duplicated / missing / unregistered), then ORDER, and only over rows
|
|
100
|
+
// that agree on both does a cell comparison run — where the first differing CELL is named, because
|
|
101
|
+
// "row 3 disagrees" leaves the reader to diff four columns by eye.
|
|
102
|
+
const rowDrift = (actual, expected) => {
|
|
103
|
+
const actualClasses = actual.map((r) => r.stepClass);
|
|
104
|
+
const expectedClasses = expected.map((r) => r.stepClass);
|
|
105
|
+
|
|
106
|
+
const duplicated = actualClasses.filter((c, i) => actualClasses.indexOf(c) !== i);
|
|
107
|
+
if (duplicated.length > 0) return `the advisor matrix names ${quoted([...new Set(duplicated)])} more than once — the registry has exactly one row per step class`;
|
|
108
|
+
|
|
109
|
+
const missing = expectedClasses.filter((c) => !actualClasses.includes(c));
|
|
110
|
+
if (missing.length > 0) return `the advisor matrix is missing ${missing.length} registry row(s): ${quoted(missing)}`;
|
|
111
|
+
|
|
112
|
+
const unregistered = actualClasses.filter((c) => !expectedClasses.includes(c));
|
|
113
|
+
if (unregistered.length > 0) return `the advisor matrix names ${unregistered.length} row(s) the registry does not: ${quoted(unregistered)}`;
|
|
114
|
+
|
|
115
|
+
const outOfOrder = actualClasses.findIndex((c, i) => c !== expectedClasses[i]);
|
|
116
|
+
if (outOfOrder !== -1) return `the advisor matrix is out of registry order — row ${outOfOrder + 1} is \`${actualClasses[outOfOrder]}\`, the registry has \`${expectedClasses[outOfOrder]}\``;
|
|
117
|
+
|
|
118
|
+
for (const [i, row] of actual.entries()) {
|
|
119
|
+
const e = expected[i];
|
|
120
|
+
const differing = ADVISOR_MATRIX_COLUMNS.find(({ key }) => row[key] !== e[key]);
|
|
121
|
+
if (differing !== undefined) {
|
|
122
|
+
return `matrix row ${i + 1} (\`${row.stepClass}\`): the ${differing.label} cell reads "${row[differing.key]}", the registry has "${e[differing.key]}"`;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
return null;
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
// blockDrift(actual, expected) → null when the two blocks are IDENTICAL, else the first line that
|
|
129
|
+
// disagrees. It is the verdict and the fallback diagnosis in one: "the blocks are equal" is exactly
|
|
130
|
+
// "no line disagrees", so there is no second comparison to keep in step with this one — and the
|
|
131
|
+
// null return is the path every green run takes, not an unreachable defensive branch.
|
|
132
|
+
const blockDrift = (actual, expected) => {
|
|
133
|
+
for (let i = 0; i < Math.max(actual.length, expected.length); i += 1) {
|
|
134
|
+
if (actual[i] === expected[i]) continue;
|
|
135
|
+
if (actual[i] === undefined) return `the anchored matrix is missing line ${i + 1}, which the canonical table renders as ${JSON.stringify(expected[i])}`;
|
|
136
|
+
if (expected[i] === undefined) return `the anchored matrix carries an extra line ${i + 1}: ${JSON.stringify(actual[i])}`;
|
|
137
|
+
return `matrix line ${i + 1} reads ${JSON.stringify(actual[i])}, the canonical table renders ${JSON.stringify(expected[i])}`;
|
|
138
|
+
}
|
|
139
|
+
return null;
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
// checkMatrixStructure(readText) → the same shape a doc-parity binding result carries, so the report,
|
|
143
|
+
// the --check line and the --json payload all render it through their existing paths. The VERDICT is
|
|
144
|
+
// whole-block equality against the canonical render: every way the doc's table can stop being the
|
|
145
|
+
// registry's table is one comparison, and the enumeration of those ways never has to be maintained.
|
|
146
|
+
export const checkMatrixStructure = (readText = readKitDoc) => {
|
|
147
|
+
const rel = ADVISOR_MATRIX_DOC;
|
|
148
|
+
const expected = ADVISOR_ROWS.map(({ stepClass, vehicle, availabilityNote, returns }) => ({ stepClass, vehicle, availabilityNote, returns }));
|
|
149
|
+
let text;
|
|
150
|
+
try {
|
|
151
|
+
text = readText(rel);
|
|
152
|
+
} catch (err) {
|
|
153
|
+
return { constant: 'advisor-matrix-structure', files: [{ rel, ok: false, reason: `unreadable (${(err && err.code) || (err && err.message) || 'read failed'})` }], ok: false };
|
|
154
|
+
}
|
|
155
|
+
const parsed = parseAdvisorMatrix(text);
|
|
156
|
+
if (parsed.ok === false) {
|
|
157
|
+
return { constant: 'advisor-matrix-structure', files: [{ rel, ok: false, reason: parsed.reason }], ok: false };
|
|
158
|
+
}
|
|
159
|
+
// The VERDICT is the block comparison; the row walk only refines the MESSAGE when it can point at a
|
|
160
|
+
// class row. A block difference the row walk cannot explain (the alignment rule, the harness lane,
|
|
161
|
+
// an interloping row, whitespace) keeps the line-level pointer.
|
|
162
|
+
const drift = blockDrift(parsed.lines, splitLines(renderAdvisorMatrix()));
|
|
163
|
+
const reason = drift === null ? null : (rowDrift(parsed.rows, expected) ?? drift);
|
|
164
|
+
return { constant: 'advisor-matrix-structure', files: [{ rel, ok: reason === null, reason }], ok: reason === null };
|
|
165
|
+
};
|
|
@@ -40,7 +40,7 @@
|
|
|
40
40
|
import { spawnSync } from 'node:child_process';
|
|
41
41
|
import { closeSync, lstatSync, openSync } from 'node:fs';
|
|
42
42
|
import { join } from 'node:path';
|
|
43
|
-
import {
|
|
43
|
+
import { isDirectRun } from './direct-run.mjs';
|
|
44
44
|
import { isExecutableFile, probeSandboxAvailability } from './velocity-profile.mjs';
|
|
45
45
|
import { assertDocsAiDeployment } from './atomic-write.mjs';
|
|
46
46
|
|
|
@@ -484,5 +484,4 @@ export const main = (argv, deps = {}) => {
|
|
|
484
484
|
return finish(applied.status, applied.finalPlan);
|
|
485
485
|
};
|
|
486
486
|
|
|
487
|
-
|
|
488
|
-
if (isDirectRun) process.exit(main(process.argv.slice(2)));
|
|
487
|
+
if (isDirectRun(import.meta.url)) process.exit(main(process.argv.slice(2)));
|
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
//
|
|
24
24
|
// Dependency-free, Node >= 22. No side effects on import (the isDirectRun idiom).
|
|
25
25
|
|
|
26
|
-
import {
|
|
26
|
+
import { isDirectRun } from './direct-run.mjs';
|
|
27
27
|
import { settingValueValid } from './manifest/validate.mjs';
|
|
28
28
|
import { writeHostConfigFileAtomic } from './atomic-write.mjs';
|
|
29
29
|
import {
|
|
@@ -294,8 +294,7 @@ export const main = (argv = [], ctx = {}) => {
|
|
|
294
294
|
}
|
|
295
295
|
};
|
|
296
296
|
|
|
297
|
-
|
|
298
|
-
if (isDirectRun) {
|
|
297
|
+
if (isDirectRun(import.meta.url)) {
|
|
299
298
|
const r = main(process.argv.slice(2));
|
|
300
299
|
if (r.stdout) console.log(r.stdout);
|
|
301
300
|
if (r.stderr) console.error(r.stderr);
|
package/tools/cheap-agents.mjs
CHANGED
|
@@ -27,7 +27,8 @@
|
|
|
27
27
|
|
|
28
28
|
import { existsSync, lstatSync, mkdirSync, readFileSync, writeFileSync, readdirSync } from 'node:fs';
|
|
29
29
|
import { join, resolve, dirname } from 'node:path';
|
|
30
|
-
import { fileURLToPath
|
|
30
|
+
import { fileURLToPath } from 'node:url';
|
|
31
|
+
import { isDirectRun } from './direct-run.mjs';
|
|
31
32
|
import { shellQuoteArg } from './repo-lex.mjs';
|
|
32
33
|
|
|
33
34
|
const HERE = dirname(fileURLToPath(import.meta.url));
|
|
@@ -251,5 +252,4 @@ export const main = (argv = process.argv.slice(2), deps = {}) => {
|
|
|
251
252
|
}
|
|
252
253
|
};
|
|
253
254
|
|
|
254
|
-
|
|
255
|
-
if (isDirectRun) process.exit(main(process.argv.slice(2)));
|
|
255
|
+
if (isDirectRun(import.meta.url)) process.exit(main(process.argv.slice(2)));
|
package/tools/commands.mjs
CHANGED
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
// the `### Mode:` headers in SKILL.md, so the catalog cannot silently drift from the documented modes.
|
|
20
20
|
// Pure, dependency-free, Node >= 22. No side effects on import (the isDirectRun idiom).
|
|
21
21
|
|
|
22
|
-
import {
|
|
22
|
+
import { isDirectRun } from './direct-run.mjs';
|
|
23
23
|
|
|
24
24
|
const SKILL_NAME = 'agent-workflow-kit';
|
|
25
25
|
const BARE_INVOCATION = `/${SKILL_NAME}`;
|
|
@@ -264,7 +264,7 @@ const CATALOG = [
|
|
|
264
264
|
invocation: invocationOf('dispatch'),
|
|
265
265
|
group: 'Orchestrate',
|
|
266
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.',
|
|
267
|
+
oneLine: 'Measure delegation: check a sub-task brief’s contract block (form only — never whether the task is genuinely bounded), ask which vehicle should carry that kind of sub-task on THIS machine and what past threads of that kind actually did — advice you may ignore, never a gate, printed on its own and again under a valid contract check, 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. After a landing, deliver the handoff verbatim and count only what is fully measurable. Writes only its own ledger file beside the repo; never commits.',
|
|
268
268
|
},
|
|
269
269
|
{
|
|
270
270
|
// NEVER `guarded` — that kind promises dry-run-first, which these writers do not have; the
|
|
@@ -273,7 +273,7 @@ const CATALOG = [
|
|
|
273
273
|
invocation: invocationOf('worktrees'),
|
|
274
274
|
group: 'Orchestrate',
|
|
275
275
|
kind: WRITER,
|
|
276
|
-
oneLine: 'Run features in parallel git worktrees: provision an isolated sibling copy, list them, stage a finished one back onto clean main (the commit still asks in dialogue), and remove a live-verified landed one. No preview step; list is read-only; cleanup --abandon destroys unlanded work.',
|
|
276
|
+
oneLine: 'Run features in parallel git worktrees: provision an isolated sibling copy, list them, print the cold-start prompt a fresh session in one of them needs — where it is, what MAIN answers NOW rather than what the record froze, the handoff as the one way back, and the bars nothing enforces — stage a finished one back onto clean main (the commit still asks in dialogue), and remove a live-verified landed one. No preview step; list is read-only and so is prompt; cleanup --abandon destroys unlanded work.',
|
|
277
277
|
},
|
|
278
278
|
];
|
|
279
279
|
|
|
@@ -380,7 +380,6 @@ const main = (argv) => {
|
|
|
380
380
|
console.log(formatHelp());
|
|
381
381
|
};
|
|
382
382
|
|
|
383
|
-
|
|
384
|
-
if (isDirectRun) main(process.argv.slice(2));
|
|
383
|
+
if (isDirectRun(import.meta.url)) main(process.argv.slice(2));
|
|
385
384
|
|
|
386
385
|
export { KINDS, SKILL_NAME, BARE_INVOCATION };
|
package/tools/commit-guard.mjs
CHANGED
|
@@ -11,7 +11,15 @@
|
|
|
11
11
|
// up to INDEX_LAG_PATH_CAP with the remainder stated. A dirty tracked SUBMODULE is named
|
|
12
12
|
// separately with its own recovery. Fail-closed on an undecidable probe. This BLOCKS the
|
|
13
13
|
// deliberate partial commit by design — `--no-verify` is the stated residual, not a flag;
|
|
14
|
-
// 1. recomputes the CURRENT tree fingerprint (the review-state export — read-only git plumbing)
|
|
14
|
+
// 1. recomputes the CURRENT tree fingerprint (the review-state export — read-only git plumbing),
|
|
15
|
+
// and decides the two CONTENT-FREE lanes here, because no store read can answer them: a
|
|
16
|
+
// payload with no bytes yields the ONE fingerprint every clean moment of every repository
|
|
17
|
+
// shares, so any receipt at it was minted elsewhere and may attest nothing. With a DIRTY
|
|
18
|
+
// index that means staged content the payload cannot see (a gitlink hidden by
|
|
19
|
+
// `submodule.<name>.ignore` / `diff.ignoreSubmodules`) and the guard REFUSES, naming the
|
|
20
|
+
// configuration rather than re-staging; with a clean index the commit introduces no bytes
|
|
21
|
+
// (`--allow-empty`, a message-only `--amend`, an empty merge) and the guard PASSES while
|
|
22
|
+
// stating that it attests NOTHING — the receipt arms are skipped, never satisfied;
|
|
15
23
|
// 2. reads the LATEST completed final-run record from the core-evidence store (only the latest
|
|
16
24
|
// attempt at a fingerprint is authoritative — a green receipt is DEAD once a later attempt at
|
|
17
25
|
// the same fingerprint went red) and refuses on: no record for the current fingerprint · a
|
|
@@ -34,11 +42,15 @@
|
|
|
34
42
|
|
|
35
43
|
import { readFileSync, lstatSync } from 'node:fs';
|
|
36
44
|
import { resolve } from 'node:path';
|
|
37
|
-
import {
|
|
45
|
+
import { fileURLToPath } from 'node:url';
|
|
38
46
|
import { spawnSync } from 'node:child_process';
|
|
39
47
|
import { createHash } from 'node:crypto';
|
|
48
|
+
import { isDirectRun } from './direct-run.mjs';
|
|
40
49
|
import { computeTreeFingerprint, buildState, decideCheck, quoteReportName, shellQuoteArg } from './review-state.mjs';
|
|
41
|
-
import {
|
|
50
|
+
import {
|
|
51
|
+
resolveEvidencePath, readEvidence, authoritativeOfKind, canonicalKindSerialization,
|
|
52
|
+
computeWorkingState, CONTENT_FREE_FINGERPRINT,
|
|
53
|
+
} from './core-evidence.mjs';
|
|
42
54
|
import { resolveLcovPath } from './coverage-check.mjs';
|
|
43
55
|
import { GATES_REL, loadDeclaration } from './run-gates.mjs';
|
|
44
56
|
import { computeFlowDecision } from './flow-check.mjs';
|
|
@@ -177,15 +189,42 @@ export const decideIndexLag = (state) => {
|
|
|
177
189
|
};
|
|
178
190
|
|
|
179
191
|
// runGuard({ cwd, env }) → { code, lines }. Every refusal names its recovery.
|
|
192
|
+
// The flow decision's two renders, shared by every lane that consults it — the empty-commit lane
|
|
193
|
+
// reaches the same store through the same consumer mode, so its wording can never drift from the
|
|
194
|
+
// byte-carrying one.
|
|
195
|
+
const flowRefusalLines = (flow) => [
|
|
196
|
+
`commit-guard: REFUSED — the flow store refuses this commit: ${flow.refusals[0]}`,
|
|
197
|
+
...flow.refusals.slice(1).map((r) => `commit-guard: flow refusal — ${r}`),
|
|
198
|
+
];
|
|
199
|
+
const flowAdvisoryLines = (flow) => (flow.present && flow.armed
|
|
200
|
+
? flow.advisories.map((a) => `commit-guard: flow advisory — ${a}`)
|
|
201
|
+
: []);
|
|
202
|
+
|
|
180
203
|
export const runGuard = ({ cwd = process.cwd(), env = process.env } = {}) => {
|
|
181
204
|
const rootTop = gitLine(['rev-parse', '--show-toplevel'], cwd);
|
|
182
205
|
if (rootTop == null) return { code: 1, lines: ['commit-guard: not a git work tree — nothing to guard'] };
|
|
183
206
|
// FIRST: a pure tree property needing no store read. Its recovery re-stages the tree and re-mints
|
|
184
207
|
// the receipt, so every arm below is re-decided anyway — naming a stale fingerprint ahead of it
|
|
185
208
|
// would send the operator down a recovery they must redo.
|
|
186
|
-
const
|
|
209
|
+
const working = computeWorkingState(cwd);
|
|
210
|
+
const indexLag = decideIndexLag(working);
|
|
187
211
|
if (indexLag !== null) return indexLag;
|
|
188
212
|
const fingerprint = computeTreeFingerprint(cwd);
|
|
213
|
+
// The CONTENT-FREE lanes — the second pure tree property, decided here for the same reason the
|
|
214
|
+
// index lag is: no store read can answer it. A payload with no bytes states nothing about what
|
|
215
|
+
// this commit will carry, and its fingerprint is the ONE value every clean moment of every
|
|
216
|
+
// repository shares, so a receipt found at it was minted by some other moment, possibly at
|
|
217
|
+
// another base. Such evidence must therefore decide NOTHING here — neither refuse nor attest
|
|
218
|
+
// (the same fact flow-check-rungs.mjs applies to a red final). The index tells the two lanes
|
|
219
|
+
// apart, and `computeWorkingState` probes it with --ignore-submodules=none precisely so a
|
|
220
|
+
// config-hidden gitlink cannot pass for a clean one.
|
|
221
|
+
const contentFree = fingerprint === CONTENT_FREE_FINGERPRINT;
|
|
222
|
+
if (contentFree && working.stagedDirty) {
|
|
223
|
+
return {
|
|
224
|
+
code: 1,
|
|
225
|
+
lines: [`commit-guard: REFUSED — the index carries staged content the fingerprint domain cannot see (a submodule gitlink hidden from \`git diff\` by \`submodule.<name>.ignore\` or \`diff.ignoreSubmodules\`), so no final receipt can describe what this commit will carry. Recovery: clear that ignore setting (or set it to \`none\`) until \`git diff --cached --no-ext-diff\` shows the change, then re-run node ${FINAL_RUN_TOOL} --final`],
|
|
226
|
+
};
|
|
227
|
+
}
|
|
189
228
|
// The guard's OWN reads resolve FIXED git-dir paths — a stray AW_CORE_EVIDENCE / AW_LCOV_FILE
|
|
190
229
|
// in the committing shell must never redirect the LAST line of defense to a forged artifact
|
|
191
230
|
// (the env stays a test seam for the producers, never for this consumer).
|
|
@@ -194,6 +233,27 @@ export const runGuard = ({ cwd = process.cwd(), env = process.env } = {}) => {
|
|
|
194
233
|
if ((read.malformed ?? 0) > 0 || read.readError) {
|
|
195
234
|
return { code: 1, lines: [`commit-guard: REFUSED — evidence store unavailable (${read.malformed} malformed line(s)${read.readError ? `, read error: ${read.readError}` : ''}); inspect ${storePath}`] };
|
|
196
235
|
}
|
|
236
|
+
// The empty-commit lane: the index equals HEAD, so this commit introduces no bytes at all
|
|
237
|
+
// (`git commit --allow-empty`, a message- or signature-only `--amend`, an empty merge). The
|
|
238
|
+
// guard's whole claim is about bytes, so here it has none to make and says so. The receipt arms
|
|
239
|
+
// are SKIPPED rather than satisfied — consulting a content-free receipt would make the outcome
|
|
240
|
+
// depend on which stray clean moment happened to be recorded last. The flow arm still runs: an
|
|
241
|
+
// empty commit still moves HEAD, and the chain bookkeeping is about that, not about bytes; its
|
|
242
|
+
// own fingerprint-keyed correlations (the D10 flow→final binding, receipt and degrade coverage)
|
|
243
|
+
// drop out inside flow-check on the same fact, so no stray content-free record decides here
|
|
244
|
+
// either. Store HEALTH is deliberately NOT waived above: an unreadable store is not a
|
|
245
|
+
// correlation, and a store that cannot be read cannot answer the chain questions either.
|
|
246
|
+
if (contentFree) {
|
|
247
|
+
const emptyFlow = computeFlowDecision({ cwd, consumer: 'commit-guard', treeCarriesBytes: false });
|
|
248
|
+
if (emptyFlow.refusals.length > 0) return { code: 1, lines: flowRefusalLines(emptyFlow) };
|
|
249
|
+
return {
|
|
250
|
+
code: 0,
|
|
251
|
+
lines: [
|
|
252
|
+
'commit-guard: PASS — this commit changes no tree content (the index contributes no tree-content delta and the work tree adds nothing), so the guard attests NOTHING about it: a receipt found at the shared content-free fingerprint cannot be correlated to THIS moment or base',
|
|
253
|
+
...flowAdvisoryLines(emptyFlow),
|
|
254
|
+
],
|
|
255
|
+
};
|
|
256
|
+
}
|
|
197
257
|
const finals = authoritativeOfKind(read.records, 'final');
|
|
198
258
|
const current = finals.find((r) => r.fingerprintBefore === fingerprint) ?? null;
|
|
199
259
|
if (!current) {
|
|
@@ -255,15 +315,7 @@ export const runGuard = ({ cwd = process.cwd(), env = process.env } = {}) => {
|
|
|
255
315
|
// evidenceHashes.flow and the store has since VANISHED (present=false) — a deletion must
|
|
256
316
|
// never un-arm the binding. A no-store repo with no flow-bearing receipt still yields zero
|
|
257
317
|
// refusals (byte-exact pre-flow behavior).
|
|
258
|
-
if (flow.refusals.length > 0) {
|
|
259
|
-
return {
|
|
260
|
-
code: 1,
|
|
261
|
-
lines: [
|
|
262
|
-
`commit-guard: REFUSED — the flow store refuses this commit: ${flow.refusals[0]}`,
|
|
263
|
-
...flow.refusals.slice(1).map((r) => `commit-guard: flow refusal — ${r}`),
|
|
264
|
-
],
|
|
265
|
-
};
|
|
266
|
-
}
|
|
318
|
+
if (flow.refusals.length > 0) return { code: 1, lines: flowRefusalLines(flow) };
|
|
267
319
|
// The ship-receipt arm: the SAME normative decision review-state --check computes, over a
|
|
268
320
|
// SANITIZED env — the receipts/evidence/flow-store overrides are producer test seams, and
|
|
269
321
|
// honoring them HERE would let a forged store bypass the fixed-path reads above.
|
|
@@ -278,10 +330,7 @@ export const runGuard = ({ cwd = process.cwd(), env = process.env } = {}) => {
|
|
|
278
330
|
const flowSuffix = flow.present && flow.armed
|
|
279
331
|
? ` — flow: armed${review.flowLabels?.length ? ` (${review.flowLabels.join('; ')})` : ''}`
|
|
280
332
|
: '';
|
|
281
|
-
|
|
282
|
-
? flow.advisories.map((a) => `commit-guard: flow advisory — ${a}`)
|
|
283
|
-
: [];
|
|
284
|
-
return { code: 0, lines: [`commit-guard: PASS — a green final receipt binds this exact tree (${fingerprint.slice(0, 12)}…), the declaration and evidence hashes match, and the review obligations are satisfied${flowSuffix}`, ...flowAdvisoryLines] };
|
|
333
|
+
return { code: 0, lines: [`commit-guard: PASS — a green final receipt binds this exact tree (${fingerprint.slice(0, 12)}…), the declaration and evidence hashes match, and the review obligations are satisfied${flowSuffix}`, ...flowAdvisoryLines(flow)] };
|
|
285
334
|
};
|
|
286
335
|
|
|
287
336
|
const HELP = `commit-guard — the read-only pre-commit guard (agent-workflow family, D10).
|
|
@@ -291,7 +340,16 @@ Usage:
|
|
|
291
340
|
|
|
292
341
|
Re-runs NOTHING: refuses an INDEX that lags the verified working tree (FIRST — unstaged tracked
|
|
293
342
|
paths, reviewable untracked paths, or a dirty tracked submodule, each named with its recovery;
|
|
294
|
-
this deliberately blocks a partial commit), then recomputes the current tree fingerprint
|
|
343
|
+
this deliberately blocks a partial commit), then recomputes the current tree fingerprint.
|
|
344
|
+
|
|
345
|
+
A CONTENT-FREE fingerprint (a payload with no bytes — the value every clean work tree shares)
|
|
346
|
+
decides WITHOUT a receipt, because one found there was minted by another clean moment: with a dirty
|
|
347
|
+
index it REFUSES (staged content the payload cannot see — a gitlink hidden by
|
|
348
|
+
\`submodule.<name>.ignore\` / \`diff.ignoreSubmodules\`; the recovery is that configuration, not
|
|
349
|
+
\`git add\`), and with a clean index it PASSES stating it attests NOTHING (the commit carries no
|
|
350
|
+
bytes: \`--allow-empty\`, a message-only \`--amend\`, an empty merge).
|
|
351
|
+
|
|
352
|
+
Otherwise it binds
|
|
295
353
|
the LATEST completed run-gates --final receipt — refusing on { no receipt for this tree · a red
|
|
296
354
|
latest attempt · before≠after · declaration content drift · evidence-hash drift · lcov drift ·
|
|
297
355
|
a flow-store refusal (a PRESENT store's open own chain / base motion / coverage — verbatim; no
|
|
@@ -324,8 +382,7 @@ export const main = (argv, ctx = {}) => {
|
|
|
324
382
|
}
|
|
325
383
|
};
|
|
326
384
|
|
|
327
|
-
|
|
328
|
-
if (isDirectRun) {
|
|
385
|
+
if (isDirectRun(import.meta.url)) {
|
|
329
386
|
const r = main(process.argv.slice(2));
|
|
330
387
|
if (r.stdout) process.stdout.write(r.stdout.endsWith('\n') ? r.stdout : `${r.stdout}\n`);
|
|
331
388
|
if (r.stderr) process.stderr.write(r.stderr.endsWith('\n') ? r.stderr : `${r.stderr}\n`);
|
package/tools/core-evidence.mjs
CHANGED
|
@@ -36,11 +36,11 @@
|
|
|
36
36
|
|
|
37
37
|
import { readFileSync, lstatSync, realpathSync, readlinkSync, openSync, readSync, closeSync } from 'node:fs';
|
|
38
38
|
import { join, dirname, normalize, sep, basename } from 'node:path';
|
|
39
|
-
import { pathToFileURL } from 'node:url';
|
|
40
39
|
import { spawnSync } from 'node:child_process';
|
|
41
40
|
import { createHash } from 'node:crypto';
|
|
42
41
|
import { writeContainedFileAtomic } from './atomic-write.mjs';
|
|
43
42
|
import { parsePositiveIntKnob, probeVerdict } from './changed-surface.mjs';
|
|
43
|
+
import { isDirectRun } from './direct-run.mjs';
|
|
44
44
|
import { readRegularFileNoFollow } from './fs-read-nofollow.mjs';
|
|
45
45
|
import { lexicalRepoRelative } from './repo-lex.mjs';
|
|
46
46
|
// The coverage vocabulary leaf: run-gates RECORDS the token this validator checks, and run-gates
|
|
@@ -160,6 +160,16 @@ export const computeTreeFingerprint = (cwd, fsx) => {
|
|
|
160
160
|
return payload == null ? null : createHash('sha256').update(payload).digest('hex');
|
|
161
161
|
};
|
|
162
162
|
|
|
163
|
+
// The fingerprint of a CONTENT-FREE payload — a clean work tree emits no bytes at all, so this ONE
|
|
164
|
+
// value is shared by every clean moment of every repository. It therefore identifies no working
|
|
165
|
+
// state and correlates to no base: evidence found at it was minted by some other clean moment,
|
|
166
|
+
// possibly at another base, and can decide nothing in either direction. Two situations reach it,
|
|
167
|
+
// and only the INDEX tells them apart (never the payload): an empty commit, where the index equals
|
|
168
|
+
// HEAD and no byte enters the repository, and staged content the payload cannot see — a gitlink
|
|
169
|
+
// hidden from `git diff` by an ignore configuration. Read by the consumers that correlate a
|
|
170
|
+
// fingerprint to a base (flow-check-rungs.mjs #65) and by commit-guard's two content-free lanes.
|
|
171
|
+
export const CONTENT_FREE_FINGERPRINT = createHash('sha256').update(Buffer.alloc(0)).digest('hex');
|
|
172
|
+
|
|
163
173
|
// The index↔worktree split the fingerprint deliberately CANNOT see: the payload above concatenates
|
|
164
174
|
// the staged and unstaged diffs, so against an otherwise-empty index a hunk moving into the index
|
|
165
175
|
// leaves it byte-identical — while `git commit` builds the commit from the INDEX alone. This is the
|
|
@@ -1220,8 +1230,7 @@ export const main = (argv, ctx = {}) => {
|
|
|
1220
1230
|
}
|
|
1221
1231
|
};
|
|
1222
1232
|
|
|
1223
|
-
|
|
1224
|
-
if (isDirectRun) {
|
|
1233
|
+
if (isDirectRun(import.meta.url)) {
|
|
1225
1234
|
const r = main(process.argv.slice(2));
|
|
1226
1235
|
if (r.stdout) process.stdout.write(r.stdout.endsWith('\n') ? r.stdout : `${r.stdout}\n`);
|
|
1227
1236
|
if (r.stderr) process.stderr.write(r.stderr.endsWith('\n') ? r.stderr : `${r.stderr}\n`);
|