@holmes-lab/holmes-kit 0.19.6 → 0.20.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 +143 -0
- package/README.md +9 -2
- package/dist/.build-id +1 -1
- package/dist/holmes/cli/approve-status.d.ts +24 -0
- package/dist/holmes/cli/approve-status.js +166 -0
- package/dist/holmes/cli/approve-watch.d.ts +6 -0
- package/dist/holmes/cli/approve-watch.js +6 -0
- package/dist/holmes/cli/approve.d.ts +27 -0
- package/dist/holmes/cli/approve.js +68 -7
- package/dist/holmes/cli/cli-execution.d.ts +2 -0
- package/dist/holmes/cli/cli-execution.js +24 -0
- package/dist/holmes/cli/doctor-check.d.ts +15 -0
- package/dist/holmes/cli/doctor-check.js +2 -0
- package/dist/holmes/cli/doctor.d.ts +2 -8
- package/dist/holmes/cli/doctor.js +12 -3
- package/dist/holmes/cli/gitignore-merge.d.ts +0 -5
- package/dist/holmes/cli/gitignore-merge.js +6 -2
- package/dist/holmes/cli/index.js +176 -139
- package/dist/holmes/cli/npx-cache-check.d.ts +16 -0
- package/dist/holmes/cli/npx-cache-check.js +130 -0
- package/dist/holmes/cli/playbook-skills.d.ts +2 -22
- package/dist/holmes/cli/playbook-skills.js +10 -54
- package/dist/holmes/cli/probe-process.d.ts +17 -2
- package/dist/holmes/cli/probe-process.js +44 -11
- package/dist/holmes/governance/approval-grants.d.ts +22 -0
- package/dist/holmes/governance/approval-grants.js +55 -1
- package/dist/holmes/governance/approval-queue.d.ts +43 -4
- package/dist/holmes/governance/approval-queue.js +91 -13
- package/dist/holmes/governance/display-id-aliases.d.ts +29 -0
- package/dist/holmes/governance/display-id-aliases.js +86 -0
- package/dist/holmes/governance/ledger-store.d.ts +42 -14
- package/dist/holmes/governance/ledger-store.js +49 -3
- package/dist/holmes/governance/ledger-timeline.d.ts +3 -1
- package/dist/holmes/governance/ledger-timeline.js +11 -1
- package/dist/holmes/governance/provenance-chain.d.ts +2 -0
- package/dist/holmes/governance/provenance-chain.js +13 -2
- package/dist/holmes/governance/provenance-ledger.d.ts +28 -0
- package/dist/holmes/governance/provenance-ledger.js +69 -16
- package/dist/holmes/governance/session-context.d.ts +3 -0
- package/dist/holmes/governance/session-context.js +26 -8
- package/dist/holmes/guardrail/risk-classifier.js +2 -2
- package/dist/holmes/guardrail/write-target.js +2 -1
- package/dist/holmes/hooks/pre-tool-use.js +12 -1
- package/dist/holmes/hooks/stop.js +14 -3
- package/dist/holmes/mcp/handlers/adoption.d.ts +118 -0
- package/dist/holmes/mcp/handlers/adoption.js +186 -0
- package/dist/holmes/mcp/handlers/entity-integration.d.ts +100 -0
- package/dist/holmes/mcp/handlers/entity-integration.js +324 -0
- package/dist/holmes/mcp/handlers/entity-renumber.d.ts +137 -0
- package/dist/holmes/mcp/handlers/entity-renumber.js +228 -0
- package/dist/holmes/mcp/handlers/entity-store.d.ts +78 -0
- package/dist/holmes/mcp/handlers/entity-store.js +212 -0
- package/dist/holmes/mcp/handlers/graph-operations.d.ts +90 -0
- package/dist/holmes/mcp/handlers/graph-operations.js +260 -0
- package/dist/holmes/mcp/handlers/maintenance-analysis.d.ts +34 -0
- package/dist/holmes/mcp/handlers/maintenance-analysis.js +288 -0
- package/dist/holmes/mcp/handlers/maintenance-evidence.d.ts +40 -0
- package/dist/holmes/mcp/handlers/maintenance-evidence.js +104 -0
- package/dist/holmes/mcp/handlers/operator-inspection.d.ts +167 -0
- package/dist/holmes/mcp/handlers/operator-inspection.js +284 -0
- package/dist/holmes/mcp/handlers/phase-queries.d.ts +34 -0
- package/dist/holmes/mcp/handlers/phase-queries.js +86 -0
- package/dist/holmes/mcp/handlers/review-evidence.d.ts +47 -0
- package/dist/holmes/mcp/handlers/review-evidence.js +370 -0
- package/dist/holmes/mcp/handlers/review-queries.d.ts +68 -0
- package/dist/holmes/mcp/handlers/review-queries.js +79 -0
- package/dist/holmes/mcp/handlers/risk-assessment.d.ts +25 -0
- package/dist/holmes/mcp/handlers/risk-assessment.js +181 -0
- package/dist/holmes/mcp/handlers/slice-orchestration.d.ts +72 -0
- package/dist/holmes/mcp/handlers/slice-orchestration.js +373 -0
- package/dist/holmes/mcp/handlers/spec-approval.d.ts +105 -0
- package/dist/holmes/mcp/handlers/spec-approval.js +504 -0
- package/dist/holmes/mcp/handlers/spec-authoring.d.ts +57 -0
- package/dist/holmes/mcp/handlers/spec-authoring.js +326 -0
- package/dist/holmes/mcp/handlers/spec-lifecycle.d.ts +122 -0
- package/dist/holmes/mcp/handlers/spec-lifecycle.js +339 -0
- package/dist/holmes/mcp/handlers/spec-queries.d.ts +30 -0
- package/dist/holmes/mcp/handlers/spec-queries.js +74 -0
- package/dist/holmes/mcp/handlers/taint-screening.d.ts +48 -0
- package/dist/holmes/mcp/handlers/taint-screening.js +183 -0
- package/dist/holmes/mcp/handlers/test-execution.d.ts +63 -0
- package/dist/holmes/mcp/handlers/test-execution.js +210 -0
- package/dist/holmes/mcp/handlers/workspace-identity.d.ts +30 -0
- package/dist/holmes/mcp/handlers/workspace-identity.js +68 -0
- package/dist/holmes/mcp/handlers/workspace-queries.d.ts +77 -0
- package/dist/holmes/mcp/handlers/workspace-queries.js +193 -0
- package/dist/holmes/mcp/handlers.d.ts +661 -424
- package/dist/holmes/mcp/handlers.js +270 -3251
- package/dist/holmes/mcp/server-specs-dir.d.ts +2 -0
- package/dist/holmes/mcp/server-specs-dir.js +71 -0
- package/dist/holmes/mcp/server.js +12 -2
- package/dist/holmes/mcp/tool-schemas.js +43 -2
- package/dist/holmes/project/execution-context.d.ts +17 -0
- package/dist/holmes/project/execution-context.js +121 -0
- package/dist/holmes/project/installer-markers.d.ts +34 -0
- package/dist/holmes/project/installer-markers.js +65 -0
- package/dist/holmes/project/root.d.ts +12 -1
- package/dist/holmes/project/root.js +22 -3
- package/dist/holmes/project/workspace-identity.d.ts +29 -0
- package/dist/holmes/project/workspace-identity.js +181 -0
- package/dist/holmes/review/test-outcomes.d.ts +6 -1
- package/dist/holmes/review/test-outcomes.js +23 -5
- package/dist/holmes/review/test-runner.d.ts +18 -0
- package/dist/holmes/review/test-runner.js +136 -5
- package/dist/holmes/rtm/graph-store.d.ts +50 -1
- package/dist/holmes/rtm/graph-store.js +244 -3
- package/dist/holmes/rtm/incremental.d.ts +1 -0
- package/dist/holmes/rtm/incremental.js +12 -3
- package/dist/holmes/rtm/localize.js +7 -0
- package/dist/holmes/rtm/rtm-graph.d.ts +9 -0
- package/dist/holmes/rtm/rtm-graph.js +13 -0
- package/dist/holmes/rtm/test-scope.js +4 -1
- package/dist/holmes/semantic/vector-cache.d.ts +16 -1
- package/dist/holmes/semantic/vector-cache.js +155 -19
- package/dist/holmes/spec/approval-status.d.ts +10 -0
- package/dist/holmes/spec/approval-status.js +7 -3
- package/dist/holmes/spec/entity-git-snapshot.d.ts +50 -0
- package/dist/holmes/spec/entity-git-snapshot.js +276 -0
- package/dist/holmes/spec/entity-integration-discharge.d.ts +73 -0
- package/dist/holmes/spec/entity-integration-discharge.js +233 -0
- package/dist/holmes/spec/entity-integration-transaction.d.ts +179 -0
- package/dist/holmes/spec/entity-integration-transaction.js +1221 -0
- package/dist/holmes/spec/entity-integration.d.ts +216 -0
- package/dist/holmes/spec/entity-integration.js +760 -0
- package/dist/holmes/spec/entity-renumber-transaction.d.ts +82 -0
- package/dist/holmes/spec/entity-renumber-transaction.js +711 -0
- package/dist/holmes/spec/entity-renumber.d.ts +32 -0
- package/dist/holmes/spec/entity-renumber.js +156 -0
- package/dist/holmes/spec/entity-store.d.ts +135 -0
- package/dist/holmes/spec/entity-store.js +1053 -0
- package/dist/holmes/spec/entity-transaction.d.ts +105 -0
- package/dist/holmes/spec/entity-transaction.js +741 -0
- package/dist/holmes/spec/renumber.d.ts +58 -0
- package/dist/holmes/spec/renumber.js +200 -1
- package/dist/holmes/spec/spec-store.d.ts +3 -2
- package/dist/holmes/spec/spec-store.js +23 -2
- package/dist/holmes/spec/transition-policy.d.ts +36 -0
- package/dist/holmes/spec/transition-policy.js +135 -0
- package/package.json +5 -2
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The doctor's verdict shape, in a module of its own.
|
|
3
|
+
*
|
|
4
|
+
* Measured 2026-09-16: `npx-cache-check.ts` imported `type Check` from `doctor.ts` while `doctor.ts`
|
|
5
|
+
* imported the check — an import cycle (A-SPEC-574.3), red on every POSIX run after the Windows
|
|
6
|
+
* merge. A check module must be able to name the shape it returns without importing the runner.
|
|
7
|
+
*/
|
|
8
|
+
export type Level = 'PASS' | 'WARN' | 'FAIL';
|
|
9
|
+
export interface Check {
|
|
10
|
+
name: string;
|
|
11
|
+
level: Level;
|
|
12
|
+
detail: string;
|
|
13
|
+
fix?: string;
|
|
14
|
+
remediate?: () => boolean;
|
|
15
|
+
}
|
|
@@ -11,14 +11,8 @@ export declare function resolvesToPackage(filePath: string, packageRoot: string)
|
|
|
11
11
|
* hooks fail OPEN, so a broken install is silent: governance simply never fires. This command makes
|
|
12
12
|
* that loud. Every check reports PASS/WARN/FAIL plus a concrete remediation.
|
|
13
13
|
*/
|
|
14
|
-
|
|
15
|
-
export
|
|
16
|
-
name: string;
|
|
17
|
-
level: Level;
|
|
18
|
-
detail: string;
|
|
19
|
-
fix?: string;
|
|
20
|
-
remediate?: () => boolean;
|
|
21
|
-
}
|
|
14
|
+
import type { Check, Level } from './doctor-check';
|
|
15
|
+
export type { Check, Level } from './doctor-check';
|
|
22
16
|
export interface DoctorOptions {
|
|
23
17
|
run?: ProbeRunner;
|
|
24
18
|
autoFix?: boolean;
|
|
@@ -64,6 +64,7 @@ const install_scripts_policy_1 = require("../project/install-scripts-policy");
|
|
|
64
64
|
const native_deps_1 = require("./native-deps");
|
|
65
65
|
const tier_1 = require("../semantic/tier");
|
|
66
66
|
const probe_process_1 = require("./probe-process");
|
|
67
|
+
const npx_cache_check_1 = require("./npx-cache-check");
|
|
67
68
|
const path = __importStar(require("node:path"));
|
|
68
69
|
const role_policy_1 = require("../governance/role-policy");
|
|
69
70
|
const blind_spots_1 = require("../guardrail/blind-spots");
|
|
@@ -515,6 +516,11 @@ async function runDoctor(packageRoot, target, opts, extraChecks) {
|
|
|
515
516
|
// before it answers (a false FAIL this check produced on its first run).
|
|
516
517
|
if (built) {
|
|
517
518
|
checks.push(await mcpHandshakeCheck(packageRoot));
|
|
519
|
+
// @implements A-SPEC-652.2 — the handshake above proves THIS install; the wiring may run a
|
|
520
|
+
// cached npx copy instead, and a corrupt cache entry dies before any handshake (measured
|
|
521
|
+
// 2026-09-15: "Connection closed" at session start, npm ENOENT on the entry's package.json).
|
|
522
|
+
if (target)
|
|
523
|
+
checks.push((0, npx_cache_check_1.checkNpxCache)(target, { env: process.env, homedir: os.homedir(), platform: process.platform, exists: fs.existsSync, readFile: (p) => fs.readFileSync(p, 'utf8'), readdir: (p) => fs.readdirSync(p) }));
|
|
518
524
|
// @implements A-SPEC-323
|
|
519
525
|
// Every wired harness, actually started. The check above proves THIS INSTALL can serve; these
|
|
520
526
|
// prove the wiring each harness will really run reaches a server. Measured: 367ms for three.
|
|
@@ -1340,9 +1346,11 @@ cwd) {
|
|
|
1340
1346
|
return;
|
|
1341
1347
|
done = true;
|
|
1342
1348
|
clearTimeout(timer);
|
|
1343
|
-
|
|
1349
|
+
// @implements A-SPEC-651.2 — the platform the spawn was adapted for is the platform the cleanup
|
|
1350
|
+
// is judged on, and an unconfirmed cleanup names its reason (timeout vs refusal vs no close).
|
|
1351
|
+
void (0, probe_process_1.stopProbeProcess)(child, { platform }).then(released => resolve(released
|
|
1344
1352
|
? { name: 'mcp wiring spawn', level, detail, fix }
|
|
1345
|
-
: { name: 'mcp wiring spawn', level: 'FAIL', detail: `${detail}; probe cleanup could not be confirmed`,
|
|
1353
|
+
: { name: 'mcp wiring spawn', level: 'FAIL', detail: `${detail}; probe cleanup could not be confirmed (${(0, probe_process_1.lastProbeStopReason)() ?? 'unknown'})`,
|
|
1346
1354
|
fix: `${fix ?? ''} Close the remaining diagnostic process for \`${command}\`, then retry doctor.` }));
|
|
1347
1355
|
};
|
|
1348
1356
|
const timer = setTimeout(() => finish('FAIL', `wiring did not answer initialize within ${timeoutMs / 1000}s: \`${quoted}\``, `Re-run \`holmes-kit init\` in the target to rewrite the wiring for \`${command}\`, then re-run doctor.`), timeoutMs);
|
|
@@ -1414,9 +1422,10 @@ function mcpHandshakeCheck(packageRoot, timeoutMs = 15000) {
|
|
|
1414
1422
|
removed = true;
|
|
1415
1423
|
}
|
|
1416
1424
|
catch { /* signal cleanup retains the path for a later attempt */ }
|
|
1425
|
+
const why = !released ? ((0, probe_process_1.lastProbeStopReason)() ?? 'unknown') : 'temp dir not removed';
|
|
1417
1426
|
resolve(released && removed
|
|
1418
1427
|
? { name: 'MCP server', level, detail, fix }
|
|
1419
|
-
: { name: 'MCP server', level: 'FAIL', detail: `${detail}; probe cleanup could not be confirmed`,
|
|
1428
|
+
: { name: 'MCP server', level: 'FAIL', detail: `${detail}; probe cleanup could not be confirmed (${why})`,
|
|
1420
1429
|
fix: 'Close the remaining diagnostic process and retry doctor.' });
|
|
1421
1430
|
});
|
|
1422
1431
|
};
|
|
@@ -1,8 +1,3 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* PURE .gitignore block merge. holmes-kit writes derived state under `.ax/` in the TARGET project
|
|
3
|
-
* (ledger, cpg_cache, reports) that must never be committed. The block is delimited so it can be
|
|
4
|
-
* added, refreshed, and removed exactly — never touching the user's own lines.
|
|
5
|
-
*/
|
|
6
1
|
export declare const BEGIN = "# >>> holmes-kit >>>";
|
|
7
2
|
export declare const END = "# <<< holmes-kit <<<";
|
|
8
3
|
/** Derived/runtime paths holmes-kit creates in a target project. */
|
|
@@ -11,8 +11,9 @@ exports.hasGitignoreBlock = hasGitignoreBlock;
|
|
|
11
11
|
* (ledger, cpg_cache, reports) that must never be committed. The block is delimited so it can be
|
|
12
12
|
* added, refreshed, and removed exactly — never touching the user's own lines.
|
|
13
13
|
*/
|
|
14
|
-
|
|
15
|
-
exports.
|
|
14
|
+
const installer_markers_1 = require("../project/installer-markers");
|
|
15
|
+
exports.BEGIN = installer_markers_1.HOLMES_BLOCK_BEGIN; // A-SPEC-646 — the walk that must ignore this block reads the same definition
|
|
16
|
+
exports.END = installer_markers_1.HOLMES_BLOCK_END;
|
|
16
17
|
/** Derived/runtime paths holmes-kit creates in a target project. */
|
|
17
18
|
exports.IGNORE_LINES = [
|
|
18
19
|
// @implements A-SPEC-148
|
|
@@ -41,6 +42,9 @@ exports.IGNORE_LINES = [
|
|
|
41
42
|
// the human saw" — round-14, exact match). Raw-and-local is the only shape that serves both ADR-012
|
|
42
43
|
// and approvals: the file never leaves the machine, and `holmes-kit approve` reads it right there.
|
|
43
44
|
'.ax/approvals/refusals*.jsonl',
|
|
45
|
+
// @implements A-SPEC-649 — a minted grant is LIVE AUTHORITY for one workspace; committed and
|
|
46
|
+
// pulled, it would open the same door in every clone (measured: a copied grant sealed elsewhere).
|
|
47
|
+
'.ax/approvals/grants/',
|
|
44
48
|
// @implements A-SPEC-193 — 하네스 배선물은 이 **설치본의 절대 경로**를 담는다(훅은 hooks.json
|
|
45
49
|
// 이 있는 디렉터리를 cwd 로 돌고, 패키지가 어디 설치됐는지는 기계마다 다르다). 그 경로가
|
|
46
50
|
// VCS 에 들어가면 다른 기계에서 남의 설치를 부르거나 아무것도 부르지 못한다. 팀과 공유하고
|
package/dist/holmes/cli/index.js
CHANGED
|
@@ -37,7 +37,8 @@ exports.isBrokenPipe = void 0;
|
|
|
37
37
|
exports.packageRoot = packageRoot;
|
|
38
38
|
exports.main = main;
|
|
39
39
|
exports.installPipeGuard = installPipeGuard;
|
|
40
|
-
// @implements A-SPEC-591
|
|
40
|
+
// @implements A-SPEC-591, A-SPEC-626
|
|
41
|
+
const cli_execution_1 = require("./cli-execution");
|
|
41
42
|
// @implements A-SPEC-100.2
|
|
42
43
|
// @implements A-SPEC-213
|
|
43
44
|
// @implements A-SPEC-215
|
|
@@ -79,7 +80,7 @@ const KNOWN_FLAGS = {
|
|
|
79
80
|
skills: ['help', 'target'],
|
|
80
81
|
ci: ['help', 'target', 'specs-dir', 'json'],
|
|
81
82
|
serve: ['help', 'target', 'port'],
|
|
82
|
-
approve: ['help', 'target', 'list', 'refusals', 'grant', 'deny', 'ask', 'reason', 'question', 'ttl', 'rationale', 'watch', 'poll-ms'],
|
|
83
|
+
approve: ['help', 'target', 'list', 'status', 'refusals', 'grant', 'deny', 'ask', 'revoke', 'reason', 'question', 'ttl', 'rationale', 'watch', 'poll-ms'], // A-SPEC-649: --revoke · A-SPEC-650: --status
|
|
83
84
|
ledger: ['help', 'target', 'ref', 'dry-run'],
|
|
84
85
|
// @implements A-SPEC-477 — the human's opt-in act for the cloud semantic tier.
|
|
85
86
|
'semantic-key': ['help'],
|
|
@@ -101,10 +102,12 @@ const APPROVE_USAGE = `holmes-kit approve — decide pending approval requests (
|
|
|
101
102
|
interactive (TTY): per item [a] approve [e] approve with reason/duration [v] details [d] deny [q] ask [s] skip [A] approve all remaining
|
|
102
103
|
--watch resident mode — decide as requests arrive, wait when the queue is empty (Ctrl-C to exit, --poll-ms <ms>)
|
|
103
104
|
--list print the pending list only
|
|
105
|
+
--status the whole picture: workspace, delegation, who asked and how risky, recent outcomes
|
|
104
106
|
--grant <ref> issue a narrow grant for that request (--ttl <minutes>, --rationale <text>)
|
|
105
107
|
ref = list number (1,2,…) | id | id prefix — no need to retype a long id
|
|
106
108
|
--deny <id> --reason <text> deny — the reason rides the agent's next refusal
|
|
107
109
|
--ask <id> --question <text> leave a question and hold — the agent relays it to the user
|
|
110
|
+
--revoke <id> --reason <text> withdraw a live grant — the request must be decided again
|
|
108
111
|
--target <dir> project root (default: current directory)
|
|
109
112
|
`;
|
|
110
113
|
class UnknownFlagError extends Error {
|
|
@@ -115,7 +118,7 @@ class UnknownFlagError extends Error {
|
|
|
115
118
|
* what you'd do" spelling performed a REAL write (round-3 HIGH; same class inverted --remove and
|
|
116
119
|
* --force). A boolean flag must never consume a token.
|
|
117
120
|
*/
|
|
118
|
-
const BOOLEAN_FLAGS = new Set(['help', 'dry-run', 'no-mcp', 'remove', 'force', 'json', 'list', 'watch', 'autonomy', 'no-autonomy']);
|
|
121
|
+
const BOOLEAN_FLAGS = new Set(['help', 'dry-run', 'no-mcp', 'remove', 'force', 'json', 'list', 'status', 'watch', 'autonomy', 'no-autonomy']); // A-SPEC-650: --status
|
|
119
122
|
function parseFlags(argv, known) {
|
|
120
123
|
const flags = {};
|
|
121
124
|
const positionals = [];
|
|
@@ -209,9 +212,11 @@ const USAGE = `holmes-kit — deterministic ASE governance for a project
|
|
|
209
212
|
holmes-kit approve review pending approval requests (HITL)
|
|
210
213
|
(no flags, TTY) interactive: [a]approve [e]approve-with-terms [v]details [d]deny [q]ask [s]skip [A]approve-all
|
|
211
214
|
--list print the pending queue and exit
|
|
215
|
+
--status workspace · delegation · per-request risk/run/subject · recent outcomes
|
|
212
216
|
--grant <ref> [--ttl <min>] [--rationale <text>] mint the narrow grant for one request
|
|
213
217
|
--deny <ref> --reason <text> record a denial the agent will see
|
|
214
218
|
--ask <ref> --question <text> hold with a question the agent relays
|
|
219
|
+
--revoke <id> --reason <text> withdraw a live grant (full id or unique prefix)
|
|
215
220
|
<ref> list index (1,2,…), full id, or a unique id prefix
|
|
216
221
|
--target <dir> project root (default: cwd)
|
|
217
222
|
|
|
@@ -331,13 +336,15 @@ async function main(argv) {
|
|
|
331
336
|
const { runAutonomy } = await Promise.resolve().then(() => __importStar(require('./autonomy')));
|
|
332
337
|
const root = path.resolve(typeof flags.target === 'string' ? flags.target : process.cwd());
|
|
333
338
|
const forStr = typeof flags.for === 'string' ? flags.for : undefined;
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
339
|
+
return (0, cli_execution_1.runOperatorExecution)(root, () => {
|
|
340
|
+
const r = runAutonomy(root, positionals[0], forStr, {
|
|
341
|
+
now: new Date().toISOString(),
|
|
342
|
+
isTTY: !!process.stdin.isTTY && !!process.stdout.isTTY,
|
|
343
|
+
approval: readApproval(),
|
|
344
|
+
});
|
|
345
|
+
process[r.ok ? 'stdout' : 'stderr'].write(`${r.message}\n`);
|
|
346
|
+
return r.exitCode;
|
|
338
347
|
});
|
|
339
|
-
process[r.ok ? 'stdout' : 'stderr'].write(`${r.message}\n`);
|
|
340
|
-
return r.exitCode;
|
|
341
348
|
}
|
|
342
349
|
if (cmd === 'doctor') {
|
|
343
350
|
// A nonexistent target read as "healthy but unwired" and both advised remedies then refused it
|
|
@@ -799,158 +806,188 @@ async function main(argv) {
|
|
|
799
806
|
process.stdout.write(APPROVE_USAGE);
|
|
800
807
|
return 0;
|
|
801
808
|
}
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
809
|
+
return (0, cli_execution_1.runOperatorExecution)(root, async () => {
|
|
810
|
+
if (typeof flags.grant === 'string') {
|
|
811
|
+
const opts = { actor };
|
|
812
|
+
if (typeof flags.ttl === 'string')
|
|
813
|
+
opts.ttlMinutes = Number(flags.ttl);
|
|
814
|
+
if (typeof flags.rationale === 'string')
|
|
815
|
+
opts.rationale = flags.rationale;
|
|
816
|
+
const ref = resolveRef(flags.grant);
|
|
817
|
+
if ('refusal' in ref) {
|
|
818
|
+
process.stdout.write(`\u2717 ${ref.refusal}\n`);
|
|
819
|
+
return 1;
|
|
820
|
+
}
|
|
821
|
+
const r = grantRequest(root, ref.id, opts);
|
|
822
|
+
process.stdout.write(r.ok ? `✓ approved — ${ref.subject('✓ approved — ')}\n${ref.detail(`valid until ${r.expires}`)}\n` : `✗ ${r.reason}\n`);
|
|
823
|
+
return r.ok ? 0 : 1;
|
|
812
824
|
}
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
825
|
+
if (typeof flags.deny === 'string') {
|
|
826
|
+
const reason = typeof flags.reason === 'string' ? flags.reason.trim() : '';
|
|
827
|
+
if (reason === '') {
|
|
828
|
+
process.stderr.write('✗ --deny requires --reason — a denial with no reason is invisible to the agent\n');
|
|
829
|
+
return 1;
|
|
830
|
+
}
|
|
831
|
+
const ref = resolveRef(flags.deny);
|
|
832
|
+
if ('refusal' in ref) {
|
|
833
|
+
process.stdout.write(`\u2717 ${ref.refusal}\n`);
|
|
834
|
+
return 1;
|
|
835
|
+
}
|
|
836
|
+
const r = denyRequest(root, ref.id, reason, actor);
|
|
837
|
+
process.stdout.write(r.ok ? `✓ denied — ${ref.subject('✓ denied — ')}\n${ref.detail('the denial was recorded')}\n` : `✗ ${r.reason}\n`);
|
|
838
|
+
return r.ok ? 0 : 1;
|
|
822
839
|
}
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
840
|
+
// @implements A-SPEC-649 — the operator withdraws a live grant. A granted id is no longer
|
|
841
|
+
// pending, so it resolves against the DECIDED ids (full id or a unique prefix), never a list index.
|
|
842
|
+
if (typeof flags.revoke === 'string') {
|
|
843
|
+
const reason = typeof flags.reason === 'string' ? flags.reason.trim() : '';
|
|
844
|
+
if (reason === '') {
|
|
845
|
+
process.stderr.write('✗ --revoke requires --reason — a revocation with no reason is invisible to the agent\n');
|
|
846
|
+
return 1;
|
|
847
|
+
}
|
|
848
|
+
const { revokeGrant } = require('./approve');
|
|
849
|
+
const ref = String(flags.revoke).trim();
|
|
850
|
+
const granted = Object.entries(readQueue(root, { includeAllKinds: true }).decisions).filter(([, d]) => d.event === 'granted').map(([id]) => id);
|
|
851
|
+
const matches = granted.includes(ref) ? [ref] : granted.filter((id) => id.startsWith(ref));
|
|
852
|
+
if (matches.length !== 1) {
|
|
853
|
+
process.stdout.write(`\u2717 ${matches.length === 0 ? `no granted request matches ${ref}` : `${ref} matches ${matches.length} granted requests — use the full id`}\n`);
|
|
854
|
+
return 1;
|
|
855
|
+
}
|
|
856
|
+
const r = revokeGrant(root, matches[0], reason, actor);
|
|
857
|
+
process.stdout.write(r.ok ? `✓ revoked — ${matches[0]}\n the grant is withdrawn; the agent's next attempt is refused and re-filed\n` : `✗ ${r.reason}\n`);
|
|
858
|
+
return r.ok ? 0 : 1;
|
|
827
859
|
}
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
860
|
+
if (typeof flags.ask === 'string') {
|
|
861
|
+
const question = typeof flags.question === 'string' ? flags.question.trim() : '';
|
|
862
|
+
if (question === '') {
|
|
863
|
+
process.stderr.write('✗ --ask requires --question\n');
|
|
864
|
+
return 1;
|
|
865
|
+
}
|
|
866
|
+
const ref = resolveRef(flags.ask);
|
|
867
|
+
if ('refusal' in ref) {
|
|
868
|
+
process.stdout.write(`\u2717 ${ref.refusal}\n`);
|
|
869
|
+
return 1;
|
|
870
|
+
}
|
|
871
|
+
const r = holdRequest(root, ref.id, question, actor);
|
|
872
|
+
process.stdout.write(r.ok ? `✓ held — ${ref.subject('✓ held — ')}\n${ref.detail('the question rides the next refusal')}\n` : `✗ ${r.reason}\n`);
|
|
873
|
+
return r.ok ? 0 : 1;
|
|
837
874
|
}
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
875
|
+
// @implements A-SPEC-507.1 — stale folding is a LIST-VIEW judgment: the clock enters only
|
|
876
|
+
// here, never on gate-path folds. `--stale-hours 0` disables it; `--all` unfolds.
|
|
877
|
+
const { staleView } = require('./approve');
|
|
878
|
+
const stale = staleView(flags);
|
|
879
|
+
const queueOpts = stale ? { now: Date.now(), ttlMs: stale.ttlMs } : undefined;
|
|
880
|
+
const listView = stale ? { staleHours: stale.ttlMs / 3_600_000, showAll: stale.showAll } : undefined;
|
|
881
|
+
if (flags.list) {
|
|
882
|
+
process.stdout.write(renderPending(readQueue(root, queueOpts), listView) + '\n');
|
|
883
|
+
return 0;
|
|
842
884
|
}
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
// here, never on gate-path folds. `--stale-hours 0` disables it; `--all` unfolds.
|
|
849
|
-
const { staleView } = require('./approve');
|
|
850
|
-
const stale = staleView(flags);
|
|
851
|
-
const queueOpts = stale ? { now: Date.now(), ttlMs: stale.ttlMs } : undefined;
|
|
852
|
-
const listView = stale ? { staleHours: stale.ttlMs / 3_600_000, showAll: stale.showAll } : undefined;
|
|
853
|
-
if (flags.list) {
|
|
854
|
-
process.stdout.write(renderPending(readQueue(root, queueOpts), listView) + '\n');
|
|
855
|
-
return 0;
|
|
856
|
-
}
|
|
857
|
-
// @implements A-SPEC-563.2 — the read-only refusal view (local log; raw targets stay on-screen).
|
|
858
|
-
if (flags.refusals !== undefined) {
|
|
859
|
-
const { renderRefusals } = require('./approve');
|
|
860
|
-
const { readRefusals } = require('../governance/approval-queue');
|
|
861
|
-
const limit = typeof flags.refusals === 'string' ? Number(flags.refusals) : 20;
|
|
862
|
-
if (!Number.isInteger(limit) || limit <= 0) {
|
|
863
|
-
process.stderr.write(`✗ --refusals expects a positive integer count — received: ${flags.refusals}\n`);
|
|
864
|
-
return 1;
|
|
885
|
+
// @implements A-SPEC-650 — the whole picture, read-only; the list above stays script-clean.
|
|
886
|
+
if (flags.status) {
|
|
887
|
+
const { renderStatus, statusContext } = require('./approve-status');
|
|
888
|
+
process.stdout.write(renderStatus(readQueue(root), statusContext(root)) + '\n');
|
|
889
|
+
return 0;
|
|
865
890
|
}
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
891
|
+
// @implements A-SPEC-563.2 — the read-only refusal view (local log; raw targets stay on-screen).
|
|
892
|
+
if (flags.refusals !== undefined) {
|
|
893
|
+
const { renderRefusals } = require('./approve');
|
|
894
|
+
const { readRefusals } = require('../governance/approval-queue');
|
|
895
|
+
const limit = typeof flags.refusals === 'string' ? Number(flags.refusals) : 20;
|
|
896
|
+
if (!Number.isInteger(limit) || limit <= 0) {
|
|
897
|
+
process.stderr.write(`✗ --refusals expects a positive integer count — received: ${flags.refusals}\n`);
|
|
898
|
+
return 1;
|
|
899
|
+
}
|
|
900
|
+
process.stdout.write(renderRefusals(readRefusals(root), limit) + '\n');
|
|
901
|
+
return 0;
|
|
902
|
+
}
|
|
903
|
+
// @implements A-SPEC-262.2 — the RESIDENT surface. Validate --poll-ms first (so a bad value is
|
|
904
|
+
// named even on a non-TTY), then refuse a non-TTY (a resident interactive loop needs a terminal),
|
|
905
|
+
// then run the loop. SIGINT and readline close both abort it — the operator leaving, either way,
|
|
906
|
+
// ends the watch at once and exit 0; pending items stay in the queue.
|
|
907
|
+
if (flags.watch === true) {
|
|
908
|
+
let pollMs;
|
|
909
|
+
if (typeof flags['poll-ms'] === 'string') {
|
|
910
|
+
pollMs = Number(flags['poll-ms']);
|
|
911
|
+
if (!Number.isInteger(pollMs) || pollMs <= 0) {
|
|
912
|
+
process.stderr.write(`✗ --poll-ms must be a positive integer (milliseconds) — received: ${flags['poll-ms']}\n`);
|
|
913
|
+
return 1;
|
|
914
|
+
}
|
|
915
|
+
}
|
|
916
|
+
if (!process.stdin.isTTY) {
|
|
917
|
+
process.stderr.write('✗ interactive resident mode (--watch) requires a TTY — use --list in scripts\n');
|
|
879
918
|
return 1;
|
|
880
919
|
}
|
|
920
|
+
// @implements A-SPEC-497.2 — the resident surface decides on ONE raw keypress (a/d/q/s, no
|
|
921
|
+
// Enter, no deadline) and pings the OS on each new arrival. Raw mode is restored on the way
|
|
922
|
+
// out whatever happens; Ctrl-C arrives as \x03 through decideByKey and ends the loop.
|
|
923
|
+
const { runWatch } = require('./approve-watch');
|
|
924
|
+
const { notifyQueueArrival } = require('./os-notify');
|
|
925
|
+
const ac = new AbortController();
|
|
926
|
+
const onSig = () => ac.abort();
|
|
927
|
+
process.once('SIGINT', onSig);
|
|
928
|
+
const stdin = process.stdin;
|
|
929
|
+
try {
|
|
930
|
+
stdin.setRawMode?.(true);
|
|
931
|
+
stdin.resume();
|
|
932
|
+
await runWatch(root, {
|
|
933
|
+
print: (t) => process.stdout.write(t + '\n'),
|
|
934
|
+
ask: async () => '', // unused on the key surface; kept for the ApproveIO shape
|
|
935
|
+
}, actor, {
|
|
936
|
+
signal: ac.signal,
|
|
937
|
+
keyStream: stdin,
|
|
938
|
+
notify: (p) => notifyQueueArrival(p),
|
|
939
|
+
// @implements A-SPEC-650 — the status header before each new-item pass.
|
|
940
|
+
status: () => { const { renderStatus, statusContext } = require('./approve-status'); return renderStatus(readQueue(root), statusContext(root)); },
|
|
941
|
+
...(pollMs !== undefined ? { pollMs } : {}),
|
|
942
|
+
});
|
|
943
|
+
}
|
|
944
|
+
catch (e) {
|
|
945
|
+
if (e?.name !== 'AbortError')
|
|
946
|
+
throw e;
|
|
947
|
+
}
|
|
948
|
+
finally {
|
|
949
|
+
process.removeListener('SIGINT', onSig);
|
|
950
|
+
stdin.setRawMode?.(false);
|
|
951
|
+
stdin.pause();
|
|
952
|
+
}
|
|
953
|
+
return 0;
|
|
881
954
|
}
|
|
955
|
+
// @implements A-SPEC-260 — the implicit non-TTY fallback tells the operator the next command,
|
|
956
|
+
// with the real pending id filled in; the explicit --list above stays script-clean.
|
|
882
957
|
if (!process.stdin.isTTY) {
|
|
883
|
-
|
|
884
|
-
|
|
958
|
+
const state = readQueue(root, queueOpts);
|
|
959
|
+
const hint = renderNonTtyHint(state);
|
|
960
|
+
process.stdout.write(renderPending(state, listView) + (hint ? '\n\n' + hint : '') + '\n');
|
|
961
|
+
return 0;
|
|
885
962
|
}
|
|
886
|
-
// @implements A-SPEC-
|
|
887
|
-
//
|
|
888
|
-
//
|
|
889
|
-
|
|
890
|
-
|
|
963
|
+
// @implements A-SPEC-262.1
|
|
964
|
+
// AWAITED, not fire-and-forget. Measured 2026-08-25 against the shipped bin with a real pty: the
|
|
965
|
+
// prompt printed and the session ended with zero grants, because `void runInteractive(...)`
|
|
966
|
+
// returned 0 immediately and bin/holmes-kit.js turns that into `process.exit(0)` — killing the
|
|
967
|
+
// loop before the first keypress. The decision surface REQ-246 shipped had never run for a real
|
|
968
|
+
// operator; every test called `runInteractive` directly and so never crossed this seam.
|
|
969
|
+
const rl = require('node:readline/promises').createInterface({ input: process.stdin, output: process.stdout });
|
|
970
|
+
// EOF is the operator LEAVING (Ctrl-D, a closed pipe). Node leaves a pending `question`
|
|
971
|
+
// unsettled when the interface closes, so without this the awaited loop would hang the command
|
|
972
|
+
// forever — measured 2026-08-25, a pty probe hung for five minutes. The abort ends the session
|
|
973
|
+
// the way the operator asked: quietly, exit 0, nothing decided that was not decided.
|
|
891
974
|
const ac = new AbortController();
|
|
892
|
-
|
|
893
|
-
process.once('SIGINT', onSig);
|
|
894
|
-
const stdin = process.stdin;
|
|
975
|
+
rl.once('close', () => ac.abort());
|
|
895
976
|
try {
|
|
896
|
-
|
|
897
|
-
stdin.resume();
|
|
898
|
-
await runWatch(root, {
|
|
977
|
+
await runInteractive(root, {
|
|
899
978
|
print: (t) => process.stdout.write(t + '\n'),
|
|
900
|
-
ask:
|
|
901
|
-
}, actor
|
|
902
|
-
signal: ac.signal,
|
|
903
|
-
keyStream: stdin,
|
|
904
|
-
notify: (p) => notifyQueueArrival(p),
|
|
905
|
-
...(pollMs !== undefined ? { pollMs } : {}),
|
|
906
|
-
});
|
|
979
|
+
ask: (q) => rl.question(q, { signal: ac.signal }),
|
|
980
|
+
}, actor);
|
|
907
981
|
}
|
|
908
982
|
catch (e) {
|
|
909
983
|
if (e?.name !== 'AbortError')
|
|
910
984
|
throw e;
|
|
911
985
|
}
|
|
912
986
|
finally {
|
|
913
|
-
|
|
914
|
-
stdin.setRawMode?.(false);
|
|
915
|
-
stdin.pause();
|
|
987
|
+
rl.close();
|
|
916
988
|
}
|
|
917
989
|
return 0;
|
|
918
|
-
}
|
|
919
|
-
// @implements A-SPEC-260 — the implicit non-TTY fallback tells the operator the next command,
|
|
920
|
-
// with the real pending id filled in; the explicit --list above stays script-clean.
|
|
921
|
-
if (!process.stdin.isTTY) {
|
|
922
|
-
const state = readQueue(root, queueOpts);
|
|
923
|
-
const hint = renderNonTtyHint(state);
|
|
924
|
-
process.stdout.write(renderPending(state, listView) + (hint ? '\n\n' + hint : '') + '\n');
|
|
925
|
-
return 0;
|
|
926
|
-
}
|
|
927
|
-
// @implements A-SPEC-262.1
|
|
928
|
-
// AWAITED, not fire-and-forget. Measured 2026-08-25 against the shipped bin with a real pty: the
|
|
929
|
-
// prompt printed and the session ended with zero grants, because `void runInteractive(...)`
|
|
930
|
-
// returned 0 immediately and bin/holmes-kit.js turns that into `process.exit(0)` — killing the
|
|
931
|
-
// loop before the first keypress. The decision surface REQ-246 shipped had never run for a real
|
|
932
|
-
// operator; every test called `runInteractive` directly and so never crossed this seam.
|
|
933
|
-
const rl = require('node:readline/promises').createInterface({ input: process.stdin, output: process.stdout });
|
|
934
|
-
// EOF is the operator LEAVING (Ctrl-D, a closed pipe). Node leaves a pending `question`
|
|
935
|
-
// unsettled when the interface closes, so without this the awaited loop would hang the command
|
|
936
|
-
// forever — measured 2026-08-25, a pty probe hung for five minutes. The abort ends the session
|
|
937
|
-
// the way the operator asked: quietly, exit 0, nothing decided that was not decided.
|
|
938
|
-
const ac = new AbortController();
|
|
939
|
-
rl.once('close', () => ac.abort());
|
|
940
|
-
try {
|
|
941
|
-
await runInteractive(root, {
|
|
942
|
-
print: (t) => process.stdout.write(t + '\n'),
|
|
943
|
-
ask: (q) => rl.question(q, { signal: ac.signal }),
|
|
944
|
-
}, actor);
|
|
945
|
-
}
|
|
946
|
-
catch (e) {
|
|
947
|
-
if (e?.name !== 'AbortError')
|
|
948
|
-
throw e;
|
|
949
|
-
}
|
|
950
|
-
finally {
|
|
951
|
-
rl.close();
|
|
952
|
-
}
|
|
953
|
-
return 0;
|
|
990
|
+
});
|
|
954
991
|
}
|
|
955
992
|
if (cmd === 'serve') {
|
|
956
993
|
if (typeof flags.target === 'string') {
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { Check } from './doctor-check';
|
|
2
|
+
export interface NpxCacheDeps {
|
|
3
|
+
env: NodeJS.ProcessEnv;
|
|
4
|
+
homedir: string;
|
|
5
|
+
platform: NodeJS.Platform;
|
|
6
|
+
exists: (p: string) => boolean;
|
|
7
|
+
readFile: (p: string) => string;
|
|
8
|
+
readdir: (p: string) => string[];
|
|
9
|
+
}
|
|
10
|
+
/** The `-p <pkg>@<ver>` pin of the target's holmes-kit wiring, or null when it is not an npx pin. */
|
|
11
|
+
export declare function npxPinOf(target: string, deps: Pick<NpxCacheDeps, 'exists' | 'readFile'>): {
|
|
12
|
+
pkg: string;
|
|
13
|
+
version: string;
|
|
14
|
+
} | null;
|
|
15
|
+
export declare function npmCacheRoot(deps: Pick<NpxCacheDeps, 'env' | 'homedir' | 'platform'>): string;
|
|
16
|
+
export declare function checkNpxCache(target: string, deps: NpxCacheDeps): Check;
|