@holmes-lab/holmes-kit 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (107) hide show
  1. package/CHANGELOG.md +22 -0
  2. package/LICENSE +21 -0
  3. package/README.md +102 -0
  4. package/bin/holmes-hook-antigravity.js +31 -0
  5. package/bin/holmes-kit.js +23 -0
  6. package/bin/holmes-mcp.js +34 -0
  7. package/bin/holmes-stop-antigravity.js +29 -0
  8. package/dist/.build-id +1 -0
  9. package/dist/holmes/cli/agents.js +168 -0
  10. package/dist/holmes/cli/doctor.js +625 -0
  11. package/dist/holmes/cli/gitignore-merge.js +84 -0
  12. package/dist/holmes/cli/governed-precondition.js +157 -0
  13. package/dist/holmes/cli/index.js +384 -0
  14. package/dist/holmes/cli/init.js +462 -0
  15. package/dist/holmes/cli/playbook-skills.js +711 -0
  16. package/dist/holmes/cli/roles-readme.js +134 -0
  17. package/dist/holmes/cli/settings-merge.js +122 -0
  18. package/dist/holmes/config/config.js +70 -0
  19. package/dist/holmes/context/bundler.js +114 -0
  20. package/dist/holmes/context/render.js +29 -0
  21. package/dist/holmes/context/tiers.js +110 -0
  22. package/dist/holmes/context/tokens.js +8 -0
  23. package/dist/holmes/cpg/cpg-scanner.js +213 -0
  24. package/dist/holmes/cpg/hash-cache.js +86 -0
  25. package/dist/holmes/cpg/language-parser-walk.js +917 -0
  26. package/dist/holmes/cpg/language-parser-worker.js +81 -0
  27. package/dist/holmes/cpg/language-parser.js +234 -0
  28. package/dist/holmes/cpg/scan-cache.js +108 -0
  29. package/dist/holmes/cpg/source-path.js +44 -0
  30. package/dist/holmes/cpg/test-files.js +84 -0
  31. package/dist/holmes/governance/constitution-debt.js +73 -0
  32. package/dist/holmes/governance/constitution-report.js +25 -0
  33. package/dist/holmes/governance/constitution.js +129 -0
  34. package/dist/holmes/governance/identity.js +30 -0
  35. package/dist/holmes/governance/ledger-lock.js +165 -0
  36. package/dist/holmes/governance/ledger-store.conformance.js +90 -0
  37. package/dist/holmes/governance/ledger-store.js +106 -0
  38. package/dist/holmes/governance/progress-ledger.js +83 -0
  39. package/dist/holmes/governance/provenance-chain.js +365 -0
  40. package/dist/holmes/governance/provenance-ledger.js +0 -0
  41. package/dist/holmes/governance/provenance-schema.js +47 -0
  42. package/dist/holmes/governance/replica-id.js +106 -0
  43. package/dist/holmes/governance/role-policy.js +137 -0
  44. package/dist/holmes/governance/trust-score.js +43 -0
  45. package/dist/holmes/guardrail/anchors.js +31 -0
  46. package/dist/holmes/guardrail/blind-spots.js +38 -0
  47. package/dist/holmes/guardrail/decision-ledger.js +107 -0
  48. package/dist/holmes/guardrail/executable-artifact.js +129 -0
  49. package/dist/holmes/guardrail/governance-history.js +101 -0
  50. package/dist/holmes/guardrail/phase.js +169 -0
  51. package/dist/holmes/guardrail/risk-classifier.js +450 -0
  52. package/dist/holmes/guardrail/risk-gate.js +160 -0
  53. package/dist/holmes/guardrail/risk-types.js +6 -0
  54. package/dist/holmes/guardrail/tspec-state.js +392 -0
  55. package/dist/holmes/guardrail/write-target.js +224 -0
  56. package/dist/holmes/hooks/adapters/antigravity.js +194 -0
  57. package/dist/holmes/hooks/pre-tool-use.js +1262 -0
  58. package/dist/holmes/hooks/stop.js +416 -0
  59. package/dist/holmes/mcp/basis.js +162 -0
  60. package/dist/holmes/mcp/handlers.js +1831 -0
  61. package/dist/holmes/mcp/server.js +71 -0
  62. package/dist/holmes/mcp/stdio-client.js +165 -0
  63. package/dist/holmes/mcp/supervisor.js +178 -0
  64. package/dist/holmes/mcp/tool-schemas.js +394 -0
  65. package/dist/holmes/mcp/validate-args.js +281 -0
  66. package/dist/holmes/messages/registry.js +50 -0
  67. package/dist/holmes/project/baseline.js +210 -0
  68. package/dist/holmes/project/change-source.js +233 -0
  69. package/dist/holmes/project/ignore.js +145 -0
  70. package/dist/holmes/project/root.js +113 -0
  71. package/dist/holmes/reverse/anchor.js +162 -0
  72. package/dist/holmes/reverse/cluster.js +187 -0
  73. package/dist/holmes/reverse/draft.js +151 -0
  74. package/dist/holmes/reverse/dynamic-wiring.js +47 -0
  75. package/dist/holmes/reverse/scan.js +194 -0
  76. package/dist/holmes/reverse/surface.js +154 -0
  77. package/dist/holmes/reverse/test-map.js +263 -0
  78. package/dist/holmes/review/coverage.js +33 -0
  79. package/dist/holmes/review/findings.js +123 -0
  80. package/dist/holmes/review/package.js +40 -0
  81. package/dist/holmes/review/review-targets.js +92 -0
  82. package/dist/holmes/review/scope.js +57 -0
  83. package/dist/holmes/review/test-evidence.js +77 -0
  84. package/dist/holmes/review/test-runner.js +572 -0
  85. package/dist/holmes/rtm/dataflow-taint.js +262 -0
  86. package/dist/holmes/rtm/gap-analyzer.js +27 -0
  87. package/dist/holmes/rtm/git-changes.js +72 -0
  88. package/dist/holmes/rtm/incremental.js +45 -0
  89. package/dist/holmes/rtm/localize.js +100 -0
  90. package/dist/holmes/rtm/rtm-builder.js +191 -0
  91. package/dist/holmes/rtm/rtm-check.js +89 -0
  92. package/dist/holmes/rtm/rtm-graph.js +232 -0
  93. package/dist/holmes/rtm/taint.js +92 -0
  94. package/dist/holmes/rtm/test-scope.js +336 -0
  95. package/dist/holmes/spec/approval-blockers.js +204 -0
  96. package/dist/holmes/spec/breaking-change.js +89 -0
  97. package/dist/holmes/spec/legacy-format.js +87 -0
  98. package/dist/holmes/spec/spec-digest.js +71 -0
  99. package/dist/holmes/spec/spec-parser.js +106 -0
  100. package/dist/holmes/spec/spec-store.conformance.js +118 -0
  101. package/dist/holmes/spec/spec-store.js +331 -0
  102. package/dist/holmes/spec/spec-types.js +177 -0
  103. package/dist/holmes/spec/validator.js +280 -0
  104. package/package.json +76 -0
  105. package/playbooks/adopt/PLAYBOOK.md +125 -0
  106. package/playbooks/author-slice/PLAYBOOK.md +119 -0
  107. package/playbooks/promote-slice/PLAYBOOK.md +134 -0
@@ -0,0 +1,71 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.HOOK_ENFORCED_TOOLS = void 0;
4
+ const index_js_1 = require("@modelcontextprotocol/sdk/server/index.js");
5
+ const stdio_js_1 = require("@modelcontextprotocol/sdk/server/stdio.js");
6
+ const types_js_1 = require("@modelcontextprotocol/sdk/types.js");
7
+ const spec_store_1 = require("../spec/spec-store");
8
+ const handlers_1 = require("./handlers");
9
+ const tool_schemas_1 = require("./tool-schemas");
10
+ Object.defineProperty(exports, "HOOK_ENFORCED_TOOLS", { enumerable: true, get: function () { return tool_schemas_1.HOOK_ENFORCED_TOOLS; } });
11
+ const validate_args_1 = require("./validate-args");
12
+ // @implements A-SPEC-100.2
13
+ const store = new spec_store_1.LocalMarkdownRepository(process.env.HOLMES_SPECS ?? '.ax/specs');
14
+ const handlers = (0, handlers_1.makeHandlers)(store);
15
+ const server = new index_js_1.Server({ name: 'holmes-kit', version: '0.1.0' }, { capabilities: { tools: {} } });
16
+ const fullProfile = process.env.HOLMES_MCP_PROFILE === 'full';
17
+ // Register each handler as a tool with its real typed inputSchema so MCP
18
+ // clients can marshal complex (array/object) arguments; fall back to a
19
+ // permissive stub only if a handler somehow lacks a schema (tool-schemas.test.ts
20
+ // pins full 1:1 coverage, so the fallback should never fire in practice).
21
+ const TOOLS = Object.keys(handlers)
22
+ .filter((name) => fullProfile || !tool_schemas_1.HOOK_ENFORCED_TOOLS.has(name))
23
+ .map((name) => {
24
+ const def = tool_schemas_1.TOOL_SCHEMAS[name];
25
+ return def
26
+ ? { name, description: def.description, inputSchema: def.inputSchema }
27
+ : { name, description: name, inputSchema: { type: 'object', properties: {} } };
28
+ });
29
+ server.setRequestHandler(types_js_1.ListToolsRequestSchema, async () => ({ tools: TOOLS }));
30
+ server.setRequestHandler(types_js_1.CallToolRequestSchema, async (req) => {
31
+ // @implements A-SPEC-189
32
+ // The server is the first consumer of its own advertised schemas. Before this check, 15 of 26
33
+ // handlers threw raw internal errors at `{}` over the wire, and a one-key typo in reverse_anchor
34
+ // produced a wrong diagnosis or a success-shaped no-op. Judged against the SAME object ListTools
35
+ // serves — a separate copy would be a second truth that drifts. In-process callers (makeHandlers)
36
+ // are authors of these schemas, not consumers; the wire is where enforcement belongs.
37
+ // Own-property lookups: 'constructor'/'__proto__' as a tool name would otherwise resolve through
38
+ // the prototype chain to a truthy non-handler and crash the new validation path (round-2 probe) —
39
+ // pre-enforcement it silently "succeeded" by invoking Object. Both are wrong; unknown tool is true.
40
+ const own = (o, k) => Object.prototype.hasOwnProperty.call(o, k) ? o[k] : undefined;
41
+ const fn = own(handlers, req.params.name);
42
+ const def = own(tool_schemas_1.TOOL_SCHEMAS, req.params.name);
43
+ const args = req.params.arguments ?? {};
44
+ const check = fn && def ? (0, validate_args_1.validateArgs)(req.params.name, def.inputSchema, args) : { ok: true };
45
+ // @implements A-SPEC-189 §6 — a handler's REFUSAL must arrive in the refusal shape. Round-9
46
+ // measured the seam: review_record's pointed guidance ('send it as summary') left the wire as a
47
+ // raw JSON-RPC -32603, i.e. this REQ closed 15 raw-error paths and opened one. A deliberate
48
+ // refusal is a plain `Error` thrown by a handler; a programming fault (TypeError, RangeError…)
49
+ // keeps failing loudly, because dressing a crash as an orderly refusal is how a bug becomes a
50
+ // policy decision in the caller's log.
51
+ let out;
52
+ if (!fn)
53
+ out = { error: 'unknown tool' };
54
+ else if (!check.ok)
55
+ out = check;
56
+ else {
57
+ try {
58
+ out = await fn(args);
59
+ }
60
+ catch (e) {
61
+ // Round-10: only a refusal the handler CHOSE is dressed as a refusal. Everything else —
62
+ // fs/exec failures included — keeps failing loudly, because a crash wearing `{ok:false}` is
63
+ // how a broken gate reads as a passed one.
64
+ if (!(0, handlers_1.isHandlerRefusal)(e))
65
+ throw e;
66
+ out = { ok: false, reason: `${req.params.name}: ${e.message}` };
67
+ }
68
+ }
69
+ return { content: [{ type: 'text', text: JSON.stringify(out) }] };
70
+ });
71
+ void server.connect(new stdio_js_1.StdioServerTransport());
@@ -0,0 +1,165 @@
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.StdioClient = exports.OMITTED_FROM_COMPARISON = void 0;
37
+ exports.comparable = comparable;
38
+ exports.parseToolResult = parseToolResult;
39
+ // @implements A-SPEC-159
40
+ const node_child_process_1 = require("node:child_process");
41
+ const fs = __importStar(require("node:fs"));
42
+ const path = __importStar(require("node:path"));
43
+ /**
44
+ * Keys excluded from surface comparison.
45
+ *
46
+ * `basis` legitimately differs: two processes observe the world at different moments, so the spec
47
+ * fingerprint can move between the two calls. Excluding it is correct — but the exclusion is named
48
+ * and asserted, because an agreement claim whose exceptions are invisible is stronger-sounding than
49
+ * it is.
50
+ */
51
+ exports.OMITTED_FROM_COMPARISON = ['basis'];
52
+ /** Strips the excluded keys so two answers can be compared for the part that must match. */
53
+ function comparable(v) {
54
+ if (v === null || typeof v !== 'object' || Array.isArray(v))
55
+ return v;
56
+ const out = {};
57
+ for (const [k, val] of Object.entries(v)) {
58
+ if (exports.OMITTED_FROM_COMPARISON.includes(k))
59
+ continue;
60
+ out[k] = val;
61
+ }
62
+ return out;
63
+ }
64
+ /**
65
+ * Extracts a tool's payload from an MCP result.
66
+ *
67
+ * Pure and exported because the failure it guards — a surface that answers with no text at all —
68
+ * cannot be produced by the real server (an unknown tool comes back as a perfectly well-formed
69
+ * `{"error":"unknown tool"}`), so the branch is unreachable from an end-to-end test. Left inline it
70
+ * would be an untested guard, and an untested guard is one a later change deletes for free: with it
71
+ * gone, a silent surface yields `undefined`, `undefined` compares equal to `undefined`, and the
72
+ * conformance suite reports agreement between two answers that contain nothing.
73
+ */
74
+ function parseToolResult(name, result, error) {
75
+ // A JSON-RPC error carries no `content`, so without this the next check would report "no text
76
+ // content" and hide what actually went wrong. Measured: this server answers an unknown tool with a
77
+ // well-formed `{"error":"unknown tool"}` result rather than a protocol error, so the branch is
78
+ // unreachable end-to-end and has to be pinned here or not at all.
79
+ if (error !== undefined)
80
+ throw new Error(`tool ${name} failed: ${JSON.stringify(error)}`);
81
+ const text = result?.content?.[0]?.text;
82
+ if (typeof text !== 'string')
83
+ throw new Error(`tool ${name} returned no text content`);
84
+ return JSON.parse(text);
85
+ }
86
+ /**
87
+ * A minimal MCP client that SPAWNS the shipped entry point.
88
+ *
89
+ * Importing the handler module — what every other test does — cannot see the boundary where the
90
+ * 2026-08-08 defect lived: a server process running a three-day-old build answered `impactedSpecs:
91
+ * []` for a commit touching 39 specs, while the same handler in a fresh process answered correctly,
92
+ * and 1,370 tests stayed green throughout. Anything that does not cross a process boundary is blind
93
+ * to that entire class.
94
+ *
95
+ * One server is started per suite and reused, so conformance costs one spawn rather than one per
96
+ * tool.
97
+ */
98
+ class StdioClient {
99
+ child;
100
+ buf = '';
101
+ nextId = 1;
102
+ pending = new Map();
103
+ constructor(child) {
104
+ this.child = child;
105
+ this.child.stdout.on('data', (d) => {
106
+ this.buf += d.toString();
107
+ let nl;
108
+ while ((nl = this.buf.indexOf('\n')) >= 0) {
109
+ const line = this.buf.slice(0, nl).trim();
110
+ this.buf = this.buf.slice(nl + 1);
111
+ if (!line.startsWith('{'))
112
+ continue;
113
+ let msg;
114
+ try {
115
+ msg = JSON.parse(line);
116
+ }
117
+ catch {
118
+ continue;
119
+ }
120
+ const resolve = msg.id !== undefined ? this.pending.get(msg.id) : undefined;
121
+ if (resolve && msg.id !== undefined) {
122
+ this.pending.delete(msg.id);
123
+ resolve(msg);
124
+ }
125
+ }
126
+ });
127
+ }
128
+ /**
129
+ * @param specsDir Overrides `HOLMES_SPECS` for the child. Needed because the server binds its
130
+ * spec store at start-up from that variable and ignores the `root` argument a tool receives
131
+ * (REQ-169) — without this, a write tool driven for conformance lands in the server's own
132
+ * project. Measured: it wrote REQ-901.md into this repository.
133
+ */
134
+ static async start(repoRoot, specsDir = '.ax/specs') {
135
+ // A conformance suite that skips when it cannot spawn reports green for exactly the situation it
136
+ // exists to detect, so an unusable root is an error rather than a quiet no-op.
137
+ const entry = path.join(repoRoot, 'bin', 'holmes-mcp.js');
138
+ if (!fs.existsSync(entry))
139
+ throw new Error(`no MCP entry point at ${entry}`);
140
+ const child = (0, node_child_process_1.spawn)('node', ['bin/holmes-mcp.js'], {
141
+ cwd: repoRoot, stdio: ['pipe', 'pipe', 'pipe'], env: { ...process.env, HOLMES_SPECS: specsDir },
142
+ });
143
+ const client = new StdioClient(child);
144
+ await client.send('initialize', {
145
+ protocolVersion: '2024-11-05', capabilities: {}, clientInfo: { name: 'conformance', version: '0' },
146
+ });
147
+ child.stdin.write(`${JSON.stringify({ jsonrpc: '2.0', method: 'notifications/initialized' })}\n`);
148
+ return client;
149
+ }
150
+ send(method, params) {
151
+ return new Promise((resolve) => {
152
+ const id = this.nextId++;
153
+ this.pending.set(id, resolve);
154
+ this.child.stdin.write(`${JSON.stringify({ jsonrpc: '2.0', id, method, params })}\n`);
155
+ });
156
+ }
157
+ async call(name, args) {
158
+ const res = await this.send('tools/call', { name, arguments: args });
159
+ return parseToolResult(name, res.result, res.error);
160
+ }
161
+ stop() {
162
+ this.child.kill();
163
+ }
164
+ }
165
+ exports.StdioClient = StdioClient;
@@ -0,0 +1,178 @@
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.Supervisor = exports.AUTORELOAD_ENV = void 0;
37
+ exports.autoreloadEnabled = autoreloadEnabled;
38
+ exports.shouldSwap = shouldSwap;
39
+ // @implements A-SPEC-162
40
+ const node_child_process_1 = require("node:child_process");
41
+ const path = __importStar(require("node:path"));
42
+ const basis_1 = require("./basis");
43
+ /** Opt-in switch. Absent means the entry point behaves exactly as it did before this existed. */
44
+ exports.AUTORELOAD_ENV = 'HOLMES_MCP_AUTORELOAD';
45
+ function autoreloadEnabled(env) {
46
+ const v = env[exports.AUTORELOAD_ENV];
47
+ return typeof v === 'string' && v.trim() !== '';
48
+ }
49
+ /**
50
+ * Whether the child may be replaced right now.
51
+ *
52
+ * Pure, so the three ways this can be wrong are testable without spawning anything.
53
+ *
54
+ * An unknown disk build never swaps: not knowing is not divergence, the same posture A-SPEC-160 took
55
+ * for the digest marker. Reading absence as change would restart continuously on any install whose
56
+ * build is unstamped.
57
+ *
58
+ * An in-flight request never swaps. Each child starts its JSON-RPC id counter at 1, so a mid-request
59
+ * swap lets the new child's reply match an id the old one issued — an answer delivered to the wrong
60
+ * question, which is worse than the staleness it was fixing.
61
+ */
62
+ function shouldSwap(loaded, disk, inflight) {
63
+ if (disk === undefined || disk === 'unknown')
64
+ return false;
65
+ if (disk === loaded)
66
+ return false;
67
+ return inflight === 0;
68
+ }
69
+ /**
70
+ * Runs the real server as a child and relays stdio, replacing the child when the build changes.
71
+ *
72
+ * A child process rather than `require.cache` surgery: this package depends on nine native addons
73
+ * (better-sqlite3 plus eight tree-sitter grammars), and re-requiring a `.node` either crashes the
74
+ * process or leaks the handles it still owns — better-sqlite3 holds open database handles. A fresh
75
+ * child loads them cleanly, and the client never sees the connection drop.
76
+ *
77
+ * This is a CONVENIENCE, not a correctness mechanism. If a swap fails, REQ-156's basis and ART-6
78
+ * still expose the divergence; making the supervisor responsible for correctness would turn it into
79
+ * a new single point of failure for the property it was meant to help with.
80
+ */
81
+ class Supervisor {
82
+ repoRoot;
83
+ onSwap;
84
+ child;
85
+ inflight = 0;
86
+ loaded;
87
+ /**
88
+ * The client's opening exchange, replayed into every replacement child. Without it the first call
89
+ * after a swap hits an uninitialised server and fails — which would break the one promise the
90
+ * supervisor makes, that the connection does not drop.
91
+ */
92
+ handshake = [];
93
+ constructor(repoRoot, onSwap) {
94
+ this.repoRoot = repoRoot;
95
+ this.onSwap = onSwap;
96
+ this.loaded = (0, basis_1.loadedBuildId)(repoRoot);
97
+ }
98
+ start(stdin, stdout) {
99
+ this.spawnChild(stdout);
100
+ let buf = '';
101
+ stdin.on('data', (d) => {
102
+ buf += d.toString();
103
+ let nl;
104
+ while ((nl = buf.indexOf('\n')) >= 0) {
105
+ const line = buf.slice(0, nl + 1);
106
+ buf = buf.slice(nl + 1);
107
+ this.forward(line, stdout);
108
+ }
109
+ });
110
+ }
111
+ forward(line, stdout) {
112
+ const trimmed = line.trim();
113
+ let isRequest = false;
114
+ if (trimmed.startsWith('{')) {
115
+ try {
116
+ const msg = JSON.parse(trimmed);
117
+ if (msg.method === 'initialize' || msg.method === 'notifications/initialized')
118
+ this.handshake.push(line);
119
+ isRequest = msg.id !== undefined;
120
+ }
121
+ catch { /* unparseable input is relayed untouched; the child owns protocol errors */ }
122
+ }
123
+ // Decide BEFORE counting this request in. Measured: incrementing first made `inflight` at least
124
+ // 1 for every request-carrying line, so the swap check never saw zero and the child was never
125
+ // replaced — the live probe caught it while the pure-function unit tests all passed, because
126
+ // they test the predicate and this is the caller.
127
+ const disk = (0, basis_1.loadedBuildId)(this.repoRoot);
128
+ if (shouldSwap(this.loaded, disk, this.inflight))
129
+ this.swap(disk, stdout);
130
+ if (isRequest)
131
+ this.inflight++;
132
+ this.child?.stdin.write(line);
133
+ }
134
+ swap(to, stdout) {
135
+ const from = this.loaded;
136
+ this.child?.kill();
137
+ this.loaded = to;
138
+ this.spawnChild(stdout);
139
+ // Counted as WRITTEN, not as captured. Reporting `handshake.length` looked like an observation
140
+ // but described the capture step, so deleting the replay left the number unchanged and the test
141
+ // kept passing — the same "crediting the wrong layer" mistake in a new place.
142
+ let replayed = 0;
143
+ for (const line of this.handshake) {
144
+ if (this.child?.stdin.write(line))
145
+ replayed++;
146
+ else if (this.child)
147
+ replayed++; // buffered rather than flushed is still sent
148
+ }
149
+ // Measured: this server answers `tools/call` without a prior `initialize`, so the replay's
150
+ // absence cannot be detected downstream — asserting "the call would fail without it" would be
151
+ // asserting something untrue. The contract is that the opening exchange is re-sent; a
152
+ // spec-compliant server is entitled to require it.
153
+ this.onSwap?.(from, to, replayed);
154
+ }
155
+ spawnChild(stdout) {
156
+ // `stdio: ['pipe','pipe','pipe']` keeps the typed streams non-null; the child's stderr is
157
+ // forwarded rather than inherited so a supervised run logs identically to an unsupervised one.
158
+ const child = (0, node_child_process_1.spawn)('node', [path.join('dist', 'holmes', 'mcp', 'server.js')], {
159
+ cwd: this.repoRoot, stdio: ['pipe', 'pipe', 'pipe'],
160
+ });
161
+ child.stderr.on('data', (d) => process.stderr.write(d));
162
+ this.child = child;
163
+ child.stdout.on('data', (d) => {
164
+ const text = d.toString();
165
+ // Count replies out so `inflight` returns to zero; the id itself does not matter here, only
166
+ // that a response arrived for something that was counted in.
167
+ for (const line of text.split('\n')) {
168
+ if (line.trim().startsWith('{') && line.includes('"id"') && this.inflight > 0)
169
+ this.inflight--;
170
+ }
171
+ stdout.write(text);
172
+ });
173
+ }
174
+ stop() {
175
+ this.child?.kill();
176
+ }
177
+ }
178
+ exports.Supervisor = Supervisor;