@holmes-lab/holmes-kit 0.1.17 → 0.1.18
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +9 -0
- package/dist/.build-id +1 -1
- package/dist/holmes/cli/approve.d.ts +11 -0
- package/dist/holmes/cli/approve.js +22 -0
- package/dist/holmes/cli/doctor.js +80 -0
- package/dist/holmes/cli/index.js +10 -2
- package/dist/holmes/cli/mcp-launcher.js +6 -1
- package/dist/holmes/mcp/server.js +13 -1
- package/dist/holmes/server/dashboard.js +2 -2
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,15 @@ All notable changes to this project will be documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
<!-- @implements A-SPEC-209 -->
|
|
8
|
+
## [0.1.18] - 2026-08-23
|
|
9
|
+
|
|
10
|
+
### Fixed
|
|
11
|
+
- **The npx MCP wiring actually starts the server (REQ-259)**: `init` on an npm-installed package wrote `npx -y @holmes-lab/holmes-kit@<ver> holmes-mcp`, which npx resolves to the package-name-matching *default* binary (`holmes-kit`) with `holmes-mcp` as its argument — exit 1, so **every npm-installed wiring since the 0.1.15 npx pin launched a dead MCP server** (`CONNECTION_CLOSED` in the harness). The wiring now rides `-p` (`npx -y -p <pkg>@<ver> holmes-mcp`), the launcher test spawns the generated form against a locally packed tarball (with the pre-fix form kept as a failing control), and `doctor` gains **`mcp wiring spawn`** — it runs the target's `.mcp.json` command verbatim inside a 30s bound and names a dead wiring FAIL with the re-init remediation. Existing projects: re-run `holmes-kit init` (doctor will point at the dead wiring).
|
|
12
|
+
- **`serverInfo.version` reports the real package version** — it was hardcoded to `0.1.0`, blinding client-side drift diagnosis.
|
|
13
|
+
|
|
14
|
+
### Changed
|
|
15
|
+
- **`approve` without a TTY names the next command (REQ-260)**: the implicit non-TTY fallback (e.g. Claude Code's `!` shell) used to print the pending list and exit 0 silently; it now appends runnable `--grant` / `--deny` / `--ask` examples carrying the first real pending id. Explicit `--list` output is unchanged for scripts.
|
|
16
|
+
|
|
8
17
|
## [0.1.17] - 2026-08-23
|
|
9
18
|
|
|
10
19
|
The two `main` lines — the Windows-compatibility line (12 commits, verified on Windows 11 and
|
package/dist/.build-id
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
|
|
1
|
+
f8c9769-mt5t8gnu
|
|
@@ -30,6 +30,17 @@ export declare function holdRequest(root: string, id: string, question: string,
|
|
|
30
30
|
* teaches syntax, humans start typing it.
|
|
31
31
|
*/
|
|
32
32
|
export declare function renderPending(state: QueueState): string;
|
|
33
|
+
/**
|
|
34
|
+
* The non-TTY fallback's next-command hint. Pure.
|
|
35
|
+
*
|
|
36
|
+
* @implements A-SPEC-260
|
|
37
|
+
* Without a TTY the interactive loop cannot run, and the silent list-then-exit-0 left the operator
|
|
38
|
+
* with no idea what to type (measured 2026-08-23 — Claude Code's `!` runs with a non-TTY stdin).
|
|
39
|
+
* The examples carry the FIRST pending id read from the queue, never a placeholder the operator
|
|
40
|
+
* must transcribe: [noguess]. An empty queue yields an empty hint — nothing to act on, nothing to
|
|
41
|
+
* advise. `renderPending` itself stays hint-free: the explicit `--list` output is for scripts.
|
|
42
|
+
*/
|
|
43
|
+
export declare function renderNonTtyHint(state: QueueState): string;
|
|
33
44
|
export interface ApproveIO {
|
|
34
45
|
print: (s: string) => void;
|
|
35
46
|
/** Ask one question, resolve with the raw answer. Injected so tests script the conversation. */
|
|
@@ -37,6 +37,7 @@ exports.grantRequest = grantRequest;
|
|
|
37
37
|
exports.denyRequest = denyRequest;
|
|
38
38
|
exports.holdRequest = holdRequest;
|
|
39
39
|
exports.renderPending = renderPending;
|
|
40
|
+
exports.renderNonTtyHint = renderNonTtyHint;
|
|
40
41
|
exports.runInteractive = runInteractive;
|
|
41
42
|
// @implements A-SPEC-246
|
|
42
43
|
const fs = __importStar(require("node:fs"));
|
|
@@ -154,6 +155,27 @@ function renderPending(state) {
|
|
|
154
155
|
lines.push(`(큐에 읽을 수 없는 줄 ${state.malformedLines}건 — 손상 여부를 확인하십시오)`);
|
|
155
156
|
return lines.join('\n');
|
|
156
157
|
}
|
|
158
|
+
/**
|
|
159
|
+
* The non-TTY fallback's next-command hint. Pure.
|
|
160
|
+
*
|
|
161
|
+
* @implements A-SPEC-260
|
|
162
|
+
* Without a TTY the interactive loop cannot run, and the silent list-then-exit-0 left the operator
|
|
163
|
+
* with no idea what to type (measured 2026-08-23 — Claude Code's `!` runs with a non-TTY stdin).
|
|
164
|
+
* The examples carry the FIRST pending id read from the queue, never a placeholder the operator
|
|
165
|
+
* must transcribe: [noguess]. An empty queue yields an empty hint — nothing to act on, nothing to
|
|
166
|
+
* advise. `renderPending` itself stays hint-free: the explicit `--list` output is for scripts.
|
|
167
|
+
*/
|
|
168
|
+
function renderNonTtyHint(state) {
|
|
169
|
+
if (state.pending.length === 0)
|
|
170
|
+
return '';
|
|
171
|
+
const id = state.pending[0].id;
|
|
172
|
+
return [
|
|
173
|
+
'대화형 결정은 TTY에서만 동작합니다 — 이 셸에서는 아래 비대화형 명령을 사용하십시오:',
|
|
174
|
+
` holmes-kit approve --grant ${id}`,
|
|
175
|
+
` holmes-kit approve --deny ${id} --reason "<사유>"`,
|
|
176
|
+
` holmes-kit approve --ask ${id} --question "<질문>"`,
|
|
177
|
+
].join('\n');
|
|
178
|
+
}
|
|
157
179
|
/**
|
|
158
180
|
* The interactive loop — one entry at a time, one letter per decision.
|
|
159
181
|
*
|
|
@@ -487,6 +487,38 @@ async function runDoctor(packageRoot, target, opts, extraChecks) {
|
|
|
487
487
|
}
|
|
488
488
|
}
|
|
489
489
|
catch { /* .mcp.json 부재/파싱 실패 → 이 점검 없이 진행 */ }
|
|
490
|
+
// @implements A-SPEC-259 — the wiring is judged by SPAWNING it. String comparison is exactly how
|
|
491
|
+
// the dead `npx -y <pkg>@<ver> holmes-mcp` form (npx resolves the default bin and hands
|
|
492
|
+
// `holmes-mcp` to it as an argument) passed the launcher test, this doctor's own handshake check
|
|
493
|
+
// (which starts the server directly, not through the wiring), and the release tarball probe —
|
|
494
|
+
// measured 2026-08-23. A node wiring is judged on path existence (a source checkout must not pay
|
|
495
|
+
// a spawn), an absent .mcp.json is skipped by name, and the spawn is BOUNDED: it succeeds or
|
|
496
|
+
// fails inside its deadline, never hangs (REQ-258's lesson, applied from this check onward).
|
|
497
|
+
{
|
|
498
|
+
const mcpPath = path.join(target, '.mcp.json');
|
|
499
|
+
if (!fs.existsSync(mcpPath)) {
|
|
500
|
+
add('mcp wiring spawn', 'PASS', 'skipped — no .mcp.json in the target: an absent wiring is not a dead one');
|
|
501
|
+
}
|
|
502
|
+
else {
|
|
503
|
+
try {
|
|
504
|
+
const server = JSON.parse(fs.readFileSync(mcpPath, 'utf8'))
|
|
505
|
+
.mcpServers?.[init_1.SERVER_NAME];
|
|
506
|
+
if (!server || typeof server.command !== 'string' || !Array.isArray(server.args)) {
|
|
507
|
+
add('mcp wiring spawn', 'WARN', `.mcp.json carries no runnable ${init_1.SERVER_NAME} entry`, 'Run `holmes-kit init` in the target to write the wiring.');
|
|
508
|
+
}
|
|
509
|
+
else if (server.command === 'node') {
|
|
510
|
+
const bin = server.args[0] ?? '';
|
|
511
|
+
add('mcp wiring spawn', fs.existsSync(bin) ? 'PASS' : 'FAIL', fs.existsSync(bin) ? `node wiring resolves: ${bin}` : `node wiring points at a missing file: ${bin}`, fs.existsSync(bin) ? undefined : 'Run `holmes-kit init` in the target to rewire the absolute path.');
|
|
512
|
+
}
|
|
513
|
+
else {
|
|
514
|
+
checks.push(await wiringSpawnCheck(server.command, server.args));
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
catch {
|
|
518
|
+
add('mcp wiring spawn', 'WARN', 'could not read .mcp.json', 'Run `holmes-kit init` in the target to rewrite the wiring.');
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
}
|
|
490
522
|
// @implements A-SPEC-193 — 배선된 하네스마다 그 하네스의 배선을 검사한다. 배선되지 않은
|
|
491
523
|
// 하네스는 진단하지 않는다(없는 것을 결함이라 부르면 doctor 가 소음이 된다).
|
|
492
524
|
const agyHooks = path.join(target, '.agents', 'hooks.json');
|
|
@@ -616,6 +648,54 @@ function cleanupOnSignal(dir) {
|
|
|
616
648
|
* and closes stdin makes the server exit before answering (a false FAIL this check produced on its
|
|
617
649
|
* very first run against a healthy server).
|
|
618
650
|
*/
|
|
651
|
+
// @implements A-SPEC-259
|
|
652
|
+
// Spawn the TARGET's wiring verbatim and require initialize inside the deadline. Unlike
|
|
653
|
+
// `mcpHandshakeCheck` below (which proves this INSTALL can serve), this proves the WIRING the
|
|
654
|
+
// harness will actually run reaches a server — the gap the dead npx form lived in.
|
|
655
|
+
function wiringSpawnCheck(command, args, timeoutMs = 30000) {
|
|
656
|
+
const quoted = `${command} ${args.join(' ')}`;
|
|
657
|
+
return new Promise((resolve) => {
|
|
658
|
+
let done = false;
|
|
659
|
+
let out = '';
|
|
660
|
+
let child;
|
|
661
|
+
const finish = (level, detail, fix) => {
|
|
662
|
+
if (done)
|
|
663
|
+
return;
|
|
664
|
+
done = true;
|
|
665
|
+
clearTimeout(timer);
|
|
666
|
+
try {
|
|
667
|
+
child.kill();
|
|
668
|
+
}
|
|
669
|
+
catch { /* already gone */ }
|
|
670
|
+
resolve({ name: 'mcp wiring spawn', level, detail, fix });
|
|
671
|
+
};
|
|
672
|
+
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, then re-run doctor.'), timeoutMs);
|
|
673
|
+
try {
|
|
674
|
+
child = (0, node_child_process_1.spawn)(command, args, { stdio: ['pipe', 'pipe', 'pipe'] });
|
|
675
|
+
}
|
|
676
|
+
catch (e) {
|
|
677
|
+
finish('FAIL', `wiring could not be spawned: \`${quoted}\` — ${e.message}`, 'Re-run `holmes-kit init` in the target to rewrite the wiring.');
|
|
678
|
+
return;
|
|
679
|
+
}
|
|
680
|
+
child.on('error', (e) => finish('FAIL', `wiring could not be spawned: \`${quoted}\` — ${e.message}`, 'Re-run `holmes-kit init` in the target to rewrite the wiring.'));
|
|
681
|
+
child.on('exit', (code) => finish('FAIL', `wiring exited (code ${code}) before answering initialize: \`${quoted}\``, 'Re-run `holmes-kit init` in the target to rewrite the wiring.'));
|
|
682
|
+
child.stdin?.on('error', () => { });
|
|
683
|
+
child.stdout?.on('data', (d) => {
|
|
684
|
+
out += String(d);
|
|
685
|
+
for (const line of out.split('\n')) {
|
|
686
|
+
try {
|
|
687
|
+
const m = JSON.parse(line);
|
|
688
|
+
if (m?.result?.serverInfo) {
|
|
689
|
+
finish('PASS', `wiring reaches a live server (v${m.result.serverInfo.version ?? '?'}): \`${quoted}\``);
|
|
690
|
+
return;
|
|
691
|
+
}
|
|
692
|
+
}
|
|
693
|
+
catch { /* partial line */ }
|
|
694
|
+
}
|
|
695
|
+
});
|
|
696
|
+
child.stdin?.write(JSON.stringify({ jsonrpc: '2.0', id: 1, method: 'initialize', params: { protocolVersion: '2024-11-05', capabilities: {}, clientInfo: { name: 'holmes-doctor', version: '1' } } }) + '\n');
|
|
697
|
+
});
|
|
698
|
+
}
|
|
619
699
|
function mcpHandshakeCheck(packageRoot, timeoutMs = 15000) {
|
|
620
700
|
return new Promise((resolve) => {
|
|
621
701
|
const mcpCwd = fs.mkdtempSync(path.join(os.tmpdir(), 'holmes-doctor-mcp-'));
|
package/dist/holmes/cli/index.js
CHANGED
|
@@ -359,7 +359,7 @@ async function main(argv) {
|
|
|
359
359
|
// @implements A-SPEC-246 — the surface where a human decides. Interactive on a TTY with no
|
|
360
360
|
// decision flags; every interactive decision has a flag equivalent so a CI or remote operator
|
|
361
361
|
// is never locked out.
|
|
362
|
-
const { grantRequest, denyRequest, holdRequest, renderPending, runInteractive } = require('./approve');
|
|
362
|
+
const { grantRequest, denyRequest, holdRequest, renderPending, renderNonTtyHint, runInteractive } = require('./approve');
|
|
363
363
|
const { readQueue } = require('../governance/approval-queue');
|
|
364
364
|
const root = typeof flags.target === 'string' ? path.resolve(flags.target) : process.cwd();
|
|
365
365
|
const actor = (() => { try {
|
|
@@ -402,10 +402,18 @@ async function main(argv) {
|
|
|
402
402
|
process.stdout.write(r.ok ? '✓ 보류 — 질문이 다음 거부 문면에 실립니다\n' : `✗ ${r.reason}\n`);
|
|
403
403
|
return r.ok ? 0 : 1;
|
|
404
404
|
}
|
|
405
|
-
if (flags.list
|
|
405
|
+
if (flags.list) {
|
|
406
406
|
process.stdout.write(renderPending(readQueue(root)) + '\n');
|
|
407
407
|
return 0;
|
|
408
408
|
}
|
|
409
|
+
// @implements A-SPEC-260 — the implicit non-TTY fallback tells the operator the next command,
|
|
410
|
+
// with the real pending id filled in; the explicit --list above stays script-clean.
|
|
411
|
+
if (!process.stdin.isTTY) {
|
|
412
|
+
const state = readQueue(root);
|
|
413
|
+
const hint = renderNonTtyHint(state);
|
|
414
|
+
process.stdout.write(renderPending(state) + (hint ? '\n\n' + hint : '') + '\n');
|
|
415
|
+
return 0;
|
|
416
|
+
}
|
|
409
417
|
const rl = require('node:readline/promises').createInterface({ input: process.stdin, output: process.stdout });
|
|
410
418
|
void runInteractive(root, {
|
|
411
419
|
print: (t) => process.stdout.write(t + '\n'),
|
|
@@ -52,7 +52,12 @@ exports.MCP_PACKAGE = '@holmes-lab/holmes-kit';
|
|
|
52
52
|
*/
|
|
53
53
|
function mcpServerEntry(opts) {
|
|
54
54
|
if (opts.mode === 'npx') {
|
|
55
|
-
|
|
55
|
+
// @implements A-SPEC-259 — the package must ride `-p` with the command named separately.
|
|
56
|
+
// `npx <pkg> holmes-mcp` runs the package-name-matching DEFAULT bin (holmes-kit) and hands
|
|
57
|
+
// `holmes-mcp` to it as an argument: measured 2026-08-23, exit 1 "unknown command" — every
|
|
58
|
+
// npm-installed wiring since the npx pin was a dead server. The `-p` form is the one that
|
|
59
|
+
// reached initialize in the same measurement.
|
|
60
|
+
return { command: 'npx', args: ['-y', '-p', `${exports.MCP_PACKAGE}@${opts.version}`, 'holmes-mcp'] };
|
|
56
61
|
}
|
|
57
62
|
return { command: 'node', args: [opts.mcpBinPath] };
|
|
58
63
|
}
|
|
@@ -12,7 +12,19 @@ const validate_args_1 = require("./validate-args");
|
|
|
12
12
|
// @implements A-SPEC-100.2
|
|
13
13
|
const store = new spec_store_1.LocalMarkdownRepository(process.env.HOLMES_SPECS ?? '.ax/specs');
|
|
14
14
|
const handlers = (0, handlers_1.makeHandlers)(store);
|
|
15
|
-
|
|
15
|
+
// @implements A-SPEC-259 — the advertised version is the package's own, not a literal that froze at
|
|
16
|
+
// 0.1.0: a hardcoded serverInfo.version blinds any client-side drift diagnosis.
|
|
17
|
+
const PKG_VERSION = (() => {
|
|
18
|
+
try {
|
|
19
|
+
// dist/holmes/mcp/server.js → three levels up is the package root, same as the source layout.
|
|
20
|
+
const raw = require('node:fs').readFileSync(require('node:path').join(__dirname, '..', '..', '..', 'package.json'), 'utf8');
|
|
21
|
+
return JSON.parse(raw).version ?? '0.0.0';
|
|
22
|
+
}
|
|
23
|
+
catch {
|
|
24
|
+
return '0.0.0';
|
|
25
|
+
}
|
|
26
|
+
})();
|
|
27
|
+
const server = new index_js_1.Server({ name: 'holmes-kit', version: PKG_VERSION }, { capabilities: { tools: {} } });
|
|
16
28
|
const fullProfile = process.env.HOLMES_MCP_PROFILE === 'full';
|
|
17
29
|
// Register each handler as a tool with its real typed inputSchema so MCP
|
|
18
30
|
// clients can marshal complex (array/object) arguments; fall back to a
|
|
@@ -765,7 +765,7 @@ function renderDashboardHtml() {
|
|
|
765
765
|
items.forEach(s => { statuses[s.status] = (statuses[s.status] || 0) + 1; });
|
|
766
766
|
const statusSummary = Object.keys(statuses).map(k => escapeHtml(k) + ' × ' + statuses[k]).join(' · ');
|
|
767
767
|
html += '<div class="req-card" style="margin-bottom:20px;">'
|
|
768
|
-
+ '<div class="req-header" onclick="toggleAccordion(
|
|
768
|
+
+ '<div class="req-header" onclick="toggleAccordion(\\'legacy-body-' + escapeHtml(kind) + '\\')">'
|
|
769
769
|
+ '<div class="req-title-group"><span class="req-id-badge">' + escapeHtml(kind) + '</span>'
|
|
770
770
|
+ '<span class="req-title">' + items.length + ' unmapped document' + (items.length === 1 ? '' : 's') + '</span>'
|
|
771
771
|
+ '<span class="neighbor-rel badge-unmeasured">' + statusSummary + '</span></div>'
|
|
@@ -851,7 +851,7 @@ function renderDashboardHtml() {
|
|
|
851
851
|
if (!label) {
|
|
852
852
|
return '<td class="' + cls + ' stage-missing" title="No artifact for this stage">' + arrow + '— missing</td>';
|
|
853
853
|
}
|
|
854
|
-
const onclick = jumpId ? ' onclick="focusSpecInGraph(
|
|
854
|
+
const onclick = jumpId ? ' onclick="focusSpecInGraph(\\'' + escapeHtml(String(jumpId)) + '\\')"' : '';
|
|
855
855
|
return '<td class="' + cls + '"' + onclick + '>' + arrow + icon + ' ' + escapeHtml(String(label)) + '</td>';
|
|
856
856
|
}
|
|
857
857
|
// REQ-Grouped Structured 6-Stage Pipeline Heatmap Matrix Renderer
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"//": "@implements A-SPEC-209",
|
|
3
3
|
"name": "@holmes-lab/holmes-kit",
|
|
4
|
-
"version": "0.1.
|
|
4
|
+
"version": "0.1.18",
|
|
5
5
|
"description": "Holmes-Kit — deterministic Agentic Software Engineering (ASE) harness with causal traceability (spec chain + D-CPG + RTM + phase guardrail)",
|
|
6
6
|
"main": "dist/holmes/mcp/server.js",
|
|
7
7
|
"types": "dist/holmes/mcp/server.d.ts",
|