@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,130 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.npxPinOf = npxPinOf;
37
+ exports.npmCacheRoot = npmCacheRoot;
38
+ exports.checkNpxCache = checkNpxCache;
39
+ // @implements A-SPEC-652.2
40
+ /**
41
+ * doctor: is the npx cache entry the wiring will run intact?
42
+ *
43
+ * Measured 2026-09-15 on Windows: `.mcp.json` wires `npx -y -p @holmes-lab/holmes-kit@0.19.4
44
+ * holmes-mcp`; npx resolved it to `_npx/a739130c5322c549`, whose root `package.json` had gone, and
45
+ * died with `npm error ENOENT … package.json` before any handshake. The harness reported only
46
+ * "Connection closed". Six entries in that cache had the same shape (two of them this package).
47
+ *
48
+ * Filesystem only — no npm spawn: the cache root is `npm_config_cache`, else the platform default
49
+ * (`%LOCALAPPDATA%\npm-cache` on win32, `~/.npm` elsewhere). Only entries holding the PINNED version
50
+ * of the pinned package are judged; a corrupt entry of another version is not what this wiring runs.
51
+ */
52
+ const path = __importStar(require("node:path"));
53
+ const NAME = 'npx cache';
54
+ /** The `-p <pkg>@<ver>` pin of the target's holmes-kit wiring, or null when it is not an npx pin. */
55
+ function npxPinOf(target, deps) {
56
+ const mcpPath = path.join(target, '.mcp.json');
57
+ if (!deps.exists(mcpPath))
58
+ return null;
59
+ let server;
60
+ try {
61
+ server = JSON.parse(deps.readFile(mcpPath)).mcpServers?.['holmes-kit'];
62
+ }
63
+ catch {
64
+ return null;
65
+ }
66
+ if (!server || !/^npx(\.cmd)?$/i.test(String(server.command ?? '')) || !Array.isArray(server.args))
67
+ return null;
68
+ const at = server.args.indexOf('-p');
69
+ const spec = at >= 0 ? server.args[at + 1] : undefined;
70
+ const m = spec ? /^(@?[^@]+)@([0-9][^\s]*)$/.exec(spec) : null;
71
+ return m ? { pkg: m[1], version: m[2] } : null;
72
+ }
73
+ function npmCacheRoot(deps) {
74
+ const configured = deps.env.npm_config_cache;
75
+ if (configured && configured.trim() !== '')
76
+ return configured;
77
+ if (deps.platform === 'win32') {
78
+ const local = deps.env.LOCALAPPDATA && deps.env.LOCALAPPDATA.trim() !== '' ? deps.env.LOCALAPPDATA : path.join(deps.homedir, 'AppData', 'Local');
79
+ return path.join(local, 'npm-cache');
80
+ }
81
+ return path.join(deps.homedir, '.npm');
82
+ }
83
+ function checkNpxCache(target, deps) {
84
+ const pin = npxPinOf(target, deps);
85
+ if (!pin)
86
+ return { name: NAME, level: 'PASS', detail: 'the holmes-kit wiring is not an npx pin — nothing cached to judge' };
87
+ const npxDir = path.join(npmCacheRoot(deps), '_npx');
88
+ if (!deps.exists(npxDir))
89
+ return { name: NAME, level: 'PASS', detail: `${pin.pkg}@${pin.version} is not cached yet (${npxDir} absent) — the first launch installs it` };
90
+ let hashes = [];
91
+ try {
92
+ hashes = deps.readdir(npxDir);
93
+ }
94
+ catch {
95
+ return { name: NAME, level: 'PASS', detail: `${npxDir} could not be listed — nothing to judge` };
96
+ }
97
+ const matching = [];
98
+ const corrupt = [];
99
+ for (const hash of hashes) {
100
+ const entry = path.join(npxDir, hash);
101
+ const pkgJson = path.join(entry, 'node_modules', ...pin.pkg.split('/'), 'package.json');
102
+ if (!deps.exists(pkgJson))
103
+ continue;
104
+ let version;
105
+ try {
106
+ version = JSON.parse(deps.readFile(pkgJson)).version;
107
+ }
108
+ catch {
109
+ continue;
110
+ }
111
+ if (version !== pin.version)
112
+ continue;
113
+ matching.push(hash);
114
+ if (!deps.exists(path.join(entry, 'package.json')))
115
+ corrupt.push(hash);
116
+ }
117
+ if (matching.length === 0)
118
+ return { name: NAME, level: 'PASS', detail: `${pin.pkg}@${pin.version} is not cached yet under ${npxDir} — the first launch installs it` };
119
+ if (corrupt.length === 0)
120
+ return { name: NAME, level: 'PASS', detail: `${matching.length} intact npx cache entr${matching.length === 1 ? 'y' : 'ies'} for ${pin.pkg}@${pin.version}` };
121
+ const dirs = corrupt.map((h) => path.join(npxDir, h));
122
+ const remove = deps.platform === 'win32'
123
+ ? dirs.map((d) => `Remove-Item -Recurse -Force "${d}"`).join('; ')
124
+ : dirs.map((d) => `rm -rf ${d}`).join(' && ');
125
+ return {
126
+ name: NAME, level: 'FAIL',
127
+ detail: `npx cache entr${corrupt.length === 1 ? 'y' : 'ies'} ${corrupt.join(', ')} for ${pin.pkg}@${pin.version} ${corrupt.length === 1 ? 'is' : 'are'} missing its package.json — the wiring dies with \`npm error ENOENT\` before the handshake and the harness reports only "Connection closed" (${dirs.join(', ')})`,
128
+ fix: `Delete the corrupt entr${corrupt.length === 1 ? 'y' : 'ies'} and let the next launch reinstall: ${remove}`,
129
+ };
130
+ }
@@ -11,12 +11,8 @@
11
11
  * Playbooks are ADVISORY. No gate decision reads them, and a user who deletes every one of them
12
12
  * still gets identical enforcement — which is why a missing playbook is a doctor WARN, never a FAIL.
13
13
  */
14
- /**
15
- * Marks a file as OURS. Ownership is decided by this marker and never by the path, because the path
16
- * is exactly what a user's own skill of the same name would occupy, and overwriting that would be
17
- * the same class of error as clobbering their settings.
18
- */
19
- export declare const PLAYBOOK_SKILL_MARKER = "<!-- installed by holmes-kit; edits will be replaced on upgrade -->";
14
+ import { PLAYBOOK_SKILL_MARKER, carriesMarker, frontmatterEnd } from '../project/installer-markers';
15
+ export { PLAYBOOK_SKILL_MARKER, carriesMarker, frontmatterEnd };
20
16
  /**
21
17
  * @implements A-SPEC-190 (round 7)
22
18
  * The bytes an install writes. The marker used to go FIRST, which put it ahead of the playbook's
@@ -28,22 +24,6 @@ export declare const PLAYBOOK_SKILL_MARKER = "<!-- installed by holmes-kit; edit
28
24
  * while doctor certified the layout as `current`. The marker now follows the frontmatter block.
29
25
  */
30
26
  export declare function installedSkillBytes(body: string): string;
31
- /**
32
- * @implements A-SPEC-190 (round 7)
33
- * Ownership is the marker AT ITS PLACE, not the string anywhere in the file. `includes` made any
34
- * document that merely QUOTES the marker — documentation about holmes-kit, a review note — read as
35
- * ours: doctor called it `drifted` and the refresh it advised overwrote the user's file with no
36
- * backup. A quotation is not a claim of ownership.
37
- */
38
- export declare function carriesMarker(content: string): boolean;
39
- /**
40
- * @implements A-SPEC-190 (round 8)
41
- * Where a leading YAML frontmatter block ends, or null. Round-8 measured the shapes the first
42
- * regex missed — a BOM, an EMPTY block (`---\n---\n`), and a closing fence at end-of-file with no
43
- * trailing newline — each of which sent the marker back to byte 0, recreating the CRITICAL the
44
- * round-7 fix existed to remove while doctor certified the result as `current`.
45
- */
46
- export declare function frontmatterEnd(content: string): number | null;
47
27
  /**
48
28
  * @implements A-SPEC-172
49
29
  * What the user types to reach a playbook.
@@ -33,10 +33,8 @@ var __importStar = (this && this.__importStar) || (function () {
33
33
  };
34
34
  })();
35
35
  Object.defineProperty(exports, "__esModule", { value: true });
36
- exports.PLAYBOOK_SKILL_MARKER = void 0;
36
+ exports.frontmatterEnd = exports.carriesMarker = exports.PLAYBOOK_SKILL_MARKER = void 0;
37
37
  exports.installedSkillBytes = installedSkillBytes;
38
- exports.carriesMarker = carriesMarker;
39
- exports.frontmatterEnd = frontmatterEnd;
40
38
  exports.invocableSkillName = invocableSkillName;
41
39
  exports.skillPathFor = skillPathFor;
42
40
  exports.installPlaybookSkills = installPlaybookSkills;
@@ -62,12 +60,10 @@ const path = __importStar(require("node:path"));
62
60
  * Playbooks are ADVISORY. No gate decision reads them, and a user who deletes every one of them
63
61
  * still gets identical enforcement — which is why a missing playbook is a doctor WARN, never a FAIL.
64
62
  */
65
- /**
66
- * Marks a file as OURS. Ownership is decided by this marker and never by the path, because the path
67
- * is exactly what a user's own skill of the same name would occupy, and overwriting that would be
68
- * the same class of error as clobbering their settings.
69
- */
70
- exports.PLAYBOOK_SKILL_MARKER = '<!-- installed by holmes-kit; edits will be replaced on upgrade -->';
63
+ const installer_markers_1 = require("../project/installer-markers");
64
+ Object.defineProperty(exports, "PLAYBOOK_SKILL_MARKER", { enumerable: true, get: function () { return installer_markers_1.PLAYBOOK_SKILL_MARKER; } });
65
+ Object.defineProperty(exports, "carriesMarker", { enumerable: true, get: function () { return installer_markers_1.carriesMarker; } });
66
+ Object.defineProperty(exports, "frontmatterEnd", { enumerable: true, get: function () { return installer_markers_1.frontmatterEnd; } });
71
67
  /**
72
68
  * @implements A-SPEC-190 (round 7)
73
69
  * The bytes an install writes. The marker used to go FIRST, which put it ahead of the playbook's
@@ -79,54 +75,14 @@ exports.PLAYBOOK_SKILL_MARKER = '<!-- installed by holmes-kit; edits will be rep
79
75
  * while doctor certified the layout as `current`. The marker now follows the frontmatter block.
80
76
  */
81
77
  function installedSkillBytes(body) {
82
- const end = frontmatterEnd(body);
78
+ const end = (0, installer_markers_1.frontmatterEnd)(body);
83
79
  if (end === null)
84
- return `${exports.PLAYBOOK_SKILL_MARKER}\n${body}`;
80
+ return `${installer_markers_1.PLAYBOOK_SKILL_MARKER}\n${body}`;
85
81
  // The offset is computed on normalised text; a body with CRLF or a BOM is rewritten normalised so
86
82
  // the marker cannot land inside the frontmatter it must follow.
87
83
  const text = body.replace(/^\uFEFF/, '').replace(/\r\n/g, '\n');
88
84
  const head = text.slice(0, end);
89
- return `${head}${head.endsWith('\n') ? '' : '\n'}${exports.PLAYBOOK_SKILL_MARKER}\n${text.slice(end)}`;
90
- }
91
- /**
92
- * @implements A-SPEC-190 (round 7)
93
- * Ownership is the marker AT ITS PLACE, not the string anywhere in the file. `includes` made any
94
- * document that merely QUOTES the marker — documentation about holmes-kit, a review note — read as
95
- * ours: doctor called it `drifted` and the refresh it advised overwrote the user's file with no
96
- * backup. A quotation is not a claim of ownership.
97
- */
98
- function carriesMarker(content) {
99
- // Round-8: judging on raw bytes made ownership brittle in the direction that cannot be repaired.
100
- // A CRLF normalisation (a Windows editor, `core.autocrlf`, `.gitattributes eol=crlf`), a BOM, or a
101
- // trailing space on the marker line each turned a file the kit itself wrote into `foreign` —
102
- // permanently, since install then skips it forever and doctor advises deleting it as "yours".
103
- // Whitespace and line endings are not ownership; the marker is.
104
- const text = content.replace(/^\uFEFF/, '').replace(/\r\n/g, '\n');
105
- const markerAt = (s) => {
106
- const line = s.split('\n', 1)[0];
107
- return line.trimEnd() === exports.PLAYBOOK_SKILL_MARKER;
108
- };
109
- if (markerAt(text))
110
- return true; // 옛 배치(마커 먼저) — 우리 것이므로 고칠 수 있다
111
- const fm = frontmatterEnd(text);
112
- // 빈 줄 하나가 소유를 지우지 않는다(round-9: frontmatter 뒤 개행 하나로 영구 foreign 이 됐다).
113
- return fm !== null && markerAt(text.slice(fm).replace(/^\s*\n/, ''));
114
- }
115
- /**
116
- * @implements A-SPEC-190 (round 8)
117
- * Where a leading YAML frontmatter block ends, or null. Round-8 measured the shapes the first
118
- * regex missed — a BOM, an EMPTY block (`---\n---\n`), and a closing fence at end-of-file with no
119
- * trailing newline — each of which sent the marker back to byte 0, recreating the CRITICAL the
120
- * round-7 fix existed to remove while doctor certified the result as `current`.
121
- */
122
- function frontmatterEnd(content) {
123
- const text = content.replace(/^\uFEFF/, '').replace(/\r\n/g, '\n');
124
- if (!/^---[ \t]*(\n|$)/.test(text))
125
- return null;
126
- const m = /\n---[ \t]*(\n|$)/.exec(text.slice(3));
127
- if (!m)
128
- return null;
129
- return 3 + m.index + m[0].length;
85
+ return `${head}${head.endsWith('\n') ? '' : '\n'}${installer_markers_1.PLAYBOOK_SKILL_MARKER}\n${text.slice(end)}`;
130
86
  }
131
87
  /**
132
88
  * @implements A-SPEC-172
@@ -168,7 +124,7 @@ function playbookNames(packageRoot) {
168
124
  }
169
125
  const isOurs = (file) => {
170
126
  try {
171
- return carriesMarker(fs.readFileSync(file, 'utf8'));
127
+ return (0, installer_markers_1.carriesMarker)(fs.readFileSync(file, 'utf8'));
172
128
  }
173
129
  catch {
174
130
  return false;
@@ -637,7 +593,7 @@ function playbookSkillStates(packageRoot, target) {
637
593
  out.push({ name, state: err.code === 'ENOENT' && !lstatPresent ? 'missing' : 'foreign' });
638
594
  continue;
639
595
  }
640
- const state = !carriesMarker(installed) ? 'foreign'
596
+ const state = !(0, installer_markers_1.carriesMarker)(installed) ? 'foreign'
641
597
  : installed === installedSkillBytes(body) ? 'current' : 'drifted';
642
598
  out.push({ name, state });
643
599
  }
@@ -1,8 +1,23 @@
1
- import { type ChildProcess } from 'node:child_process';
1
+ import { execFile as nodeExecFile, type ChildProcess } from 'node:child_process';
2
2
  /** Each POSIX probe gets its own process group; Windows taskkill targets only its owned PID. */
3
3
  export declare const PROBE_SPAWN_OPTIONS: {
4
4
  detached: boolean;
5
5
  windowsHide: boolean;
6
6
  };
7
+ /**
8
+ * A-SPEC-651.2 — budgets measured, not guessed. taskkill /T /F finished in 0.56–0.67 s on a quiet
9
+ * box (2026-09-14); the old 2.5 s budget, spent once, read "cleanup could not be confirmed" on a
10
+ * healthy install in 1 of 4 quiet doctor runs and 2 of 3 loaded ones. 8 s each, and a timed-out
11
+ * taskkill gets exactly one more try — a refused one does not (A-SPEC-592: a failed tree kill is never
12
+ * success).
13
+ */
14
+ export declare const PROBE_KILL_TIMEOUT_MS = 8000;
15
+ export declare const PROBE_CLOSE_TIMEOUT_MS = 8000;
16
+ export interface ProbeStopDeps {
17
+ platform?: NodeJS.Platform;
18
+ execFile?: typeof nodeExecFile;
19
+ }
20
+ /** Why the most recent stopProbeProcess answered false; undefined after a confirmed cleanup. */
21
+ export declare function lastProbeStopReason(): string | undefined;
7
22
  export declare function trackProbeProcess(child: ChildProcess): void;
8
- export declare function stopProbeProcess(child?: ChildProcess): Promise<boolean>;
23
+ export declare function stopProbeProcess(child?: ChildProcess, deps?: ProbeStopDeps): Promise<boolean>;
@@ -1,13 +1,27 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.PROBE_SPAWN_OPTIONS = void 0;
3
+ exports.PROBE_CLOSE_TIMEOUT_MS = exports.PROBE_KILL_TIMEOUT_MS = exports.PROBE_SPAWN_OPTIONS = void 0;
4
+ exports.lastProbeStopReason = lastProbeStopReason;
4
5
  exports.trackProbeProcess = trackProbeProcess;
5
6
  exports.stopProbeProcess = stopProbeProcess;
6
7
  // @implements A-SPEC-592
8
+ // @implements A-SPEC-651.2
7
9
  const node_child_process_1 = require("node:child_process");
8
10
  /** Each POSIX probe gets its own process group; Windows taskkill targets only its owned PID. */
9
11
  exports.PROBE_SPAWN_OPTIONS = { detached: process.platform !== 'win32', windowsHide: true };
12
+ /**
13
+ * A-SPEC-651.2 — budgets measured, not guessed. taskkill /T /F finished in 0.56–0.67 s on a quiet
14
+ * box (2026-09-14); the old 2.5 s budget, spent once, read "cleanup could not be confirmed" on a
15
+ * healthy install in 1 of 4 quiet doctor runs and 2 of 3 loaded ones. 8 s each, and a timed-out
16
+ * taskkill gets exactly one more try — a refused one does not (A-SPEC-592: a failed tree kill is never
17
+ * success).
18
+ */
19
+ exports.PROBE_KILL_TIMEOUT_MS = 8000;
20
+ exports.PROBE_CLOSE_TIMEOUT_MS = 8000;
10
21
  const states = new WeakMap();
22
+ let lastReason;
23
+ /** Why the most recent stopProbeProcess answered false; undefined after a confirmed cleanup. */
24
+ function lastProbeStopReason() { return lastReason; }
11
25
  function trackProbeProcess(child) {
12
26
  if (states.has(child))
13
27
  return;
@@ -15,30 +29,45 @@ function trackProbeProcess(child) {
15
29
  states.set(child, state);
16
30
  child.once('close', () => { state.closed = true; });
17
31
  }
18
- async function stopProbeProcess(child) {
32
+ const isTimeout = (error) => {
33
+ const e = error;
34
+ return !!e && (e.killed === true || e.code === null || e.code === undefined);
35
+ };
36
+ async function stopProbeProcess(child, deps = {}) {
37
+ lastReason = undefined;
19
38
  if (!child)
20
39
  return true;
21
40
  trackProbeProcess(child);
41
+ const platform = deps.platform ?? process.platform;
42
+ const execFile = deps.execFile ?? node_child_process_1.execFile;
22
43
  let closed = states.get(child).closed;
23
44
  let onClose = () => { };
24
45
  let closeTimer;
25
46
  const closure = new Promise(resolve => {
26
47
  onClose = () => { closed = true; clearTimeout(closeTimer); resolve(true); };
27
48
  child.once('close', onClose);
28
- closeTimer = setTimeout(() => resolve(false), 3000);
49
+ closeTimer = setTimeout(() => resolve(false), exports.PROBE_CLOSE_TIMEOUT_MS);
29
50
  if (closed || !child.pid)
30
51
  onClose();
31
52
  });
32
53
  let stopped = true;
54
+ let stopReason;
33
55
  try {
34
- if (child.pid && process.platform === 'win32' && !closed) {
35
- stopped = await new Promise(resolve => {
36
- (0, node_child_process_1.execFile)('taskkill.exe', ['/PID', String(child.pid), '/T', '/F'], { windowsHide: true, timeout: 2500 }, error => {
37
- resolve(!error);
38
- });
56
+ if (child.pid && platform === 'win32' && !closed) {
57
+ const taskkill = () => new Promise(resolve => {
58
+ execFile('taskkill.exe', ['/PID', String(child.pid), '/T', '/F'], { windowsHide: true, timeout: exports.PROBE_KILL_TIMEOUT_MS }, error => resolve(error ?? null));
39
59
  });
60
+ let error = await taskkill();
61
+ if (error && isTimeout(error))
62
+ error = await taskkill(); // one retry, for a timeout only
63
+ if (error) {
64
+ stopped = false;
65
+ stopReason = isTimeout(error)
66
+ ? `taskkill timed out twice (${exports.PROBE_KILL_TIMEOUT_MS} ms each)`
67
+ : `taskkill exited ${String(error.code)}`;
68
+ }
40
69
  }
41
- else if (child.pid && process.platform !== 'win32') {
70
+ else if (child.pid && platform !== 'win32') {
42
71
  const signalGroup = (signal) => {
43
72
  try {
44
73
  process.kill(-child.pid, signal);
@@ -56,8 +85,9 @@ async function stopProbeProcess(child) {
56
85
  signalGroup('SIGKILL');
57
86
  }
58
87
  }
59
- catch {
88
+ catch (error) {
60
89
  stopped = false;
90
+ stopReason = `kill failed: ${error.message}`;
61
91
  }
62
92
  // Terminate the tree before closing stdin: an EOF could make the wrapper exit first,
63
93
  // leaving Windows taskkill with no parent from which to enumerate its descendants.
@@ -69,5 +99,8 @@ async function stopProbeProcess(child) {
69
99
  child.removeListener('close', onClose);
70
100
  if (!released)
71
101
  child.unref();
72
- return stopped && released;
102
+ const ok = stopped && released;
103
+ if (!ok)
104
+ lastReason = stopReason ?? `child never closed within ${exports.PROBE_CLOSE_TIMEOUT_MS} ms`;
105
+ return ok;
73
106
  }
@@ -33,6 +33,27 @@ export declare function readGrants(root: string): {
33
33
  grants: Approval[];
34
34
  ignored: number;
35
35
  };
36
+ /**
37
+ * @implements A-SPEC-649
38
+ * A minted grant may carry its BINDING: the workspace it was minted in and the digest of the subject
39
+ * the operator read. Neither is part of the guardrail's `Approval` (an operator's env token has no
40
+ * binding); both are optional so legacy grant files keep covering exactly as before (stated).
41
+ */
42
+ export type BoundGrant = Approval & {
43
+ workspace?: string;
44
+ subject?: string;
45
+ requestedBy?: string;
46
+ };
47
+ export type BindingRefusal = 'foreign-workspace' | 'stale-subject';
48
+ /** @implements A-SPEC-649 — the grants that WOULD cover this action but are refused by their binding, by name. */
49
+ export declare function refusedGrantReasons(root: string, action: {
50
+ kind: string;
51
+ target: string;
52
+ subject?: string;
53
+ }, now: string): Array<{
54
+ nonce: string;
55
+ reason: BindingRefusal;
56
+ }>;
36
57
  /** First grant that covers the action — judged by `approvalCovers` and nothing else. Pure. */
37
58
  export declare function pickCoveringGrant(grants: Approval[], action: {
38
59
  kind: string;
@@ -48,6 +69,7 @@ export declare function pickCoveringGrant(grants: Approval[], action: {
48
69
  export declare function resolveApproval(root: string, envApproval: Approval | undefined, action: {
49
70
  kind: string;
50
71
  target: string;
72
+ subject?: string;
51
73
  }, now: string): {
52
74
  approval: Approval;
53
75
  source: 'env' | 'grant';
@@ -35,6 +35,7 @@ var __importStar = (this && this.__importStar) || (function () {
35
35
  Object.defineProperty(exports, "__esModule", { value: true });
36
36
  exports.GRANTS_RELDIR = void 0;
37
37
  exports.readGrants = readGrants;
38
+ exports.refusedGrantReasons = refusedGrantReasons;
38
39
  exports.pickCoveringGrant = pickCoveringGrant;
39
40
  exports.resolveApproval = resolveApproval;
40
41
  exports.consumeGrantFile = consumeGrantFile;
@@ -42,6 +43,8 @@ exports.consumeGrantFile = consumeGrantFile;
42
43
  const fs = __importStar(require("node:fs"));
43
44
  const path = __importStar(require("node:path"));
44
45
  const risk_gate_1 = require("../guardrail/risk-gate");
46
+ const workspace_identity_1 = require("../project/workspace-identity");
47
+ const approval_queue_1 = require("./approval-queue");
45
48
  /**
46
49
  * The grant file channel — approval delivery with no file editing and no reconnect.
47
50
  *
@@ -102,6 +105,47 @@ function readGrants(root) {
102
105
  }
103
106
  return { grants, ignored };
104
107
  }
108
+ /** The current workspace id, or undefined for a legacy workspace / an unreadable identity. */
109
+ function currentWorkspaceId(root) {
110
+ try {
111
+ const id = (0, workspace_identity_1.workspaceIdentity)(root, 'status');
112
+ return id.state === 'legacy' ? undefined : id.workspaceId;
113
+ }
114
+ catch {
115
+ return undefined;
116
+ }
117
+ }
118
+ /**
119
+ * Why this grant must NOT cover this action here, or null. Measured 2026-09-14 on the shipped dist:
120
+ * a grant file copied into another workspace with the same request id sealed there, and a grant
121
+ * read against one spec body sealed a different one. A grant that names a workspace is refused in
122
+ * any other (or in a workspace with no identity to compare); a grant that names a subject is
123
+ * refused when the action NAMES a subject and it differs. An action that names no subject is
124
+ * judged by its consumer at the point where the subject is known: `spec_approve` compares the
125
+ * grant's subject with the digest of its ONE versioned read (an extra read here to learn the
126
+ * subject moved the optimistic-write window and let an external edit be sealed — measured by the
127
+ * A-SPEC-188 TOCTOU pin), and every other decision kind carries a content-addressed target.
128
+ */
129
+ function bindingRefusal(grant, action, workspace) {
130
+ if (typeof grant.workspace === 'string' && grant.workspace !== '' && grant.workspace !== workspace)
131
+ return 'foreign-workspace';
132
+ if (typeof grant.subject === 'string' && grant.subject !== '' && action.subject !== undefined && grant.subject !== action.subject)
133
+ return 'stale-subject';
134
+ return null;
135
+ }
136
+ /** @implements A-SPEC-649 — the grants that WOULD cover this action but are refused by their binding, by name. */
137
+ function refusedGrantReasons(root, action, now) {
138
+ const workspace = currentWorkspaceId(root);
139
+ const out = [];
140
+ for (const g of readGrants(root).grants) {
141
+ if (!(0, risk_gate_1.approvalCovers)(g, action, now))
142
+ continue;
143
+ const reason = bindingRefusal(g, action, workspace);
144
+ if (reason && typeof g.nonce === 'string')
145
+ out.push({ nonce: g.nonce, reason });
146
+ }
147
+ return out;
148
+ }
105
149
  /** First grant that covers the action — judged by `approvalCovers` and nothing else. Pure. */
106
150
  function pickCoveringGrant(grants, action, now) {
107
151
  for (const g of Array.isArray(grants) ? grants : []) {
@@ -120,7 +164,9 @@ function pickCoveringGrant(grants, action, now) {
120
164
  function resolveApproval(root, envApproval, action, now) {
121
165
  if ((0, risk_gate_1.approvalCovers)(envApproval, action, now))
122
166
  return { approval: envApproval, source: 'env' };
123
- const grant = pickCoveringGrant(readGrants(root).grants, action, now);
167
+ // @implements A-SPEC-649 — a bound grant is a candidate only in its own workspace and for its own subject.
168
+ const workspace = currentWorkspaceId(root);
169
+ const grant = pickCoveringGrant(readGrants(root).grants.filter((g) => bindingRefusal(g, action, workspace) === null), action, now);
124
170
  return grant ? { approval: grant, source: 'grant' } : undefined;
125
171
  }
126
172
  /**
@@ -157,6 +203,14 @@ function consumeGrantFile(root, nonce) {
157
203
  // No directory, or it cannot be read: there is nothing to spend, which is an ordinary state.
158
204
  return;
159
205
  }
206
+ // @implements A-SPEC-649 — the spend is a queue event too, so the decision surface can show the
207
+ // grant's outcome without reading the provenance ledger. Best-effort, like the removal itself.
208
+ if (removed > 0) {
209
+ try {
210
+ (0, approval_queue_1.appendQueueEvent)(root, { event: 'consumed', id: wanted });
211
+ }
212
+ catch { /* fire-and-forget */ }
213
+ }
160
214
  if (removed === 0) {
161
215
  // Consumption runs AFTER the act succeeded, so this must never throw — but it must not be silent
162
216
  // either: an unspent single-use grant is still spendable, and nobody would know.
@@ -1,3 +1,4 @@
1
+ import { ExecutionContext } from '../project/execution-context';
1
2
  /**
2
3
  * The approval request queue — the review list a human batches decisions over.
3
4
  *
@@ -53,6 +54,17 @@ export interface PendingRequest {
53
54
  * reads it, so an absent or wrong value can never change a verdict.
54
55
  */
55
56
  replica?: string;
57
+ /**
58
+ * @implements A-SPEC-649
59
+ * What the FIRST `requested` event of this id said about itself: the execution that asked
60
+ * (workspace/replica/run — absent for the CLI, hooks and legacy events), the digest of the subject
61
+ * the human will read (`spec-approve`: `specDigest` of the spec as read), and the risk grade. A
62
+ * grant minted from this entry copies `subject` so a seal of DIFFERENT content is refused — measured
63
+ * 2026-09-14: a grant read against "Original title" sealed "CHANGED after the grant".
64
+ */
65
+ execution?: ExecutionContext;
66
+ subject?: string;
67
+ risk?: string;
56
68
  }
57
69
  export interface QueueState {
58
70
  pending: PendingRequest[];
@@ -75,10 +87,24 @@ export interface QueueState {
75
87
  * An invisible decision cannot stop a retry loop.
76
88
  */
77
89
  decisions: Record<string, {
78
- event: 'granted' | 'denied';
90
+ event: 'granted' | 'denied' | 'revoked';
79
91
  reason?: string;
80
92
  ts: string;
81
93
  covered?: number;
94
+ execution?: ExecutionContext;
95
+ consumedTs?: string;
96
+ expires?: string;
97
+ actor?: string;
98
+ }>;
99
+ /**
100
+ * @implements A-SPEC-650
101
+ * kind/target per request id, for EVERY `requested` event the fold saw (first wins) — the decision
102
+ * surface names what a past decision was about without re-reading the file. Present only when at
103
+ * least one request was seen, so the empty-state pins (A-SPEC-244) keep their exact shape.
104
+ */
105
+ subjects?: Record<string, {
106
+ kind: string;
107
+ target: string;
82
108
  }>;
83
109
  /**
84
110
  * @implements A-SPEC-563.2
@@ -89,6 +115,7 @@ export interface QueueState {
89
115
  holds: Record<string, {
90
116
  question?: string;
91
117
  ts: string;
118
+ execution?: ExecutionContext;
92
119
  }>;
93
120
  }
94
121
  /**
@@ -121,12 +148,17 @@ export declare function foldQueue(lines: string[], opts?: {
121
148
  * dead queue could change a verdict, killing the queue becomes a way to manipulate the gate. The
122
149
  * caller uses the boolean only to decide whether to print the review hint.
123
150
  */
151
+ /** @implements A-SPEC-649 — what a request may say about its subject and risk, beyond kind/target/why. */
152
+ export interface RequestDetails {
153
+ subject?: string;
154
+ risk?: string;
155
+ }
124
156
  export declare function enqueueApprovalRequest(root: string, req: {
125
157
  kind: string;
126
158
  target: string;
127
159
  why: string;
128
160
  reasonBytes?: number;
129
- }): boolean;
161
+ } & RequestDetails): boolean;
130
162
  /**
131
163
  * The same act, told in full: what happened and why.
132
164
  *
@@ -140,10 +172,17 @@ export declare function enqueueApprovalRequestDetailed(root: string, req: {
140
172
  target: string;
141
173
  why: string;
142
174
  reasonBytes?: number;
143
- }): {
175
+ } & RequestDetails): {
144
176
  written: boolean;
145
177
  reason: 'written' | 'duplicate' | 'no-project' | 'unwritable';
146
178
  };
179
+ /**
180
+ * @implements A-SPEC-649
181
+ * Append a lifecycle event (`consumed`, `revoked`) to the tracked queue: `.ax` must exist, the
182
+ * queue must be a plain file, the timestamp is this write's, and the active execution (if any) is
183
+ * attached — the same discipline as the CLI's decision writer. Fire-and-forget: `false` on failure.
184
+ */
185
+ export declare function appendQueueEvent(root: string, event: Record<string, unknown>): boolean;
147
186
  /** Read and fold the queue on disk. A missing file is an empty queue, not an error. */
148
187
  export declare function readQueue(root: string, opts?: {
149
188
  now?: number;
@@ -160,6 +199,6 @@ export declare function queueHint(root: string, req: {
160
199
  kind: string;
161
200
  target: string;
162
201
  why: string;
163
- }, opts?: {
202
+ } & RequestDetails, opts?: {
164
203
  baseReasonBytes?: number;
165
204
  }): string;