@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.
Files changed (139) hide show
  1. package/CHANGELOG.md +143 -0
  2. package/README.md +9 -2
  3. package/dist/.build-id +1 -1
  4. package/dist/holmes/cli/approve-status.d.ts +24 -0
  5. package/dist/holmes/cli/approve-status.js +166 -0
  6. package/dist/holmes/cli/approve-watch.d.ts +6 -0
  7. package/dist/holmes/cli/approve-watch.js +6 -0
  8. package/dist/holmes/cli/approve.d.ts +27 -0
  9. package/dist/holmes/cli/approve.js +68 -7
  10. package/dist/holmes/cli/cli-execution.d.ts +2 -0
  11. package/dist/holmes/cli/cli-execution.js +24 -0
  12. package/dist/holmes/cli/doctor-check.d.ts +15 -0
  13. package/dist/holmes/cli/doctor-check.js +2 -0
  14. package/dist/holmes/cli/doctor.d.ts +2 -8
  15. package/dist/holmes/cli/doctor.js +12 -3
  16. package/dist/holmes/cli/gitignore-merge.d.ts +0 -5
  17. package/dist/holmes/cli/gitignore-merge.js +6 -2
  18. package/dist/holmes/cli/index.js +176 -139
  19. package/dist/holmes/cli/npx-cache-check.d.ts +16 -0
  20. package/dist/holmes/cli/npx-cache-check.js +130 -0
  21. package/dist/holmes/cli/playbook-skills.d.ts +2 -22
  22. package/dist/holmes/cli/playbook-skills.js +10 -54
  23. package/dist/holmes/cli/probe-process.d.ts +17 -2
  24. package/dist/holmes/cli/probe-process.js +44 -11
  25. package/dist/holmes/governance/approval-grants.d.ts +22 -0
  26. package/dist/holmes/governance/approval-grants.js +55 -1
  27. package/dist/holmes/governance/approval-queue.d.ts +43 -4
  28. package/dist/holmes/governance/approval-queue.js +91 -13
  29. package/dist/holmes/governance/display-id-aliases.d.ts +29 -0
  30. package/dist/holmes/governance/display-id-aliases.js +86 -0
  31. package/dist/holmes/governance/ledger-store.d.ts +42 -14
  32. package/dist/holmes/governance/ledger-store.js +49 -3
  33. package/dist/holmes/governance/ledger-timeline.d.ts +3 -1
  34. package/dist/holmes/governance/ledger-timeline.js +11 -1
  35. package/dist/holmes/governance/provenance-chain.d.ts +2 -0
  36. package/dist/holmes/governance/provenance-chain.js +13 -2
  37. package/dist/holmes/governance/provenance-ledger.d.ts +28 -0
  38. package/dist/holmes/governance/provenance-ledger.js +69 -16
  39. package/dist/holmes/governance/session-context.d.ts +3 -0
  40. package/dist/holmes/governance/session-context.js +26 -8
  41. package/dist/holmes/guardrail/risk-classifier.js +2 -2
  42. package/dist/holmes/guardrail/write-target.js +2 -1
  43. package/dist/holmes/hooks/pre-tool-use.js +12 -1
  44. package/dist/holmes/hooks/stop.js +14 -3
  45. package/dist/holmes/mcp/handlers/adoption.d.ts +118 -0
  46. package/dist/holmes/mcp/handlers/adoption.js +186 -0
  47. package/dist/holmes/mcp/handlers/entity-integration.d.ts +100 -0
  48. package/dist/holmes/mcp/handlers/entity-integration.js +324 -0
  49. package/dist/holmes/mcp/handlers/entity-renumber.d.ts +137 -0
  50. package/dist/holmes/mcp/handlers/entity-renumber.js +228 -0
  51. package/dist/holmes/mcp/handlers/entity-store.d.ts +78 -0
  52. package/dist/holmes/mcp/handlers/entity-store.js +212 -0
  53. package/dist/holmes/mcp/handlers/graph-operations.d.ts +90 -0
  54. package/dist/holmes/mcp/handlers/graph-operations.js +260 -0
  55. package/dist/holmes/mcp/handlers/maintenance-analysis.d.ts +34 -0
  56. package/dist/holmes/mcp/handlers/maintenance-analysis.js +288 -0
  57. package/dist/holmes/mcp/handlers/maintenance-evidence.d.ts +40 -0
  58. package/dist/holmes/mcp/handlers/maintenance-evidence.js +104 -0
  59. package/dist/holmes/mcp/handlers/operator-inspection.d.ts +167 -0
  60. package/dist/holmes/mcp/handlers/operator-inspection.js +284 -0
  61. package/dist/holmes/mcp/handlers/phase-queries.d.ts +34 -0
  62. package/dist/holmes/mcp/handlers/phase-queries.js +86 -0
  63. package/dist/holmes/mcp/handlers/review-evidence.d.ts +47 -0
  64. package/dist/holmes/mcp/handlers/review-evidence.js +370 -0
  65. package/dist/holmes/mcp/handlers/review-queries.d.ts +68 -0
  66. package/dist/holmes/mcp/handlers/review-queries.js +79 -0
  67. package/dist/holmes/mcp/handlers/risk-assessment.d.ts +25 -0
  68. package/dist/holmes/mcp/handlers/risk-assessment.js +181 -0
  69. package/dist/holmes/mcp/handlers/slice-orchestration.d.ts +72 -0
  70. package/dist/holmes/mcp/handlers/slice-orchestration.js +373 -0
  71. package/dist/holmes/mcp/handlers/spec-approval.d.ts +105 -0
  72. package/dist/holmes/mcp/handlers/spec-approval.js +504 -0
  73. package/dist/holmes/mcp/handlers/spec-authoring.d.ts +57 -0
  74. package/dist/holmes/mcp/handlers/spec-authoring.js +326 -0
  75. package/dist/holmes/mcp/handlers/spec-lifecycle.d.ts +122 -0
  76. package/dist/holmes/mcp/handlers/spec-lifecycle.js +339 -0
  77. package/dist/holmes/mcp/handlers/spec-queries.d.ts +30 -0
  78. package/dist/holmes/mcp/handlers/spec-queries.js +74 -0
  79. package/dist/holmes/mcp/handlers/taint-screening.d.ts +48 -0
  80. package/dist/holmes/mcp/handlers/taint-screening.js +183 -0
  81. package/dist/holmes/mcp/handlers/test-execution.d.ts +63 -0
  82. package/dist/holmes/mcp/handlers/test-execution.js +210 -0
  83. package/dist/holmes/mcp/handlers/workspace-identity.d.ts +30 -0
  84. package/dist/holmes/mcp/handlers/workspace-identity.js +68 -0
  85. package/dist/holmes/mcp/handlers/workspace-queries.d.ts +77 -0
  86. package/dist/holmes/mcp/handlers/workspace-queries.js +193 -0
  87. package/dist/holmes/mcp/handlers.d.ts +661 -424
  88. package/dist/holmes/mcp/handlers.js +270 -3251
  89. package/dist/holmes/mcp/server-specs-dir.d.ts +2 -0
  90. package/dist/holmes/mcp/server-specs-dir.js +71 -0
  91. package/dist/holmes/mcp/server.js +12 -2
  92. package/dist/holmes/mcp/tool-schemas.js +43 -2
  93. package/dist/holmes/project/execution-context.d.ts +17 -0
  94. package/dist/holmes/project/execution-context.js +121 -0
  95. package/dist/holmes/project/installer-markers.d.ts +34 -0
  96. package/dist/holmes/project/installer-markers.js +65 -0
  97. package/dist/holmes/project/root.d.ts +12 -1
  98. package/dist/holmes/project/root.js +22 -3
  99. package/dist/holmes/project/workspace-identity.d.ts +29 -0
  100. package/dist/holmes/project/workspace-identity.js +181 -0
  101. package/dist/holmes/review/test-outcomes.d.ts +6 -1
  102. package/dist/holmes/review/test-outcomes.js +23 -5
  103. package/dist/holmes/review/test-runner.d.ts +18 -0
  104. package/dist/holmes/review/test-runner.js +136 -5
  105. package/dist/holmes/rtm/graph-store.d.ts +50 -1
  106. package/dist/holmes/rtm/graph-store.js +244 -3
  107. package/dist/holmes/rtm/incremental.d.ts +1 -0
  108. package/dist/holmes/rtm/incremental.js +12 -3
  109. package/dist/holmes/rtm/localize.js +7 -0
  110. package/dist/holmes/rtm/rtm-graph.d.ts +9 -0
  111. package/dist/holmes/rtm/rtm-graph.js +13 -0
  112. package/dist/holmes/rtm/test-scope.js +4 -1
  113. package/dist/holmes/semantic/vector-cache.d.ts +16 -1
  114. package/dist/holmes/semantic/vector-cache.js +155 -19
  115. package/dist/holmes/spec/approval-status.d.ts +10 -0
  116. package/dist/holmes/spec/approval-status.js +7 -3
  117. package/dist/holmes/spec/entity-git-snapshot.d.ts +50 -0
  118. package/dist/holmes/spec/entity-git-snapshot.js +276 -0
  119. package/dist/holmes/spec/entity-integration-discharge.d.ts +73 -0
  120. package/dist/holmes/spec/entity-integration-discharge.js +233 -0
  121. package/dist/holmes/spec/entity-integration-transaction.d.ts +179 -0
  122. package/dist/holmes/spec/entity-integration-transaction.js +1221 -0
  123. package/dist/holmes/spec/entity-integration.d.ts +216 -0
  124. package/dist/holmes/spec/entity-integration.js +760 -0
  125. package/dist/holmes/spec/entity-renumber-transaction.d.ts +82 -0
  126. package/dist/holmes/spec/entity-renumber-transaction.js +711 -0
  127. package/dist/holmes/spec/entity-renumber.d.ts +32 -0
  128. package/dist/holmes/spec/entity-renumber.js +156 -0
  129. package/dist/holmes/spec/entity-store.d.ts +135 -0
  130. package/dist/holmes/spec/entity-store.js +1053 -0
  131. package/dist/holmes/spec/entity-transaction.d.ts +105 -0
  132. package/dist/holmes/spec/entity-transaction.js +741 -0
  133. package/dist/holmes/spec/renumber.d.ts +58 -0
  134. package/dist/holmes/spec/renumber.js +200 -1
  135. package/dist/holmes/spec/spec-store.d.ts +3 -2
  136. package/dist/holmes/spec/spec-store.js +23 -2
  137. package/dist/holmes/spec/transition-policy.d.ts +36 -0
  138. package/dist/holmes/spec/transition-policy.js +135 -0
  139. 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
+ }
@@ -0,0 +1,2 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
@@ -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
- export type Level = 'PASS' | 'WARN' | 'FAIL';
15
- export interface Check {
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
- void (0, probe_process_1.stopProbeProcess)(child).then(released => resolve(released
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
- exports.BEGIN = '# >>> holmes-kit >>>';
15
- exports.END = '# <<< holmes-kit <<<';
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 에 들어가면 다른 기계에서 남의 설치를 부르거나 아무것도 부르지 못한다. 팀과 공유하고
@@ -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
- const r = runAutonomy(root, positionals[0], forStr, {
335
- now: new Date().toISOString(),
336
- isTTY: !!process.stdin.isTTY && !!process.stdout.isTTY,
337
- approval: readApproval(),
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
- if (typeof flags.grant === 'string') {
803
- const opts = { actor };
804
- if (typeof flags.ttl === 'string')
805
- opts.ttlMinutes = Number(flags.ttl);
806
- if (typeof flags.rationale === 'string')
807
- opts.rationale = flags.rationale;
808
- const ref = resolveRef(flags.grant);
809
- if ('refusal' in ref) {
810
- process.stdout.write(`\u2717 ${ref.refusal}\n`);
811
- return 1;
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
- const r = grantRequest(root, ref.id, opts);
814
- process.stdout.write(r.ok ? `✓ approved — ${ref.subject('✓ approved ')}\n${ref.detail(`valid until ${r.expires}`)}\n` : `✗ ${r.reason}\n`);
815
- return r.ok ? 0 : 1;
816
- }
817
- if (typeof flags.deny === 'string') {
818
- const reason = typeof flags.reason === 'string' ? flags.reason.trim() : '';
819
- if (reason === '') {
820
- process.stderr.write(' --deny requires --reason — a denial with no reason is invisible to the agent\n');
821
- return 1;
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
- const ref = resolveRef(flags.deny);
824
- if ('refusal' in ref) {
825
- process.stdout.write(`\u2717 ${ref.refusal}\n`);
826
- return 1;
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
- const r = denyRequest(root, ref.id, reason, actor);
829
- process.stdout.write(r.ok ? `✓ denied — ${ref.subject('✓ denied ')}\n${ref.detail('the denial was recorded')}\n` : `✗ ${r.reason}\n`);
830
- return r.ok ? 0 : 1;
831
- }
832
- if (typeof flags.ask === 'string') {
833
- const question = typeof flags.question === 'string' ? flags.question.trim() : '';
834
- if (question === '') {
835
- process.stderr.write(' --ask requires --question\n');
836
- return 1;
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
- const ref = resolveRef(flags.ask);
839
- if ('refusal' in ref) {
840
- process.stdout.write(`\u2717 ${ref.refusal}\n`);
841
- return 1;
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
- const r = holdRequest(root, ref.id, question, actor);
844
- process.stdout.write(r.ok ? `✓ held — ${ref.subject('✓ held — ')}\n${ref.detail('the question rides the next refusal')}\n` : `✗ ${r.reason}\n`);
845
- return r.ok ? 0 : 1;
846
- }
847
- // @implements A-SPEC-507.1 — stale folding is a LIST-VIEW judgment: the clock enters only
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
- process.stdout.write(renderRefusals(readRefusals(root), limit) + '\n');
867
- return 0;
868
- }
869
- // @implements A-SPEC-262.2 the RESIDENT surface. Validate --poll-ms first (so a bad value is
870
- // named even on a non-TTY), then refuse a non-TTY (a resident interactive loop needs a terminal),
871
- // then run the loop. SIGINT and readline close both abort it — the operator leaving, either way,
872
- // ends the watch at once and exit 0; pending items stay in the queue.
873
- if (flags.watch === true) {
874
- let pollMs;
875
- if (typeof flags['poll-ms'] === 'string') {
876
- pollMs = Number(flags['poll-ms']);
877
- if (!Number.isInteger(pollMs) || pollMs <= 0) {
878
- process.stderr.write(`✗ --poll-ms must be a positive integer (milliseconds) — received: ${flags['poll-ms']}\n`);
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
- process.stderr.write('✗ interactive resident mode (--watch) requires a TTY — use --list in scripts\n');
884
- return 1;
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-497.2 — the resident surface decides on ONE raw keypress (a/d/q/s, no
887
- // Enter, no deadline) and pings the OS on each new arrival. Raw mode is restored on the way
888
- // out whatever happens; Ctrl-C arrives as \x03 through decideByKey and ends the loop.
889
- const { runWatch } = require('./approve-watch');
890
- const { notifyQueueArrival } = require('./os-notify');
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
- const onSig = () => ac.abort();
893
- process.once('SIGINT', onSig);
894
- const stdin = process.stdin;
975
+ rl.once('close', () => ac.abort());
895
976
  try {
896
- stdin.setRawMode?.(true);
897
- stdin.resume();
898
- await runWatch(root, {
977
+ await runInteractive(root, {
899
978
  print: (t) => process.stdout.write(t + '\n'),
900
- ask: async () => '', // unused on the key surface; kept for the ApproveIO shape
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
- process.removeListener('SIGINT', onSig);
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;