@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 +44 -0
- package/dist/.build-id +1 -1
- package/dist/holmes/cli/agents.d.ts +2 -0
- package/dist/holmes/cli/agents.js +17 -10
- package/dist/holmes/cli/doctor.js +33 -0
- package/dist/holmes/cli/index.js +22 -3
- package/dist/holmes/cli/init.d.ts +2 -0
- package/dist/holmes/cli/init.js +6 -2
- package/dist/holmes/cli/mcp-launcher.d.ts +55 -0
- package/dist/holmes/cli/mcp-launcher.js +96 -0
- package/dist/holmes/cli/mcp-version.d.ts +23 -0
- package/dist/holmes/cli/mcp-version.js +82 -0
- package/dist/holmes/cli/settings-merge.d.ts +4 -1
- package/dist/holmes/cli/settings-merge.js +6 -2
- package/package.json +1 -1
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
|
-
|
|
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
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
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
|
-
}
|
|
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');
|
package/dist/holmes/cli/index.js
CHANGED
|
@@ -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
|
-
|
|
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;
|
package/dist/holmes/cli/init.js
CHANGED
|
@@ -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
|
-
|
|
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,
|
|
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
|
-
|
|
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:
|
|
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.
|
|
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",
|