@holmes-lab/holmes-kit 0.12.2 → 0.14.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 +127 -0
- package/README.md +13 -3
- package/dist/.build-id +1 -1
- package/dist/holmes/cli/approve-context.js +10 -10
- package/dist/holmes/cli/approve-ref.js +5 -5
- package/dist/holmes/cli/approve-watch.d.ts +1 -1
- package/dist/holmes/cli/approve-watch.js +6 -6
- package/dist/holmes/cli/approve.d.ts +3 -3
- package/dist/holmes/cli/approve.js +57 -57
- package/dist/holmes/cli/autonomy.d.ts +22 -0
- package/dist/holmes/cli/autonomy.js +145 -0
- package/dist/holmes/cli/colophon.d.ts +6 -0
- package/dist/holmes/cli/colophon.js +24 -0
- package/dist/holmes/cli/doctor.d.ts +2 -2
- package/dist/holmes/cli/doctor.js +104 -87
- package/dist/holmes/cli/index.js +122 -63
- package/dist/holmes/cli/init.d.ts +2 -0
- package/dist/holmes/cli/init.js +31 -19
- package/dist/holmes/cli/interactive-prompt.d.ts +8 -0
- package/dist/holmes/cli/interactive-prompt.js +23 -0
- package/dist/holmes/cli/semantic-key.js +9 -9
- package/dist/holmes/cli/settings-merge.d.ts +2 -1
- package/dist/holmes/cli/settings-merge.js +15 -3
- package/dist/holmes/cli/upgrade.js +7 -7
- package/dist/holmes/cpg/proposed-content.js +2 -2
- package/dist/holmes/governance/autonomy.d.ts +10 -2
- package/dist/holmes/governance/autonomy.js +191 -5
- package/dist/holmes/guardrail/blind-spots.js +15 -15
- package/dist/holmes/guardrail/dependency-delta.d.ts +3 -0
- package/dist/holmes/guardrail/dependency-delta.js +118 -0
- package/dist/holmes/hooks/pre-tool-use.js +111 -42
- package/dist/holmes/hooks/session-start.js +17 -0
- package/dist/holmes/hooks/stop.d.ts +22 -1
- package/dist/holmes/hooks/stop.js +176 -14
- package/dist/holmes/mcp/handlers.js +14 -1
- package/dist/holmes/project/root.js +9 -1
- package/dist/holmes/semantic/credentials.js +1 -1
- package/dist/holmes/spec/id-collision.js +2 -2
- package/package.json +2 -2
- package/playbooks/publish/PLAYBOOK.md +47 -35
- package/playbooks/remediation/PLAYBOOK.md +1 -1
|
@@ -3,12 +3,12 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.GATE_BLIND_SPOTS = void 0;
|
|
4
4
|
exports.blindSpotSummary = blindSpotSummary;
|
|
5
5
|
exports.GATE_BLIND_SPOTS = [
|
|
6
|
-
{ example: 'node ./cleanup.js', why: '
|
|
7
|
-
{ example: 'bash ./deploy.sh', why: '
|
|
8
|
-
{ example: 'npm run clean', why: 'package.json
|
|
9
|
-
{ example: 'make distclean', why: 'Makefile
|
|
10
|
-
{ example: 'CMD="something"; $CMD', why: '
|
|
11
|
-
{ example: 'xargs rm < targets.txt', why: '
|
|
6
|
+
{ example: 'node ./cleanup.js', why: 'the script file\'s contents are not in the command string' },
|
|
7
|
+
{ example: 'bash ./deploy.sh', why: 'same reason — what runs lives inside the file' },
|
|
8
|
+
{ example: 'npm run clean', why: 'the script definition in package.json is outside the command string' },
|
|
9
|
+
{ example: 'make distclean', why: 'the Makefile rule is outside the command string' },
|
|
10
|
+
{ example: 'CMD="something"; $CMD', why: 'the command to run is assembled at execution time' },
|
|
11
|
+
{ example: 'xargs rm < targets.txt', why: 'the target list arrives on standard input' },
|
|
12
12
|
];
|
|
13
13
|
/**
|
|
14
14
|
* One line for `doctor`.
|
|
@@ -18,23 +18,23 @@ exports.GATE_BLIND_SPOTS = [
|
|
|
18
18
|
* parts include creating the script in the first place.
|
|
19
19
|
*/
|
|
20
20
|
function blindSpotSummary() {
|
|
21
|
-
return '
|
|
22
|
-
+ `(${exports.GATE_BLIND_SPOTS.slice(0, 3).map((b) => b.example).join(', ')}
|
|
23
|
-
+ '
|
|
21
|
+
return 'The guardrail inspects the command string — it cannot see the contents of a command that runs a script already in the repository'
|
|
22
|
+
+ ` (e.g. ${exports.GATE_BLIND_SPOTS.slice(0, 3).map((b) => b.example).join(', ')}).`
|
|
23
|
+
+ ' Writing a new script requires approval (A-SPEC-167).'
|
|
24
24
|
// @implements A-SPEC-170
|
|
25
25
|
// Stated, not implied. Four sweep rounds each found runner-manifest names the previous round's
|
|
26
26
|
// list had missed, so calling the list complete would be false — and this is the one place the
|
|
27
27
|
// harness tells a user what it cannot see. It belongs in the prose, not in GATE_BLIND_SPOTS,
|
|
28
28
|
// whose entries are commands a test executes against the gate.
|
|
29
|
-
+ '
|
|
29
|
+
+ ' The list of root runner-manifest names (Rakefile, Gemfile, etc.) is not complete — every ecosystem adds new ones.'
|
|
30
30
|
// @implements A-SPEC-175
|
|
31
31
|
// The residual after REQ-175. Losing `.ax/specs` is now detected, because the ledger records the
|
|
32
32
|
// approvals and `init`'s .gitignore block keeps `provenance*.jsonl` committed. Losing the whole
|
|
33
33
|
// `.ax` takes that evidence with it, and a project then looks like one that never opted in. Every
|
|
34
34
|
// shell route to either state is denied by this gate; what remains is deletion from outside the
|
|
35
35
|
// session, and saying so is the only honest option left.
|
|
36
|
-
+ ' `.ax`
|
|
37
|
-
+ '
|
|
36
|
+
+ ' If the whole `.ax` is deleted from outside the session (editor, Finder, git outside the session), the ledger goes with it,'
|
|
37
|
+
+ ' making the project indistinguishable from one that never opted in — losing only `.ax/specs` is detected (REQ-175).'
|
|
38
38
|
// @implements A-SPEC-421
|
|
39
39
|
// Measured against the INSTALLED build, which is the only place it shows: `tool_name: "Bash"`
|
|
40
40
|
// with `rm -rf /` is denied and `tool_name: "mcp__shell__run"` with the same command is allowed.
|
|
@@ -43,7 +43,7 @@ function blindSpotSummary() {
|
|
|
43
43
|
// is the shape of the gap, not a hypothetical. Denying unknown names instead was considered and
|
|
44
44
|
// rejected: it would refuse every harness's Read, Grep and search tools and leave the guardrail
|
|
45
45
|
// unusable. What is fixable here is the map, and an undeclared gap reads as coverage.
|
|
46
|
-
+ '
|
|
47
|
-
+ ' MCP
|
|
48
|
-
+ '
|
|
46
|
+
+ ' The gate dispatches on the **tool name**, so a tool whose name it does not know is not inspected —'
|
|
47
|
+
+ ' a tool provided by an MCP server (`mcp__<server>__<tool>`) does not pass through this gate even when it runs a shell.'
|
|
48
|
+
+ ' Control MCP servers through their own approval paths.';
|
|
49
49
|
}
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// @implements A-SPEC-559.1
|
|
3
|
+
// @implements A-SPEC-560.1
|
|
4
|
+
// Pure external-dependency-delta detector: given a source file's content, extract its EXTERNAL
|
|
5
|
+
// (non-relative, "bare") import specifiers normalized to the top-level package, and report which
|
|
6
|
+
// external packages a before→after change newly introduces. The repo convention is `importTargetOf`'s
|
|
7
|
+
// (`guardrail/forbidden-edges.ts`): a `.`-prefixed specifier is relative (internal); anything else is
|
|
8
|
+
// external. Pure, deterministic, never throws — the git/env/queue wiring lives in the Stop hook.
|
|
9
|
+
//
|
|
10
|
+
// Hardened after the post-ship adversarial audit (REQ-560): prettier-style MULTILINE imports are the
|
|
11
|
+
// most common real-world form and the original line-anchored regex missed them entirely; CRLF files
|
|
12
|
+
// broke line-comment stripping into false deltas; a string-embedded `/*` swallowed real imports; and
|
|
13
|
+
// the un-anchored require()/import() patterns matched inside string literals (our own test fixtures
|
|
14
|
+
// self-triggered). The pipeline below is: normalize CRLF → strip line-START block comments only →
|
|
15
|
+
// strip template/triple-quoted literals → strip line comments (URL-preserving) → match, with a
|
|
16
|
+
// quote-parity rejection for require()/import() so string-embedded calls stay silent.
|
|
17
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
18
|
+
exports.externalImportsOf = externalImportsOf;
|
|
19
|
+
exports.dependencyDelta = dependencyDelta;
|
|
20
|
+
// A new import of one of these is a language builtin, not an added third-party dependency, so it is
|
|
21
|
+
// NOT an architecture-drift signal. The lists are deliberately common-case, not exhaustive: a missed
|
|
22
|
+
// builtin only produces an observe-first warning, never a block.
|
|
23
|
+
const TS_BUILTINS = new Set([
|
|
24
|
+
'fs', 'path', 'os', 'crypto', 'util', 'events', 'stream', 'http', 'https', 'url', 'child_process',
|
|
25
|
+
'process', 'assert', 'buffer', 'net', 'tls', 'zlib', 'querystring', 'string_decoder', 'timers',
|
|
26
|
+
'tty', 'dgram', 'dns', 'readline', 'repl', 'vm', 'worker_threads', 'cluster', 'perf_hooks',
|
|
27
|
+
'async_hooks', 'v8', 'module', 'constants', 'punycode', 'inspector', 'diagnostics_channel', 'test',
|
|
28
|
+
]);
|
|
29
|
+
const PY_STDLIB = new Set([
|
|
30
|
+
'os', 'sys', 're', 'json', 'typing', 'pathlib', 'subprocess', 'time', 'datetime', 'collections',
|
|
31
|
+
'itertools', 'functools', 'math', 'random', 'logging', 'io', 'abc', 'enum', 'dataclasses', 'asyncio',
|
|
32
|
+
'contextlib', 'hashlib', 'base64', 'tempfile', 'shutil', 'glob', 'argparse', 'unittest', 'threading',
|
|
33
|
+
'queue', 'socket', 'struct', 'copy', 'warnings', 'traceback', 'inspect', 'textwrap', 'operator',
|
|
34
|
+
'importlib', 'urllib', 'pickle', 'csv', 'uuid', 'string', 'multiprocessing', 'concurrent', 'sqlite3',
|
|
35
|
+
'secrets', 'signal', 'types', 'ast', 'email', 'http', 'xml', 'array',
|
|
36
|
+
]);
|
|
37
|
+
/** Is position `idx` of `line` inside a single- or double-quoted string? (parity of quotes before it) */
|
|
38
|
+
function insideString(line, idx) {
|
|
39
|
+
let sq = 0, dq = 0;
|
|
40
|
+
for (let i = 0; i < idx; i++) {
|
|
41
|
+
const c = line[i];
|
|
42
|
+
if (c === "'")
|
|
43
|
+
sq += 1;
|
|
44
|
+
else if (c === '"')
|
|
45
|
+
dq += 1;
|
|
46
|
+
}
|
|
47
|
+
return sq % 2 === 1 || dq % 2 === 1;
|
|
48
|
+
}
|
|
49
|
+
/** Top-level package of a TS/JS specifier: `@scope/pkg/sub` → `@scope/pkg`, `pkg/sub` → `pkg`. */
|
|
50
|
+
function tsTopLevel(spec) {
|
|
51
|
+
const parts = spec.split('/');
|
|
52
|
+
return spec.startsWith('@') && parts.length >= 2 ? `${parts[0]}/${parts[1]}` : parts[0];
|
|
53
|
+
}
|
|
54
|
+
function externalImportsOf(source, lang) {
|
|
55
|
+
const out = new Set();
|
|
56
|
+
try {
|
|
57
|
+
let src = source.replace(/\r\n?/g, '\n'); // CRLF/CR → LF first
|
|
58
|
+
if (lang === 'ts') {
|
|
59
|
+
// Line-START-anchored block comments only: kills a commented-out import block WITHOUT letting a
|
|
60
|
+
// string-embedded "/*" swallow the real code after it (and keeps the scan linear — the engine
|
|
61
|
+
// only attempts matches at line starts).
|
|
62
|
+
src = src.replace(/^[ \t]*\/\*[\s\S]*?\*\//gm, '');
|
|
63
|
+
src = src.replace(/`[^`]*`/g, '``'); // template literals are data
|
|
64
|
+
src = src.replace(/(?<!:)\/\/.*$/gm, ''); // line comments; `https://` survives
|
|
65
|
+
const add = (raw) => {
|
|
66
|
+
if (raw === undefined || raw === '' || raw.startsWith('.') || raw.startsWith('node:'))
|
|
67
|
+
return;
|
|
68
|
+
const pkg = tsTopLevel(raw);
|
|
69
|
+
if (pkg !== '' && !TS_BUILTINS.has(pkg))
|
|
70
|
+
out.add(pkg);
|
|
71
|
+
};
|
|
72
|
+
// A from-clause ENDS some line — any non-quote prefix, so prettier's `} from 'pkg'` is caught.
|
|
73
|
+
for (const m of src.matchAll(/^[^'"`\n]*\bfrom\s*['"]([^'"\n]+)['"]/gm))
|
|
74
|
+
add(m[1]);
|
|
75
|
+
for (const m of src.matchAll(/^\s*import\s*['"]([^'"\n]+)['"]/gm))
|
|
76
|
+
add(m[1]); // side-effect import
|
|
77
|
+
// require()/dynamic import() are position-free, so reject matches sitting inside a string
|
|
78
|
+
// literal (quote parity before the match on its line) and method calls (`loader.require`).
|
|
79
|
+
for (const line of src.split('\n')) {
|
|
80
|
+
for (const m of line.matchAll(/(?<!\.)\brequire\s*\(\s*['"]([^'"\n]+)['"]\s*\)/g)) {
|
|
81
|
+
if (!insideString(line, m.index ?? 0))
|
|
82
|
+
add(m[1]);
|
|
83
|
+
}
|
|
84
|
+
for (const m of line.matchAll(/\bimport\s*\(\s*['"]([^'"\n]+)['"]\s*\)/g)) {
|
|
85
|
+
if (!insideString(line, m.index ?? 0))
|
|
86
|
+
add(m[1]);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
else {
|
|
91
|
+
src = src.replace(/"""[\s\S]*?"""/g, '').replace(/'''[\s\S]*?'''/g, ''); // docstrings are data
|
|
92
|
+
src = src.split('\n').map((l) => l.replace(/#.*$/, '')).join('\n');
|
|
93
|
+
const add = (raw) => {
|
|
94
|
+
if (raw === undefined || raw === '' || raw.startsWith('.'))
|
|
95
|
+
return;
|
|
96
|
+
const pkg = raw.split('.')[0];
|
|
97
|
+
if (pkg !== '' && !PY_STDLIB.has(pkg))
|
|
98
|
+
out.add(pkg);
|
|
99
|
+
};
|
|
100
|
+
for (const m of src.matchAll(/^\s*import\s+(.+)$/gm)) {
|
|
101
|
+
// `import a, b as c` — every comma part counts; sanitize each to a leading dotted name so a
|
|
102
|
+
// trailing `;`/comment fragment can never fabricate a package token.
|
|
103
|
+
for (const part of m[1].split(','))
|
|
104
|
+
add(/^[A-Za-z_][\w.]*/.exec(part.trim())?.[0]);
|
|
105
|
+
}
|
|
106
|
+
for (const m of src.matchAll(/^\s*from\s+([.\w]+)\s+import\b/gm))
|
|
107
|
+
add(m[1]);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
catch {
|
|
111
|
+
return out; // never throw — observe-first
|
|
112
|
+
}
|
|
113
|
+
return out;
|
|
114
|
+
}
|
|
115
|
+
function dependencyDelta(before, after, lang) {
|
|
116
|
+
const had = externalImportsOf(before, lang);
|
|
117
|
+
return [...externalImportsOf(after, lang)].filter((x) => !had.has(x)).sort();
|
|
118
|
+
}
|
|
@@ -305,7 +305,7 @@ function preEditEvidenceGate(o) {
|
|
|
305
305
|
}
|
|
306
306
|
function decideOnGateError(opts) {
|
|
307
307
|
if (opts.bypass) {
|
|
308
|
-
return { permissionDecision: 'allow', permissionDecisionReason: `[Holmes-Kit]
|
|
308
|
+
return { permissionDecision: 'allow', permissionDecisionReason: `[Holmes-Kit] an internal error was bypassed via HOLMES_GATE_BYPASS: ${opts.message}` };
|
|
309
309
|
}
|
|
310
310
|
if (!opts.governed)
|
|
311
311
|
return { permissionDecision: 'allow' };
|
|
@@ -313,8 +313,8 @@ function decideOnGateError(opts) {
|
|
|
313
313
|
// spec chain that was never the problem.
|
|
314
314
|
return {
|
|
315
315
|
permissionDecision: 'deny',
|
|
316
|
-
permissionDecisionReason: `[Holmes-Kit]
|
|
317
|
-
+ ' → `holmes-kit doctor
|
|
316
|
+
permissionDecisionReason: `[Holmes-Kit] cannot judge due to an internal error — the gate does not allow when it cannot decide: ${opts.message}`
|
|
317
|
+
+ ' → check the install with `holmes-kit doctor`. To let it through by operator judgment, set HOLMES_GATE_BYPASS=1 in the environment (an out-of-band channel a session cannot set itself).',
|
|
318
318
|
};
|
|
319
319
|
}
|
|
320
320
|
// @implements A-SPEC-514.1
|
|
@@ -479,7 +479,7 @@ function evaluateHook(input, specsDir, opts) {
|
|
|
479
479
|
// first legitimate use in every new project. The single-use guarantee still holds: the record
|
|
480
480
|
// lands in the resolved root, which is the same file every later call resolves to.
|
|
481
481
|
if ((0, provenance_chain_1.blankNonce)(acting?.nonce)) {
|
|
482
|
-
return deny('
|
|
482
|
+
return deny('the approval declared single-use (nonce) but the value is empty — single-use cannot be enforced, so it is denied');
|
|
483
483
|
}
|
|
484
484
|
if (acting?.nonce) {
|
|
485
485
|
let won;
|
|
@@ -579,8 +579,8 @@ function evaluateHook(input, specsDir, opts) {
|
|
|
579
579
|
return {
|
|
580
580
|
permissionDecision: 'deny',
|
|
581
581
|
permissionDecisionReason: GRANTS_SELF
|
|
582
|
-
? '[Holmes-Kit]
|
|
583
|
-
+ '
|
|
582
|
+
? '[Holmes-Kit] the command sets or reads HOLMES_ROLE/HOLMES_GATE_BYPASS — a path by which a session grants itself authority.'
|
|
583
|
+
+ ' Roles are set out-of-band by whoever launches the agent. To run it by operator judgment, an approval that includes config-write is required.'
|
|
584
584
|
: '[Holmes-Kit] command reads or dumps process environment (holds the out-of-band HOLMES_APPROVAL / HOLMES_LEDGER_KEY secrets) — requires out-of-band approval',
|
|
585
585
|
};
|
|
586
586
|
}
|
|
@@ -624,7 +624,7 @@ function evaluateHook(input, specsDir, opts) {
|
|
|
624
624
|
// containing that letter. Caught by a test, not by reading.
|
|
625
625
|
// @implements A-SPEC-463 — the path sits BETWEEN quotes here, so spaces inside it are part
|
|
626
626
|
// of the path, not an argument boundary. `[^'"\s]*` stopped at the first space and missed
|
|
627
|
-
// `open('C:\Users
|
|
627
|
+
// `open('C:\Users\Given Family\a.ts','w')`; `[^'"]*` keeps the space and still cannot cross a quote.
|
|
628
628
|
const NOT_QUOTE = String.raw `[^'"]*`;
|
|
629
629
|
const QUOTED_PATH = new RegExp(String.raw `['"](` + NOT_QUOTE + String.raw `\.` + CODE_EXT + '|' + NOT_QUOTE + CONFIG_FILE + String.raw `)['"]`, 'g');
|
|
630
630
|
const out = [];
|
|
@@ -644,7 +644,7 @@ function evaluateHook(input, specsDir, opts) {
|
|
|
644
644
|
// @implements A-SPEC-463 — Windows W-2. The extraction below approximated a path with a
|
|
645
645
|
// character class that excludes spaces (`[^\s...]*`) preceded by an OPTIONAL opening quote
|
|
646
646
|
// that no closing quote ever paired. A quoted path with a space — the Windows default home is
|
|
647
|
-
// `C:\Users
|
|
647
|
+
// `C:\Users\<Given Family>` — matched only up to the first space, never reached the extension, and
|
|
648
648
|
// was captured as nothing, so the write was allowed. `pathAlt` is three alternatives: a
|
|
649
649
|
// double- or single-quoted path (spaces kept, up to the closing quote) or the unquoted form
|
|
650
650
|
// (still ends at the first shell word boundary — an un-escaped space is an argument break).
|
|
@@ -740,6 +740,7 @@ function evaluateHook(input, specsDir, opts) {
|
|
|
740
740
|
// which is an ordinary backup and not this gate's business.
|
|
741
741
|
// (A data heredoc body may still carry separators inside one segment, so the per-segment
|
|
742
742
|
// text is split the way the whole command used to be — the base is the segment's either way.)
|
|
743
|
+
const CODE_TAIL_RE = new RegExp(`\\.${CODE_EXT}$`);
|
|
743
744
|
for (const seg of segments) {
|
|
744
745
|
for (const sub of seg.text.split(/[;|&]+/)) {
|
|
745
746
|
if (!/\b(?:cp|mv|install|rsync|ln|dd)\b/.test(sub))
|
|
@@ -752,10 +753,72 @@ function evaluateHook(input, specsDir, opts) {
|
|
|
752
753
|
continue;
|
|
753
754
|
}
|
|
754
755
|
}
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
756
|
+
// @implements A-SPEC-550.1 — for cp/mv/install/rsync/ln the DESTINATION is the last path
|
|
757
|
+
// argument, whatever the source's extension. The old rule matched only code-extension paths
|
|
758
|
+
// and required >=2 of them, so a NON-code source left the destination as the sole code path
|
|
759
|
+
// and walked through: `cp /tmp/x.txt src/a.ts`, `install /dev/null src/a.ts`, and even
|
|
760
|
+
// `cp /dev/stdin src/a.ts <<EOF` (content in the command). Judge the destination directly.
|
|
761
|
+
// A code path that is a SOURCE (not last) is not the destination and is left alone, so a
|
|
762
|
+
// read-out backup `cp src/a.ts /tmp/backup.ts` (destination outside) still resolves 'out'.
|
|
763
|
+
const head = sub.split(/<<|>>|>/)[0]; // drop a heredoc/redirect tail
|
|
764
|
+
// Quote-aware tokens: a quoted string is ONE argument even with spaces (Windows W-2 /
|
|
765
|
+
// A-SPEC-463 — a backup to `"C:\Users\Full Name\a.ts"` must not be split into two).
|
|
766
|
+
const rawToks = head.match(/"[^"]*"|'[^']*'|\S+/g) ?? [];
|
|
767
|
+
const unq = (t) => t.replace(/^['"]|['"]$/g, '');
|
|
768
|
+
// @implements A-SPEC-557.1 — judge the LANDING path, not the last token's extension. GNU
|
|
769
|
+
// `-t DIR`/`--target-directory[=]DIR` name an explicit destination directory and make every
|
|
770
|
+
// positional a SOURCE; and a destination that is a directory lands each source at
|
|
771
|
+
// `dir/basename(src)`. Both slipped a code file into the governed tree unjudged before.
|
|
772
|
+
let targetDir;
|
|
773
|
+
const positional = [];
|
|
774
|
+
for (let i = 0; i < rawToks.length; i++) {
|
|
775
|
+
const t = rawToks[i];
|
|
776
|
+
const eq = /^--target-directory=(.+)$/.exec(t);
|
|
777
|
+
if (eq) {
|
|
778
|
+
targetDir = unq(eq[1]);
|
|
779
|
+
continue;
|
|
780
|
+
}
|
|
781
|
+
if (t === '-t' || t === '--target-directory') {
|
|
782
|
+
const n = rawToks[i + 1];
|
|
783
|
+
if (n !== undefined) {
|
|
784
|
+
targetDir = unq(n);
|
|
785
|
+
i++;
|
|
786
|
+
}
|
|
787
|
+
continue;
|
|
788
|
+
}
|
|
789
|
+
if (t.startsWith('-'))
|
|
790
|
+
continue; // any other flag
|
|
791
|
+
positional.push(unq(t));
|
|
792
|
+
}
|
|
793
|
+
const baseName = (s) => s.split(/[/\\]/).pop() ?? s;
|
|
794
|
+
const stripSlash = (s) => s.replace(/[/\\]+$/, '');
|
|
795
|
+
const pushLanding = (destDir, sources) => {
|
|
796
|
+
for (const src of sources)
|
|
797
|
+
if (CODE_TAIL_RE.test(src))
|
|
798
|
+
candidates.push({ raw: `${stripSlash(destDir)}/${baseName(src)}`, base: seg.base });
|
|
799
|
+
};
|
|
800
|
+
const looksLikeDir = (d) => {
|
|
801
|
+
if (/[/\\]$/.test(d))
|
|
802
|
+
return true; // trailing separator = explicit dir
|
|
803
|
+
try {
|
|
804
|
+
const abs = (0, write_target_1.absoluteKindOf)(d) !== null ? d : (seg.base === null ? undefined : (0, write_target_1.pathFlavorFor)(seg.base, d).resolve(seg.base, d));
|
|
805
|
+
return abs !== undefined && fs.statSync(abs).isDirectory();
|
|
806
|
+
}
|
|
807
|
+
catch {
|
|
808
|
+
return false;
|
|
809
|
+
} // non-existent / unreadable ⇒ a file, not a dir
|
|
810
|
+
};
|
|
811
|
+
const argv = positional.slice(1); // positional[0] is the tool name
|
|
812
|
+
if (targetDir !== undefined) {
|
|
813
|
+
pushLanding(targetDir, argv); // -t DIR: every positional is a source
|
|
814
|
+
}
|
|
815
|
+
else if (argv.length >= 2) {
|
|
816
|
+
const dest = argv[argv.length - 1];
|
|
817
|
+
if (CODE_TAIL_RE.test(dest))
|
|
818
|
+
candidates.push({ raw: dest, base: seg.base }); // dest is a code file
|
|
819
|
+
else if (looksLikeDir(dest))
|
|
820
|
+
pushLanding(dest, argv.slice(0, -1)); // dest is a directory
|
|
821
|
+
}
|
|
759
822
|
}
|
|
760
823
|
}
|
|
761
824
|
// @implements A-SPEC-447 — same predicate as the config rule; it lived in two copies, which
|
|
@@ -772,7 +835,12 @@ function evaluateHook(input, specsDir, opts) {
|
|
|
772
835
|
return { permissionDecision: 'deny', permissionDecisionReason: UNRESOLVED_BASE_DENY };
|
|
773
836
|
}
|
|
774
837
|
if (codeVerdicts.includes('in') || codeVerdicts.includes('unknown')) {
|
|
775
|
-
|
|
838
|
+
// @implements A-SPEC-550.1 — governance is "specs EXIST", not "a spec is approved". The old
|
|
839
|
+
// `some(status==='approved')` slept through the authoring window: after `spec_slice_init` and
|
|
840
|
+
// before approval a repo has DRAFT specs, so `cat > test/x.test.js <<EOF` walked through while
|
|
841
|
+
// Write/Edit denied the same unapproved anchor — the exact tool-vs-shell asymmetry reported.
|
|
842
|
+
// A truly spec-less repo still has zero specs here, so ordinary ungoverned coding stays free.
|
|
843
|
+
const governed = readSpecsSync(specsDir).length > 0;
|
|
776
844
|
if (governed) {
|
|
777
845
|
return codeVerdicts.includes('in')
|
|
778
846
|
? { permissionDecision: 'deny', permissionDecisionReason: '[Holmes-Kit] shell write to a project code file bypasses the No-Spec-No-Code gate — use the Write/Edit tools (gated) or supply out-of-band approval' }
|
|
@@ -786,7 +854,7 @@ function evaluateHook(input, specsDir, opts) {
|
|
|
786
854
|
}
|
|
787
855
|
// @implements A-SPEC-191 §27 (round 13) — `PATH_FIELDS` exists because `notebook_path` slipped
|
|
788
856
|
// past a gate that knew only `file_path`, and this line still knew only `file_path`: a valid
|
|
789
|
-
// NotebookEdit was denied in EVERY project with "
|
|
857
|
+
// NotebookEdit was denied in EVERY project with "cannot read the edit target path", which names
|
|
790
858
|
// the wrong fault (the path was there and readable) and blocks a legitimate tool outright.
|
|
791
859
|
const p1 = typeof input.tool_input.file_path === 'string' ? input.tool_input.file_path : undefined;
|
|
792
860
|
const p2 = typeof input.tool_input.notebook_path === 'string' ? input.tool_input.notebook_path : undefined;
|
|
@@ -804,7 +872,7 @@ function evaluateHook(input, specsDir, opts) {
|
|
|
804
872
|
// has to recognise.
|
|
805
873
|
return {
|
|
806
874
|
permissionDecision: 'deny',
|
|
807
|
-
permissionDecisionReason: '[Holmes-Kit]
|
|
875
|
+
permissionDecisionReason: '[Holmes-Kit] cannot read the edit target path, so it cannot be judged (file_path missing or not a string)',
|
|
808
876
|
};
|
|
809
877
|
}
|
|
810
878
|
const writesCode = (0, write_target_1.writesFiles)(input.tool_name, input.tool_input);
|
|
@@ -818,8 +886,8 @@ function evaluateHook(input, specsDir, opts) {
|
|
|
818
886
|
if (writesCode && !p) {
|
|
819
887
|
return {
|
|
820
888
|
permissionDecision: 'deny',
|
|
821
|
-
permissionDecisionReason: '[Holmes-Kit]
|
|
822
|
-
+ ' —
|
|
889
|
+
permissionDecisionReason: '[Holmes-Kit] cannot read the edit target path, so it cannot be judged (file_path missing or not a string)'
|
|
890
|
+
+ ' — there is no way to check whether an unnamed file overwrites an approved spec, so it is not allowed.',
|
|
823
891
|
};
|
|
824
892
|
}
|
|
825
893
|
if (!p || !writesCode)
|
|
@@ -880,9 +948,9 @@ function evaluateHook(input, specsDir, opts) {
|
|
|
880
948
|
// nothing to govern"; an undecidable target is refused, not allowed.
|
|
881
949
|
return {
|
|
882
950
|
permissionDecision: 'deny',
|
|
883
|
-
permissionDecisionReason: '[Holmes-Kit]
|
|
884
|
-
+ '
|
|
885
|
-
+ '
|
|
951
|
+
permissionDecisionReason: '[Holmes-Kit] a network path (UNC) cannot be resolved to a local filesystem identity,'
|
|
952
|
+
+ ' so it cannot be judged inside or outside the project — an untellable target is not allowed.'
|
|
953
|
+
+ ' Put the project on a local drive, or use a drive-letter-mapped path.',
|
|
886
954
|
};
|
|
887
955
|
}
|
|
888
956
|
if (absKind === 'posix' || absKind === 'drive') {
|
|
@@ -918,8 +986,9 @@ function evaluateHook(input, specsDir, opts) {
|
|
|
918
986
|
}
|
|
919
987
|
})();
|
|
920
988
|
const fold = (s) => (caseBlind ? s.toLowerCase() : s);
|
|
921
|
-
//
|
|
922
|
-
//
|
|
989
|
+
// Case-folding applies only to the RESOLVED location — folding the literal-spelling norm and
|
|
990
|
+
// calling it 'inside' lets a link that points OUTSIDE the project enter by name alone. Resolve
|
|
991
|
+
// first, fold on top of that.
|
|
923
992
|
const canonical = inside(norm, projectRoot) ? asProject(norm, projectRoot)
|
|
924
993
|
: inside(realTarget, realRoot) ? asProject(realTarget, realRoot)
|
|
925
994
|
: inside(fold(realTarget), fold(realRoot)) ? asProject(realTarget, realRoot)
|
|
@@ -936,8 +1005,8 @@ function evaluateHook(input, specsDir, opts) {
|
|
|
936
1005
|
if (norm.includes('\u0000')) {
|
|
937
1006
|
return {
|
|
938
1007
|
permissionDecision: 'deny',
|
|
939
|
-
permissionDecisionReason: '[Holmes-Kit]
|
|
940
|
-
+ ' —
|
|
1008
|
+
permissionDecisionReason: '[Holmes-Kit] the edit target path contains a NUL byte, so which file it is cannot be judged'
|
|
1009
|
+
+ ' — a target whose name cannot be pinned down is not allowed.',
|
|
941
1010
|
};
|
|
942
1011
|
}
|
|
943
1012
|
norm = norm.length > 1 ? norm.replace(/\/+$/, '') : norm;
|
|
@@ -964,15 +1033,15 @@ function evaluateHook(input, specsDir, opts) {
|
|
|
964
1033
|
if (rolesResolved === undefined) {
|
|
965
1034
|
return {
|
|
966
1035
|
permissionDecision: 'deny',
|
|
967
|
-
permissionDecisionReason: '[Holmes-Kit]
|
|
968
|
-
+ '
|
|
969
|
-
+ (0, approval_queue_1.queueHint)(opts.projectRoot, { kind: 'config-write', target: norm, why: '
|
|
1036
|
+
permissionDecisionReason: '[Holmes-Kit] the role policy (.ax/roles/) defines the very authority this gate enforces —'
|
|
1037
|
+
+ ' an agent cannot change it itself. An out-of-band approval that includes config-write is required.'
|
|
1038
|
+
+ (0, approval_queue_1.queueHint)(opts.projectRoot, { kind: 'config-write', target: norm, why: 'writing the role policy (.ax/roles/)' }),
|
|
970
1039
|
};
|
|
971
1040
|
}
|
|
972
1041
|
if (rolesResolved.source === 'grant' && rolesResolved.approval.nonce)
|
|
973
1042
|
(0, approval_grants_1.consumeGrantFile)(opts.projectRoot, rolesResolved.approval.nonce);
|
|
974
1043
|
}
|
|
975
|
-
// @implements A-SPEC-191 §22 —
|
|
1044
|
+
// @implements A-SPEC-191 §22 — ask by identity. A spelling regex was defeated by a single in-project link.
|
|
976
1045
|
const configKind = (0, write_target_1.protectedFileKindOf)(opts.projectRoot, norm);
|
|
977
1046
|
// @implements A-SPEC-163 — identity, not spelling. Measured: `.ax//ledger//p.jsonl`,
|
|
978
1047
|
// `.ax/./ledger/p.jsonl` and `.AX/roles/policy.yaml` all reached the protected file while the
|
|
@@ -982,11 +1051,11 @@ function evaluateHook(input, specsDir, opts) {
|
|
|
982
1051
|
// out-of-scope token does not, so config forgery cannot ride a narrow grant.
|
|
983
1052
|
const cfgResolved = (0, approval_grants_1.resolveApproval)(opts.projectRoot, weApproval, { kind: 'config-write', target: norm }, nowTs);
|
|
984
1053
|
if (cfgResolved === undefined) {
|
|
985
|
-
// @implements A-SPEC-193 §8 —
|
|
986
|
-
//
|
|
987
|
-
const kindName = configKind ?? (0, write_target_1.protectedKindOf)(opts.projectRoot, norm) ?? '
|
|
988
|
-
return { permissionDecision: 'deny', permissionDecisionReason: `[Holmes-Kit] ${kindName}
|
|
989
|
-
+ (0, approval_queue_1.queueHint)(opts.projectRoot, { kind: 'config-write', target: norm, why:
|
|
1054
|
+
// @implements A-SPEC-193 §8 — name what was blocked. Baking a list into the sentence makes the
|
|
1055
|
+
// wording false as harnesses grow (REQ-155: a diagnosis that points at the wrong file has negative value).
|
|
1056
|
+
const kindName = configKind ?? (0, write_target_1.protectedKindOf)(opts.projectRoot, norm) ?? 'config';
|
|
1057
|
+
return { permissionDecision: 'deny', permissionDecisionReason: `[Holmes-Kit] ${kindName} is a hook/approval config — it can forge HOLMES_APPROVAL or turn governance off, so an out-of-band approval that includes config-write is required`
|
|
1058
|
+
+ (0, approval_queue_1.queueHint)(opts.projectRoot, { kind: 'config-write', target: norm, why: `writing ${kindName}` }) };
|
|
990
1059
|
}
|
|
991
1060
|
if (cfgResolved.source === 'grant' && cfgResolved.approval.nonce)
|
|
992
1061
|
(0, approval_grants_1.consumeGrantFile)(opts.projectRoot, cfgResolved.approval.nonce);
|
|
@@ -1092,7 +1161,7 @@ function evaluateHook(input, specsDir, opts) {
|
|
|
1092
1161
|
if (violations.length > 0) {
|
|
1093
1162
|
return {
|
|
1094
1163
|
permissionDecision: 'deny',
|
|
1095
|
-
permissionDecisionReason: '[Holmes-Kit]
|
|
1164
|
+
permissionDecisionReason: '[Holmes-Kit] structural-constraint violation — a shape an approved C-SPEC forbids:\n'
|
|
1096
1165
|
+ violations.map((v) => ' - ' + v).join('\n')
|
|
1097
1166
|
+ '\n' + proposed_content_1.CSPEC_GATE_LIMITS,
|
|
1098
1167
|
};
|
|
@@ -1138,7 +1207,7 @@ function evaluateHook(input, specsDir, opts) {
|
|
|
1138
1207
|
// told `id "" fails …`. Splitting loses nothing and can collide with nothing.
|
|
1139
1208
|
// Split on the spec's OWN id at a token boundary. A raw `split(id)` fired mid-token whenever
|
|
1140
1209
|
// the id was a prefix of another id in the same sentence — measured: `A-SPEC-901` whose parent
|
|
1141
|
-
// `A-SPEC-9011` is missing rendered `depends_on "
|
|
1210
|
+
// `A-SPEC-9011` is missing rendered `depends_on "that spec 1" not found`, destroying the one
|
|
1142
1211
|
// fact the author needed. This repository's own dotted ids (`A-SPEC-100.1`) make the prefix
|
|
1143
1212
|
// relation ordinary, not exotic.
|
|
1144
1213
|
const selfRe = (id) => new RegExp(`(?<![\\w.-])${id.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}(?![\\w.-])`, 'g');
|
|
@@ -1168,7 +1237,7 @@ function evaluateHook(input, specsDir, opts) {
|
|
|
1168
1237
|
let reasonLeft = REASON_MAX;
|
|
1169
1238
|
// SKIP, never cut (round-4): truncation mid-sentence sliced spec ids into other (or
|
|
1170
1239
|
// nonexistent) ids, split astral chars, and — worst — one pathological first group zeroed
|
|
1171
|
-
// the budget so later SHORT sentences vanished under a FALSE '
|
|
1240
|
+
// the budget so later SHORT sentences vanished under a FALSE 'truncated' label. clampBlockers'
|
|
1172
1241
|
// discipline instead: an oversized sentence is replaced whole by an honest omission label,
|
|
1173
1242
|
// and fitting sentences keep rendering out of the shared budget.
|
|
1174
1243
|
const OMIT_OVER = ' (reason exceeds text budget — verify spec individually)';
|
|
@@ -1176,7 +1245,7 @@ function evaluateHook(input, specsDir, opts) {
|
|
|
1176
1245
|
const shown0 = groups.slice(0, DISTINCT_REASON_CAP);
|
|
1177
1246
|
// @implements A-SPEC-192 §5R (round 9) — a SHARE, not a race. First-come-full-draw made the
|
|
1178
1247
|
// budget a cliff: one 1,199-character sentence (just under the cap) took the whole allowance
|
|
1179
|
-
// and every short sibling vanished behind '
|
|
1248
|
+
// and every short sibling vanished behind 'budget exhausted', while making that same sentence TEN
|
|
1180
1249
|
// characters LONGER flipped it to the omission label and brought the siblings back — a
|
|
1181
1250
|
// refusal that improves when the input gets worse. Each group is guaranteed its equal share
|
|
1182
1251
|
// first; whatever the short ones do not use is then offered to the rest in order.
|
|
@@ -1308,7 +1377,7 @@ function evaluateHook(input, specsDir, opts) {
|
|
|
1308
1377
|
// Stale-aware gate (rev.1): a target A-SPEC whose seal is PRESENT and BROKEN (edited after
|
|
1309
1378
|
// approval) or STALE (a parent moved) is not a trustworthy approval, so code must not be written
|
|
1310
1379
|
// against it — even though its status still reads 'approved'. The denial names staleness and
|
|
1311
|
-
// spec_approve, deliberately DISTINCT from phaseCheck's "
|
|
1380
|
+
// spec_approve, deliberately DISTINCT from phaseCheck's "… is not approved" (REQ-130's lesson:
|
|
1312
1381
|
// two different failures must not share the same words). An ABSENT seal falls through — the backfill
|
|
1313
1382
|
// closes that window and the Stop hook's unsealed-approval error blocks the turn regardless.
|
|
1314
1383
|
const resolveSpec = (id) => specs.find((s) => s.id === id) ?? null;
|
|
@@ -1322,14 +1391,14 @@ function evaluateHook(input, specsDir, opts) {
|
|
|
1322
1391
|
if (!seal.approvedDigest)
|
|
1323
1392
|
return null; // absent seal: backfill + Stop hook territory
|
|
1324
1393
|
if ((0, spec_digest_1.specDigest)(spec) !== seal.approvedDigest)
|
|
1325
|
-
return `${id}
|
|
1394
|
+
return `${id} was edited after approval (post-approval-edit)`;
|
|
1326
1395
|
for (const pid of spec.dependsOn) {
|
|
1327
1396
|
const parent = resolveSpec(pid);
|
|
1328
1397
|
if (!parent || parent.status !== 'approved')
|
|
1329
1398
|
continue;
|
|
1330
1399
|
const parentSeal = (0, spec_digest_1.sealOf)(parent).approvedDigest;
|
|
1331
1400
|
if (parentSeal && seal.parentDigests[pid] !== parentSeal)
|
|
1332
|
-
return `${id}
|
|
1401
|
+
return `${id}'s parent ${pid} was re-approved, so the chain is stale (stale-parent)`;
|
|
1333
1402
|
}
|
|
1334
1403
|
return null;
|
|
1335
1404
|
};
|
|
@@ -1345,7 +1414,7 @@ function evaluateHook(input, specsDir, opts) {
|
|
|
1345
1414
|
// @implements A-SPEC-133 — the override is authorized by a token covering the code write.
|
|
1346
1415
|
const codeWriteCovered = (0, risk_gate_1.approvalCovers)(weApproval, { kind: 'code-write', target: relPath }, nowTs);
|
|
1347
1416
|
if (staleTarget && !codeWriteCovered) {
|
|
1348
|
-
return { permissionDecision: 'deny', permissionDecisionReason: `[Holmes-Kit] ${staleTarget} — spec_approve
|
|
1417
|
+
return { permissionDecision: 'deny', permissionDecisionReason: `[Holmes-Kit] ${staleTarget} — re-approve with spec_approve, then proceed` };
|
|
1349
1418
|
}
|
|
1350
1419
|
// @implements A-SPEC-152
|
|
1351
1420
|
// Role gate — BEFORE the spec gate on purpose: if this identity may not perform the action at all,
|
|
@@ -1641,7 +1710,7 @@ if (require.main === module) {
|
|
|
1641
1710
|
// @implements A-SPEC-133 — a master-key use becomes an audit line: the approval's
|
|
1642
1711
|
// narrowing (or its absence) travels with the record (r7-191: promised, never shipped).
|
|
1643
1712
|
inputs: [input.tool_name, target,
|
|
1644
|
-
...(unlocked ? [(0, provenance_chain_1.approvalMarkers)(approval)] : []), //
|
|
1713
|
+
...(unlocked ? [(0, provenance_chain_1.approvalMarkers)(approval)] : []), // one item, last position — the marker cannot split and self-contradict
|
|
1645
1714
|
].filter(Boolean),
|
|
1646
1715
|
// round-9: a deny inherited the actor/rationale/authorization of an ambient approval that
|
|
1647
1716
|
// did NOT cover it, so the audit line credited an authorizer who authorized nothing.
|
|
@@ -158,6 +158,23 @@ if (require.main === module) {
|
|
|
158
158
|
out.hookSpecificOutput.additionalContext += notice;
|
|
159
159
|
}
|
|
160
160
|
catch { /* the banner is never a gate */ }
|
|
161
|
+
// @implements A-SPEC-554.1 — remind the human of the current autonomy posture (surface only, the
|
|
162
|
+
// headless hook never decides), and clean up an expired/invalid session marker. Fail-open.
|
|
163
|
+
try {
|
|
164
|
+
const { autonomyBanner, autonomyMarkerShouldPrune, SESSION_AUTONOMY_MARKER } = require('../governance/autonomy');
|
|
165
|
+
const cwd = process.cwd();
|
|
166
|
+
const nowIso = new Date().toISOString();
|
|
167
|
+
const line = autonomyBanner(process.env, cwd, nowIso);
|
|
168
|
+
if (line)
|
|
169
|
+
out.hookSpecificOutput.additionalContext += '\n' + line;
|
|
170
|
+
if (autonomyMarkerShouldPrune(cwd, nowIso)) {
|
|
171
|
+
try {
|
|
172
|
+
fs.rmSync(path.join(cwd, ...SESSION_AUTONOMY_MARKER), { force: true });
|
|
173
|
+
}
|
|
174
|
+
catch { /* housekeeping only */ }
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
catch { /* the banner is never a gate */ }
|
|
161
178
|
process.stdout.write(JSON.stringify({ hookSpecificOutput: out.hookSpecificOutput }));
|
|
162
179
|
if (out.shouldRefresh) {
|
|
163
180
|
// Detached, unref'd child so the session start does not wait on the network. The refresh
|
|
@@ -12,7 +12,7 @@ import type { TestOutcome } from '../review/test-runner';
|
|
|
12
12
|
* or validation errors, it BLOCKS the stop and feeds the findings back so the model must fix them
|
|
13
13
|
* before finishing. No model cooperation required — the governance fires whether or not it is asked.
|
|
14
14
|
*
|
|
15
|
-
* HARDENING H1 (GWT
|
|
15
|
+
* HARDENING H1 (GWT-to-real-test bridge): declared 4-quadrant coverage previously verified only the
|
|
16
16
|
* T-SPEC DOCUMENT. Now, for every approved T-SPEC, its parent A-SPEC must have at least one anchored
|
|
17
17
|
* TEST FILE containing REAL test cases (`it(`/`test(`) — declared coverage over an empty/absent test
|
|
18
18
|
* suite blocks the stop. Evidence is supplied by the CLI (I/O half); evaluateStop stays pure.
|
|
@@ -114,6 +114,27 @@ export declare function changedAnchoredAspecs(root: string): string[] | undefine
|
|
|
114
114
|
* which produces no violation. Failing closed here would block every ungoverned scratch directory.
|
|
115
115
|
*/
|
|
116
116
|
export declare function unanchoredChangedSources(root: string): string[] | undefined;
|
|
117
|
+
export interface DependencyReappraisal {
|
|
118
|
+
file: string;
|
|
119
|
+
aspecs: string[];
|
|
120
|
+
packages: string[];
|
|
121
|
+
}
|
|
122
|
+
export interface ReappraisalIO {
|
|
123
|
+
status?: () => string | undefined;
|
|
124
|
+
show?: (rel: string) => string | undefined;
|
|
125
|
+
read?: (rel: string) => string | undefined;
|
|
126
|
+
}
|
|
127
|
+
export declare function dependencyReappraisals(root: string, isApproved: (id: string) => boolean, io?: ReappraisalIO): DependencyReappraisal[];
|
|
128
|
+
export declare function reappraisalWarningLine(reaps: DependencyReappraisal[]): string | null;
|
|
129
|
+
export declare function escalateReappraisals(root: string, reaps: DependencyReappraisal[], opts: {
|
|
130
|
+
autonomous: boolean;
|
|
131
|
+
enqueue: (root: string, req: {
|
|
132
|
+
kind: string;
|
|
133
|
+
target: string;
|
|
134
|
+
why: string;
|
|
135
|
+
}) => boolean;
|
|
136
|
+
readQueueLines?: (root: string) => string[];
|
|
137
|
+
}): number;
|
|
117
138
|
/**
|
|
118
139
|
* @implements A-SPEC-453
|
|
119
140
|
* ART-5 evidence: approvals that happened without the approving act.
|