@holmes-lab/holmes-kit 0.1.13 → 0.1.15

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 CHANGED
@@ -4,6 +4,50 @@ All notable changes to this project will be documented in this file.
4
4
 
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
+ <!-- @implements A-SPEC-209 -->
8
+ ## [0.1.15] - 2026-08-23
9
+
10
+ The stale-install trap, fixed at the root and made visible. A Windows user upgraded holmes-kit but
11
+ their MCP kept launching 0.1.9: `.mcp.json` pinned the global install by absolute path, and `-g`
12
+ upgrades EPERM on Windows so the global never moved.
13
+
14
+ ### Added
15
+ - **npx-pinned MCP wiring (REQ-1404)**: `init` now wires the holmes-kit MCP server as
16
+ `npx -y @holmes-lab/holmes-kit@<exact-version> holmes-mcp` for installed packages — no global
17
+ install, no `-g` (no EPERM), no absolute-path drift. It auto-detects: an installed packageRoot
18
+ (under `node_modules/@holmes-lab/holmes-kit`) wires npx; a source checkout keeps `node` against
19
+ local `dist`. `--mcp-launcher npx|node` overrides; an unknown value is refused, not defaulted.
20
+ The pin is the exact version init ran from, so an upgrade happens only on an explicit
21
+ `npx @holmes-lab/holmes-kit@latest init` — never silently. All three harness wirings
22
+ (Claude/antigravity/codex) share one launch contract via `mcpEntryForInstall`.
23
+ - **doctor sees MCP version drift, offline (REQ-1405)**: `doctor` reads the version its
24
+ `.mcp.json` will ACTUALLY launch — from the npx pin, or a node wiring's LOCAL package.json — and
25
+ WARNs when it differs from the CLI, naming both versions and the `init` fix. No registry call:
26
+ the zero-outbound stance holds. This is the diagnostic that would have named the 0.1.9 stall.
27
+
28
+ ### Changed
29
+ - `mergeMcpServers(existing, name, entry, specsDir)` takes a computed `{command, args}` entry
30
+ instead of a bin path — env preservation (A-SPEC-179) is unchanged.
31
+
32
+ <!-- @implements A-SPEC-209 -->
33
+ ## [0.1.14] - 2026-08-23
34
+
35
+ A defect found by installing 0.1.13 from the registry and driving the shipped HITL cycle:
36
+ `holmes-kit approve --help` printed the whole-CLI usage instead of its own. The approve
37
+ subcommand — the human decision surface — had its help swallowed at its first touch.
38
+
39
+ ### Fixed
40
+ - **`approve --help` reaches APPROVE_USAGE (A-SPEC-171)**: the global `--help` handler fired
41
+ before approve's dispatch (which carries its own usage) could run, so it printed the top-level
42
+ usage over it — approve dispatches last only because it was added last. A declarative
43
+ `SELF_HELP_COMMANDS` set now exempts any subcommand that renders its own usage, so the fix does
44
+ not depend on dispatch order. Two reproducing tests (RED before, GREEN after).
45
+ - **Repo hygiene: a stray `C:/proj/.ax` was tracked (A-SPEC-237/§25a)**: the Windows path tests
46
+ pass `projectRoot: 'C:/proj'`, which on POSIX resolves as a relative path into the repo. A
47
+ pre-§25a residue directory had been committed in 0.1.13 and kept growing on every suite run;
48
+ removed, with a `.gitignore` guard against drive-letter directories. Verified the §25a
49
+ enqueue-only-under-existing-.ax guard now prevents recreation — the suite no longer plants it.
50
+
7
51
  <!-- @implements A-SPEC-209 -->
8
52
  ## [0.1.13] - 2026-08-23
9
53
 
package/dist/.build-id CHANGED
@@ -1 +1 @@
1
- 0c506c6-mt5buls8
1
+ d6b14ca-mt5ea641
@@ -1,3 +1,4 @@
1
+ import { LauncherMode } from './mcp-launcher';
1
2
  /**
2
3
  * 하네스별 배선물.
3
4
  *
@@ -29,6 +30,7 @@ export interface AgentWiringOptions {
29
30
  target: string;
30
31
  packageRoot: string;
31
32
  specsDir: string;
33
+ launcher?: LauncherMode;
32
34
  }
33
35
  export interface AgentFile {
34
36
  path: string;
@@ -38,6 +38,7 @@ exports.agentFiles = agentFiles;
38
38
  exports.agentLinks = agentLinks;
39
39
  // @implements A-SPEC-193
40
40
  const path = __importStar(require("node:path"));
41
+ const mcp_launcher_1 = require("./mcp-launcher");
41
42
  /**
42
43
  * 하네스별 배선물.
43
44
  *
@@ -69,15 +70,21 @@ exports.HARNESS_ENFORCES = {
69
70
  codex: false,
70
71
  };
71
72
  const q = (p) => p;
72
- const mcpConfig = (packageRoot, specsDir) => `${JSON.stringify({
73
- mcpServers: {
74
- 'holmes-kit': {
75
- command: 'node',
76
- args: [q(path.join(packageRoot, 'bin', 'holmes-mcp.js'))],
77
- env: { HOLMES_SPECS: specsDir },
73
+ // @implements A-SPEC-1404 same launch contract as the Claude wiring: npx-pin for installed
74
+ // packageRoots, node for source checkouts. Shared via mcpEntryForInstall so all three harnesses
75
+ // move together.
76
+ const mcpConfig = (packageRoot, specsDir, launcher) => {
77
+ const entry = (0, mcp_launcher_1.mcpEntryForInstall)({
78
+ packageRoot,
79
+ mcpBinPath: q(path.join(packageRoot, 'bin', 'holmes-mcp.js')),
80
+ flag: launcher,
81
+ });
82
+ return `${JSON.stringify({
83
+ mcpServers: {
84
+ 'holmes-kit': { command: entry.command, args: entry.args, env: { HOLMES_SPECS: specsDir } },
78
85
  },
79
- },
80
- }, null, 2)}\n`;
86
+ }, null, 2)}\n`;
87
+ };
81
88
  /**
82
89
  * Antigravity 훅 배선.
83
90
  *
@@ -152,12 +159,12 @@ function agentFiles(agent, opts) {
152
159
  case 'antigravity':
153
160
  return [
154
161
  { path: path.join(target, '.agents', 'hooks.json'), content: hooksJson(packageRoot) },
155
- { path: path.join(target, '.agents', 'mcp_config.json'), content: mcpConfig(packageRoot, specsDir) },
162
+ { path: path.join(target, '.agents', 'mcp_config.json'), content: mcpConfig(packageRoot, specsDir, opts.launcher) },
156
163
  { path: path.join(target, 'AGENTS.md'), content: AGENTS_MD(exports.HARNESS_ENFORCES.antigravity) },
157
164
  ];
158
165
  case 'codex':
159
166
  return [
160
- { path: path.join(target, '.codex', 'mcp_config.json'), content: mcpConfig(packageRoot, specsDir) },
167
+ { path: path.join(target, '.codex', 'mcp_config.json'), content: mcpConfig(packageRoot, specsDir, opts.launcher) },
161
168
  { path: path.join(target, 'AGENTS.md'), content: AGENTS_MD(exports.HARNESS_ENFORCES.codex) },
162
169
  ];
163
170
  default:
@@ -50,6 +50,8 @@ const os = __importStar(require("node:os"));
50
50
  const settings_merge_1 = require("./settings-merge");
51
51
  const playbook_skills_1 = require("./playbook-skills");
52
52
  const init_1 = require("./init");
53
+ const mcp_version_1 = require("./mcp-version");
54
+ const mcp_launcher_1 = require("./mcp-launcher");
53
55
  const GRAMMARS = [
54
56
  'tree-sitter-typescript', 'tree-sitter-python', 'tree-sitter-c-sharp', 'tree-sitter-java',
55
57
  'tree-sitter-go', 'tree-sitter-rust', 'tree-sitter-cpp',
@@ -454,6 +456,37 @@ async function runDoctor(packageRoot, target, opts, extraChecks) {
454
456
  else {
455
457
  add('approval channel', 'WARN', '이 프로세스에는 HOLMES_APPROVAL이 있으나 .mcp.json의 서버 환경에는 없습니다 — spec_approve는 서버 환경을 읽습니다', 'settings.local.json의 env가 MCP 서버까지 전달된다는 보장은 없습니다. 그 변수를 설정한 셸에서 Claude Code를 띄우면 자식 서버가 상속합니다.');
456
458
  }
459
+ // @implements A-SPEC-1405 — the version this project's MCP server will ACTUALLY launch, vs this
460
+ // CLI. The stale-global trap (measured 2026-08-23: .mcp.json pinned a 0.1.9 global while the user
461
+ // believed they had upgraded) was invisible because nothing compared them. Offline: the launch
462
+ // version comes from the wiring string, or a node wiring's LOCAL package.json — never the registry.
463
+ try {
464
+ const raw = fs.readFileSync(path.join(target, '.mcp.json'), 'utf8');
465
+ const server = JSON.parse(raw)
466
+ .mcpServers?.[init_1.SERVER_NAME];
467
+ const cli = (0, mcp_launcher_1.readPackageVersion)(packageRoot);
468
+ if (server && typeof server.command === 'string' && Array.isArray(server.args) && cli) {
469
+ const readPkgVersionAt = (dir) => {
470
+ try {
471
+ const pkg = JSON.parse(fs.readFileSync(path.join(dir, 'package.json'), 'utf8'));
472
+ return pkg?.name === '@holmes-lab/holmes-kit' && typeof pkg.version === 'string' ? pkg.version : undefined;
473
+ }
474
+ catch {
475
+ return undefined;
476
+ }
477
+ };
478
+ const launch = (0, mcp_version_1.mcpLaunchVersion)({ command: server.command, args: server.args }, readPkgVersionAt);
479
+ const verdict = (0, mcp_version_1.versionDriftVerdict)(launch, cli);
480
+ if (verdict === 'drift') {
481
+ add('mcp server version', 'WARN', `이 프로젝트의 MCP 서버는 holmes-kit ${launch} 를 띄우지만 이 CLI 는 ${cli} 입니다 — 재배선 전까지 옛 서버가 계속 뜹니다`, 'npx @holmes-lab/holmes-kit@latest init 을 다시 실행해 .mcp.json 을 최신 버전으로 재배선하십시오 (전역 -g 업그레이드 불필요).');
482
+ }
483
+ else if (verdict === 'match') {
484
+ add('mcp server version', 'PASS', `MCP 서버 버전 ${launch} 가 이 CLI 와 일치합니다`);
485
+ }
486
+ // 'unknown' → 항목 없음: 판정 불능을 결함으로 만들지 않는다.
487
+ }
488
+ }
489
+ catch { /* .mcp.json 부재/파싱 실패 → 이 점검 없이 진행 */ }
457
490
  // @implements A-SPEC-193 — 배선된 하네스마다 그 하네스의 배선을 검사한다. 배선되지 않은
458
491
  // 하네스는 진단하지 않는다(없는 것을 결함이라 부르면 doctor 가 소음이 된다).
459
492
  const agyHooks = path.join(target, '.agents', 'hooks.json');
@@ -69,13 +69,19 @@ function packageRoot() {
69
69
  * REQ-144's "cannot decide is not permission", applied to CLI arguments.
70
70
  */
71
71
  const KNOWN_FLAGS = {
72
- init: ['help', 'target', 'mode', 'specs-dir', 'settings', 'matcher', 'dry-run', 'no-mcp', 'remove', 'force', 'agent'],
72
+ init: ['help', 'target', 'mode', 'specs-dir', 'settings', 'matcher', 'dry-run', 'no-mcp', 'remove', 'force', 'agent', 'mcp-launcher'],
73
73
  doctor: ['help', 'target', 'json'],
74
74
  skills: ['help', 'target'],
75
75
  ci: ['help', 'target', 'specs-dir', 'json'],
76
76
  serve: ['help', 'target', 'port'],
77
77
  approve: ['help', 'target', 'list', 'grant', 'deny', 'ask', 'reason', 'question', 'ttl', 'rationale'],
78
78
  };
79
+ // @implements A-SPEC-171 — subcommands that render their OWN usage on `--help`. A-SPEC-171 governs
80
+ // "help before any side effect"; a command whose usage lives past this handler (approve, whose
81
+ // APPROVE_USAGE is emitted in its own dispatch) must be exempted here or the global handler prints
82
+ // the whole-CLI usage over it. This set is that exemption, kept declarative so a future self-help
83
+ // command is one edit, not a dispatch-order gamble.
84
+ const SELF_HELP_COMMANDS = new Set(['approve']);
79
85
  const APPROVE_USAGE = `holmes-kit approve — 승인 대기 요청의 결재 (HITL)
80
86
 
81
87
  대화형(TTY): 항목마다 [a]승인 [d]거부 [q]질문 [s]건너뛰기
@@ -237,8 +243,10 @@ async function main(argv) {
237
243
  }
238
244
  // @implements A-SPEC-171
239
245
  // BEFORE any side effect. The measured defect was not that help was missing but that the writing
240
- // happened first, so the order here is the fix.
241
- if (knownFlagsFor(cmd) && flags.help) {
246
+ // happened first, so the order here is the fix. Measured again on 0.1.13: `approve --help` printed
247
+ // the whole-CLI usage because this handler fired before approve's dispatch (which carries its own
248
+ // APPROVE_USAGE) could run — so a command in SELF_HELP_COMMANDS is deferred to, not printed over.
249
+ if (knownFlagsFor(cmd) && flags.help && !SELF_HELP_COMMANDS.has(cmd)) {
242
250
  process.stdout.write(USAGE);
243
251
  return 0;
244
252
  }
@@ -421,6 +429,16 @@ async function main(argv) {
421
429
  }
422
430
  if (cmd === 'init') {
423
431
  const mode = (typeof flags.mode === 'string' ? flags.mode : 'guardrail');
432
+ // @implements A-SPEC-1404 — an unknown launch mode is refused, not defaulted: substituting a
433
+ // default acts where the operator did not point (REQ-144). Only npx|node are wiring modes.
434
+ let mcpLauncher;
435
+ if (typeof flags['mcp-launcher'] === 'string') {
436
+ if (flags['mcp-launcher'] !== 'npx' && flags['mcp-launcher'] !== 'node') {
437
+ process.stderr.write(`--mcp-launcher must be 'npx' or 'node'\n\n${USAGE}`);
438
+ return 2;
439
+ }
440
+ mcpLauncher = flags['mcp-launcher'];
441
+ }
424
442
  // 형제 열거 오류와 같은 모양(round 7): --settings·--target 은 USAGE 와 함께 exit 2 인데
425
443
  // --mode 만 맨몸 exit 1 이었다 — 같은 실수의 진단이 두 벌이면 스크립트가 갈린다.
426
444
  if (mode !== 'guardrail' && mode !== 'governed') {
@@ -496,6 +514,7 @@ async function main(argv) {
496
514
  force: flags.force === true,
497
515
  remove: flags.remove === true,
498
516
  approval: readApproval(),
517
+ mcpLauncher,
499
518
  };
500
519
  const res = (0, init_1.runInit)(opts);
501
520
  for (const m of res.messages)
@@ -1,5 +1,6 @@
1
1
  import { HookPlan } from './settings-merge';
2
2
  import { Agent } from './agents';
3
+ import { LauncherMode } from './mcp-launcher';
3
4
  import { Approval } from '../guardrail/risk-gate';
4
5
  /**
5
6
  * `holmes-kit init` — wire holmes-kit into a TARGET project (the I/O half; all merge logic is pure
@@ -29,6 +30,7 @@ export interface InitOptions {
29
30
  approval?: Approval;
30
31
  agents?: Agent[];
31
32
  allowAdditive?: boolean;
33
+ mcpLauncher?: LauncherMode;
32
34
  }
33
35
  export interface FileChange {
34
36
  path: string;
@@ -45,6 +45,7 @@ const settings_merge_2 = require("./settings-merge");
45
45
  const playbook_skills_1 = require("./playbook-skills");
46
46
  const agents_1 = require("./agents");
47
47
  const roles_readme_1 = require("./roles-readme");
48
+ const mcp_launcher_1 = require("./mcp-launcher");
48
49
  const pre_tool_use_1 = require("../hooks/pre-tool-use");
49
50
  const governed_precondition_1 = require("./governed-precondition");
50
51
  const risk_gate_1 = require("../guardrail/risk-gate");
@@ -309,7 +310,10 @@ function runInit(opts) {
309
310
  const kept = Object.keys(prevEnv).filter((k) => !settings_merge_2.HOLMES_OWNED_MCP_ENV.includes(k));
310
311
  if (kept.length > 0)
311
312
  messages.push(`Preserved your MCP server env: ${kept.join(', ')}`);
312
- changes.push({ path: mcpPath, before: m.raw, after: JSON.stringify((0, settings_merge_1.mergeMcpServers)(m.value, exports.SERVER_NAME, mcpBin, opts.specsDir), null, 2) + '\n' });
313
+ // @implements A-SPEC-1404 compute the launch entry once (npx-pin for installs, node for source),
314
+ // then merge; env preservation is unchanged.
315
+ const mcpEntry = (0, mcp_launcher_1.mcpEntryForInstall)({ packageRoot: opts.packageRoot, mcpBinPath: mcpBin, flag: opts.mcpLauncher });
316
+ changes.push({ path: mcpPath, before: m.raw, after: JSON.stringify((0, settings_merge_1.mergeMcpServers)(m.value, exports.SERVER_NAME, mcpEntry, opts.specsDir), null, 2) + '\n' });
313
317
  }
314
318
  changes.push({ path: settingsPath, before: s.raw, after: JSON.stringify(settings, null, 2) + '\n' });
315
319
  const gitignoreAfter = (0, gitignore_merge_1.mergeGitignore)(gitignoreBefore);
@@ -357,7 +361,7 @@ function runInit(opts) {
357
361
  // 같은 집합을 예고한다(A-SPEC-190 §9 의 규율). Claude 는 위에서 이미 배선했으므로 빈 목록이다.
358
362
  if (!opts.remove) {
359
363
  for (const agent of opts.agents ?? []) {
360
- for (const f of (0, agents_1.agentFiles)(agent, { target: opts.target, packageRoot: opts.packageRoot, specsDir: opts.specsDir })) {
364
+ for (const f of (0, agents_1.agentFiles)(agent, { target: opts.target, packageRoot: opts.packageRoot, specsDir: opts.specsDir, launcher: opts.mcpLauncher })) {
361
365
  const before = fs.existsSync(f.path) ? fs.readFileSync(f.path, 'utf8') : null;
362
366
  changes.push({ path: f.path, before, after: f.content });
363
367
  }
@@ -0,0 +1,55 @@
1
+ /**
2
+ * MCP 서버 배선의 launch 방식을 한 곳에서 계산한다.
3
+ *
4
+ * 왜 한 곳인가: `.mcp.json`(Claude)·`.agents/mcp_config.json`(antigravity)·`.codex/mcp_config.json`
5
+ * 세 배선이 같은 서버를 띄운다. 셋이 각자 command/args 를 지으면 하나가 npx 로 옮겨갈 때 나머지가
6
+ * 절대경로에 남아 어긋난다 — 그 드리프트가 REQ-1404 자체의 출발점이었다.
7
+ *
8
+ * 왜 두 모드인가: 최종 사용자는 npm 설치본에서 init 하므로 `npx ...@<정확한버전> holmes-mcp` 가
9
+ * 옳다 — 전역 설치도 `-g` 도 필요 없고(Windows EPERM 회피), 절대경로 고착이 없다. 그러나 개발
10
+ * 저장소(소스 체크아웃)는 아직 발행되지 않은 working tree 를 쓰므로 npx 로 레지스트리를 당기면 안
11
+ * 된다 — 로컬 dist 를 직접 부르는 node 배선이 맞다. 그래서 기본은 packageRoot 성격으로 자동 판별한다.
12
+ */
13
+ export type LauncherMode = 'npx' | 'node';
14
+ export interface McpServerEntry {
15
+ command: string;
16
+ args: string[];
17
+ }
18
+ /** npm 상의 정식 패키지 이름. 핀은 이 이름에 정확한 버전을 붙인다. */
19
+ export declare const MCP_PACKAGE = "@holmes-lab/holmes-kit";
20
+ /**
21
+ * 배선 entry 를 낸다 — 순수.
22
+ *
23
+ * npx 핀은 `version` 을 **그대로** 붙인다(정규화·범위화 없음): 프리릴리스·빌드메타 문자열도
24
+ * 있는 그대로 핀해야 init 을 실행한 그 버전이 재현된다. `version` 유무 판정은 상위(배선 지점)가
25
+ * 하고, 여기서는 받은 것을 박기만 한다.
26
+ */
27
+ export declare function mcpServerEntry(opts: {
28
+ mode: LauncherMode;
29
+ version: string;
30
+ mcpBinPath: string;
31
+ }): McpServerEntry;
32
+ /**
33
+ * 기본 모드를 정한다 — 순수, 경로 문자열만 본다(파일시스템 조회 없음).
34
+ *
35
+ * `flag` 가 명시되면 그대로. 없으면 packageRoot 가 설치본 세그먼트를 포함하면 npx, 아니면 node.
36
+ * Windows 구분자(`\`)를 `/` 로 정규화해 비교하므로 두 OS 의 설치 경로가 같게 인식된다.
37
+ */
38
+ export declare function resolveLauncherMode(opts: {
39
+ flag?: LauncherMode;
40
+ packageRoot: string;
41
+ }): LauncherMode;
42
+ /** packageRoot/package.json 의 version. 못 읽으면 undefined — 이것이 node 물러남을 부른다. */
43
+ export declare function readPackageVersion(packageRoot: string): string | undefined;
44
+ /**
45
+ * 세 배선(Claude/antigravity/codex)이 공유하는 조립: 모드 판별 → 버전 읽기 → entry.
46
+ *
47
+ * npx 로 판별됐어도 버전을 못 읽으면 node 로 물러난다 — 빈 핀(`@holmes-lab/holmes-kit@`)을 박느니
48
+ * 로컬 bin 을 부르는 편이 유효하다(A-SPEC-1404 경계). `readVersion` 은 주입 가능해 시험이 fs 없이 돈다.
49
+ */
50
+ export declare function mcpEntryForInstall(opts: {
51
+ packageRoot: string;
52
+ mcpBinPath: string;
53
+ flag?: LauncherMode;
54
+ readVersion?: (packageRoot: string) => string | undefined;
55
+ }): McpServerEntry;
@@ -0,0 +1,96 @@
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.MCP_PACKAGE = void 0;
37
+ exports.mcpServerEntry = mcpServerEntry;
38
+ exports.resolveLauncherMode = resolveLauncherMode;
39
+ exports.readPackageVersion = readPackageVersion;
40
+ exports.mcpEntryForInstall = mcpEntryForInstall;
41
+ // @implements A-SPEC-1404
42
+ const fs = __importStar(require("node:fs"));
43
+ const path = __importStar(require("node:path"));
44
+ /** npm 상의 정식 패키지 이름. 핀은 이 이름에 정확한 버전을 붙인다. */
45
+ exports.MCP_PACKAGE = '@holmes-lab/holmes-kit';
46
+ /**
47
+ * 배선 entry 를 낸다 — 순수.
48
+ *
49
+ * npx 핀은 `version` 을 **그대로** 붙인다(정규화·범위화 없음): 프리릴리스·빌드메타 문자열도
50
+ * 있는 그대로 핀해야 init 을 실행한 그 버전이 재현된다. `version` 유무 판정은 상위(배선 지점)가
51
+ * 하고, 여기서는 받은 것을 박기만 한다.
52
+ */
53
+ function mcpServerEntry(opts) {
54
+ if (opts.mode === 'npx') {
55
+ return { command: 'npx', args: ['-y', `${exports.MCP_PACKAGE}@${opts.version}`, 'holmes-mcp'] };
56
+ }
57
+ return { command: 'node', args: [opts.mcpBinPath] };
58
+ }
59
+ const INSTALLED_SEGMENT = 'node_modules/@holmes-lab/holmes-kit';
60
+ /**
61
+ * 기본 모드를 정한다 — 순수, 경로 문자열만 본다(파일시스템 조회 없음).
62
+ *
63
+ * `flag` 가 명시되면 그대로. 없으면 packageRoot 가 설치본 세그먼트를 포함하면 npx, 아니면 node.
64
+ * Windows 구분자(`\`)를 `/` 로 정규화해 비교하므로 두 OS 의 설치 경로가 같게 인식된다.
65
+ */
66
+ function resolveLauncherMode(opts) {
67
+ if (opts.flag)
68
+ return opts.flag;
69
+ const normalized = opts.packageRoot.replace(/\\/g, '/');
70
+ return normalized.includes(INSTALLED_SEGMENT) ? 'npx' : 'node';
71
+ }
72
+ /** packageRoot/package.json 의 version. 못 읽으면 undefined — 이것이 node 물러남을 부른다. */
73
+ function readPackageVersion(packageRoot) {
74
+ try {
75
+ const v = JSON.parse(fs.readFileSync(path.join(packageRoot, 'package.json'), 'utf8')).version;
76
+ return typeof v === 'string' && v.length > 0 ? v : undefined;
77
+ }
78
+ catch {
79
+ return undefined;
80
+ }
81
+ }
82
+ /**
83
+ * 세 배선(Claude/antigravity/codex)이 공유하는 조립: 모드 판별 → 버전 읽기 → entry.
84
+ *
85
+ * npx 로 판별됐어도 버전을 못 읽으면 node 로 물러난다 — 빈 핀(`@holmes-lab/holmes-kit@`)을 박느니
86
+ * 로컬 bin 을 부르는 편이 유효하다(A-SPEC-1404 경계). `readVersion` 은 주입 가능해 시험이 fs 없이 돈다.
87
+ */
88
+ function mcpEntryForInstall(opts) {
89
+ const mode = resolveLauncherMode({ flag: opts.flag, packageRoot: opts.packageRoot });
90
+ const read = opts.readVersion ?? readPackageVersion;
91
+ const version = mode === 'npx' ? read(opts.packageRoot) : undefined;
92
+ if (mode === 'npx' && !version) {
93
+ return mcpServerEntry({ mode: 'node', version: '', mcpBinPath: opts.mcpBinPath });
94
+ }
95
+ return mcpServerEntry({ mode, version: version ?? '', mcpBinPath: opts.mcpBinPath });
96
+ }
@@ -0,0 +1,23 @@
1
+ /**
2
+ * `.mcp.json` 배선이 **실제로 띄울** holmes-kit 버전을, 네트워크 없이 로컬로 뽑는다.
3
+ *
4
+ * 이 판정이 없어서 사용자의 전역 0.1.9 정체를 아무 진단도 보지 못했다(REQ-1405). 레지스트리를
5
+ * 묻지 않는다 — 배선 문자열과, node 배선인 경우 주입된 로컬 reader 만 본다. reader 를 안 주면 node
6
+ * 경로는 판정 불능(null)이지 실제 fs 를 더듬지 않는다: 순수성이 [offline] 을 시험 가능하게 한다.
7
+ */
8
+ export interface McpEntryShape {
9
+ command: string;
10
+ args: string[];
11
+ }
12
+ /**
13
+ * 배선이 띄울 버전, 또는 못 뽑으면 null.
14
+ *
15
+ * - npx: args 중 핀 스펙에서 버전 캡처.
16
+ * - node: args[0] 경로의 dirname 에서 위로 올라가며 `readVersion(dir)` 가 값을 줄 때까지. reader 가
17
+ * 없으면 null(파일 접근 없음).
18
+ * - 그 외 command → null.
19
+ */
20
+ export declare function mcpLaunchVersion(entry: McpEntryShape, readVersion?: (packageDir: string) => string | undefined): string | null;
21
+ export type DriftVerdict = 'match' | 'drift' | 'unknown';
22
+ /** launch 를 못 뽑았으면 unknown(거짓 drift 를 만들지 않는다). 같으면 match, 다르면 drift. */
23
+ export declare function versionDriftVerdict(launchVersion: string | null, cliVersion: string): DriftVerdict;
@@ -0,0 +1,82 @@
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.mcpLaunchVersion = mcpLaunchVersion;
37
+ exports.versionDriftVerdict = versionDriftVerdict;
38
+ // @implements A-SPEC-1405
39
+ const path = __importStar(require("node:path"));
40
+ /** npx 인자에서 핀 버전을 뽑는 정규식: `@holmes-lab/holmes-kit@<X>` 의 X. `@` 없는 스펙은 안 잡힌다. */
41
+ const NPX_PIN = /^@holmes-lab\/holmes-kit@(.+)$/;
42
+ /**
43
+ * 배선이 띄울 버전, 또는 못 뽑으면 null.
44
+ *
45
+ * - npx: args 중 핀 스펙에서 버전 캡처.
46
+ * - node: args[0] 경로의 dirname 에서 위로 올라가며 `readVersion(dir)` 가 값을 줄 때까지. reader 가
47
+ * 없으면 null(파일 접근 없음).
48
+ * - 그 외 command → null.
49
+ */
50
+ function mcpLaunchVersion(entry, readVersion) {
51
+ if (entry.command === 'npx') {
52
+ for (const a of entry.args) {
53
+ const m = NPX_PIN.exec(a);
54
+ if (m)
55
+ return m[1];
56
+ }
57
+ return null;
58
+ }
59
+ if (entry.command === 'node') {
60
+ if (!readVersion || entry.args.length === 0)
61
+ return null;
62
+ let dir = path.dirname(entry.args[0]);
63
+ // Walk up to the filesystem root, asking the reader at each level. `path.dirname('/') === '/'`,
64
+ // so the loop terminates when dir stops shrinking.
65
+ for (;;) {
66
+ const v = readVersion(dir);
67
+ if (v)
68
+ return v;
69
+ const parent = path.dirname(dir);
70
+ if (parent === dir)
71
+ return null;
72
+ dir = parent;
73
+ }
74
+ }
75
+ return null;
76
+ }
77
+ /** launch 를 못 뽑았으면 unknown(거짓 drift 를 만들지 않는다). 같으면 match, 다르면 drift. */
78
+ function versionDriftVerdict(launchVersion, cliVersion) {
79
+ if (launchVersion === null)
80
+ return 'unknown';
81
+ return launchVersion === cliVersion ? 'match' : 'drift';
82
+ }
@@ -62,5 +62,8 @@ export declare function disableMcpServer(existing: Settings | undefined, name: s
62
62
  * put it there.
63
63
  */
64
64
  export declare const HOLMES_OWNED_MCP_ENV: readonly ["HOLMES_SPECS"];
65
- export declare function mergeMcpServers(existing: McpConfig | undefined, name: string, mcpBinPath: string, specsDir: string): McpConfig;
65
+ export declare function mergeMcpServers(existing: McpConfig | undefined, name: string, entry: {
66
+ command: string;
67
+ args: string[];
68
+ }, specsDir: string): McpConfig;
66
69
  export declare function removeMcpServer(existing: McpConfig | undefined, name: string): McpConfig;
@@ -90,7 +90,11 @@ function disableMcpServer(existing, name) {
90
90
  * put it there.
91
91
  */
92
92
  exports.HOLMES_OWNED_MCP_ENV = ['HOLMES_SPECS'];
93
- function mergeMcpServers(existing, name, mcpBinPath, specsDir) {
93
+ // @implements A-SPEC-1404
94
+ // The server entry (command/args) is COMPUTED by the caller (mcp-launcher: npx-pin for installs,
95
+ // node for source checkouts) rather than assembled here, so all three harness wirings share one
96
+ // launch contract. This merge owns only env preservation, not how the server is launched.
97
+ function mergeMcpServers(existing, name, entry, specsDir) {
94
98
  const out = { ...(existing ?? {}) };
95
99
  // @implements A-SPEC-179
96
100
  // Merge, not replace. Measured 2026-08-13 while previewing `--force` on holmes-kit's own
@@ -104,7 +108,7 @@ function mergeMcpServers(existing, name, mcpBinPath, specsDir) {
104
108
  const prev = (prevEntry && typeof prevEntry === 'object' ? prevEntry.env : undefined) ?? {};
105
109
  out.mcpServers = {
106
110
  ...(out.mcpServers ?? {}),
107
- [name]: { command: 'node', args: [mcpBinPath], env: { ...prev, HOLMES_SPECS: specsDir } },
111
+ [name]: { command: entry.command, args: entry.args, env: { ...prev, HOLMES_SPECS: specsDir } },
108
112
  };
109
113
  return out;
110
114
  }
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.13",
4
+ "version": "0.1.15",
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",