@holmes-lab/holmes-kit 0.1.15 → 0.1.16

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,28 @@ 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.16] - 2026-08-23
9
+
10
+ Predictable spec numbering — enforced at the door, and the corpus cleaned to match.
11
+
12
+ ### Added
13
+ - **spec_create refuses a wild id (REQ-252)**: a new id whose base number leaps past
14
+ `max(base) + 1` is refused, naming the next available number. `spec_slice_init` assigns
15
+ `max + 1`, which is only predictable while numbering stays sequential — a single hand-picked
16
+ id (REQ-1403) once dragged the max to 1403 and every slice after jumped to 1404. The guard
17
+ closes the one door that let a manual id in, so `max + 1` can never be poisoned again.
18
+ Gap-fills, chain completion (base == max), and sub-slices (250.1) still pass; slice_init is
19
+ unaffected. Pure `sequentialIdVerdict`, injected id list — no filesystem.
20
+
21
+ ### Changed
22
+ - **Spec numbering restructured for readability**: the MCP drift fix shipped in 0.1.15 as two
23
+ top-level REQs (1404/1405) that were really one problem. Refiled under this repo's own
24
+ sub-slice convention as REQ-251 with A-SPEC-251.1 (npx wiring) and A-SPEC-251.2 (doctor
25
+ drift). The out-of-sequence REQ-1403 was renumbered to its rightful 250. Code `@implements`
26
+ anchors moved with them. No runtime behavior change — ids and anchors only; the 0.1.15
27
+ features are byte-identical.
28
+
7
29
  <!-- @implements A-SPEC-209 -->
8
30
  ## [0.1.15] - 2026-08-23
9
31
 
package/dist/.build-id CHANGED
@@ -1 +1 @@
1
- d6b14ca-mt5ea641
1
+ 3c84046-mt5fni4h
@@ -70,7 +70,7 @@ exports.HARNESS_ENFORCES = {
70
70
  codex: false,
71
71
  };
72
72
  const q = (p) => p;
73
- // @implements A-SPEC-1404 — same launch contract as the Claude wiring: npx-pin for installed
73
+ // @implements A-SPEC-251.1 — same launch contract as the Claude wiring: npx-pin for installed
74
74
  // packageRoots, node for source checkouts. Shared via mcpEntryForInstall so all three harnesses
75
75
  // move together.
76
76
  const mcpConfig = (packageRoot, specsDir, launcher) => {
@@ -456,7 +456,7 @@ async function runDoctor(packageRoot, target, opts, extraChecks) {
456
456
  else {
457
457
  add('approval channel', 'WARN', '이 프로세스에는 HOLMES_APPROVAL이 있으나 .mcp.json의 서버 환경에는 없습니다 — spec_approve는 서버 환경을 읽습니다', 'settings.local.json의 env가 MCP 서버까지 전달된다는 보장은 없습니다. 그 변수를 설정한 셸에서 Claude Code를 띄우면 자식 서버가 상속합니다.');
458
458
  }
459
- // @implements A-SPEC-1405 — the version this project's MCP server will ACTUALLY launch, vs this
459
+ // @implements A-SPEC-251.2 — the version this project's MCP server will ACTUALLY launch, vs this
460
460
  // CLI. The stale-global trap (measured 2026-08-23: .mcp.json pinned a 0.1.9 global while the user
461
461
  // believed they had upgraded) was invisible because nothing compared them. Offline: the launch
462
462
  // version comes from the wiring string, or a node wiring's LOCAL package.json — never the registry.
@@ -429,7 +429,7 @@ async function main(argv) {
429
429
  }
430
430
  if (cmd === 'init') {
431
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
432
+ // @implements A-SPEC-251.1 — an unknown launch mode is refused, not defaulted: substituting a
433
433
  // default acts where the operator did not point (REQ-144). Only npx|node are wiring modes.
434
434
  let mcpLauncher;
435
435
  if (typeof flags['mcp-launcher'] === 'string') {
@@ -310,7 +310,7 @@ function runInit(opts) {
310
310
  const kept = Object.keys(prevEnv).filter((k) => !settings_merge_2.HOLMES_OWNED_MCP_ENV.includes(k));
311
311
  if (kept.length > 0)
312
312
  messages.push(`Preserved your MCP server env: ${kept.join(', ')}`);
313
- // @implements A-SPEC-1404 — compute the launch entry once (npx-pin for installs, node for source),
313
+ // @implements A-SPEC-251.1 — compute the launch entry once (npx-pin for installs, node for source),
314
314
  // then merge; env preservation is unchanged.
315
315
  const mcpEntry = (0, mcp_launcher_1.mcpEntryForInstall)({ packageRoot: opts.packageRoot, mcpBinPath: mcpBin, flag: opts.mcpLauncher });
316
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' });
@@ -3,7 +3,7 @@
3
3
  *
4
4
  * 왜 한 곳인가: `.mcp.json`(Claude)·`.agents/mcp_config.json`(antigravity)·`.codex/mcp_config.json`
5
5
  * 세 배선이 같은 서버를 띄운다. 셋이 각자 command/args 를 지으면 하나가 npx 로 옮겨갈 때 나머지가
6
- * 절대경로에 남아 어긋난다 — 그 드리프트가 REQ-1404 자체의 출발점이었다.
6
+ * 절대경로에 남아 어긋난다 — 그 드리프트가 REQ-251 자체의 출발점이었다.
7
7
  *
8
8
  * 왜 두 모드인가: 최종 사용자는 npm 설치본에서 init 하므로 `npx ...@<정확한버전> holmes-mcp` 가
9
9
  * 옳다 — 전역 설치도 `-g` 도 필요 없고(Windows EPERM 회피), 절대경로 고착이 없다. 그러나 개발
@@ -45,7 +45,7 @@ export declare function readPackageVersion(packageRoot: string): string | undefi
45
45
  * 세 배선(Claude/antigravity/codex)이 공유하는 조립: 모드 판별 → 버전 읽기 → entry.
46
46
  *
47
47
  * npx 로 판별됐어도 버전을 못 읽으면 node 로 물러난다 — 빈 핀(`@holmes-lab/holmes-kit@`)을 박느니
48
- * 로컬 bin 을 부르는 편이 유효하다(A-SPEC-1404 경계). `readVersion` 은 주입 가능해 시험이 fs 없이 돈다.
48
+ * 로컬 bin 을 부르는 편이 유효하다(A-SPEC-251.1 경계). `readVersion` 은 주입 가능해 시험이 fs 없이 돈다.
49
49
  */
50
50
  export declare function mcpEntryForInstall(opts: {
51
51
  packageRoot: string;
@@ -38,7 +38,7 @@ exports.mcpServerEntry = mcpServerEntry;
38
38
  exports.resolveLauncherMode = resolveLauncherMode;
39
39
  exports.readPackageVersion = readPackageVersion;
40
40
  exports.mcpEntryForInstall = mcpEntryForInstall;
41
- // @implements A-SPEC-1404
41
+ // @implements A-SPEC-251.1
42
42
  const fs = __importStar(require("node:fs"));
43
43
  const path = __importStar(require("node:path"));
44
44
  /** npm 상의 정식 패키지 이름. 핀은 이 이름에 정확한 버전을 붙인다. */
@@ -83,7 +83,7 @@ function readPackageVersion(packageRoot) {
83
83
  * 세 배선(Claude/antigravity/codex)이 공유하는 조립: 모드 판별 → 버전 읽기 → entry.
84
84
  *
85
85
  * npx 로 판별됐어도 버전을 못 읽으면 node 로 물러난다 — 빈 핀(`@holmes-lab/holmes-kit@`)을 박느니
86
- * 로컬 bin 을 부르는 편이 유효하다(A-SPEC-1404 경계). `readVersion` 은 주입 가능해 시험이 fs 없이 돈다.
86
+ * 로컬 bin 을 부르는 편이 유효하다(A-SPEC-251.1 경계). `readVersion` 은 주입 가능해 시험이 fs 없이 돈다.
87
87
  */
88
88
  function mcpEntryForInstall(opts) {
89
89
  const mode = resolveLauncherMode({ flag: opts.flag, packageRoot: opts.packageRoot });
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * `.mcp.json` 배선이 **실제로 띄울** holmes-kit 버전을, 네트워크 없이 로컬로 뽑는다.
3
3
  *
4
- * 이 판정이 없어서 사용자의 전역 0.1.9 정체를 아무 진단도 보지 못했다(REQ-1405). 레지스트리를
4
+ * 이 판정이 없어서 사용자의 전역 0.1.9 정체를 아무 진단도 보지 못했다(REQ-251). 레지스트리를
5
5
  * 묻지 않는다 — 배선 문자열과, node 배선인 경우 주입된 로컬 reader 만 본다. reader 를 안 주면 node
6
6
  * 경로는 판정 불능(null)이지 실제 fs 를 더듬지 않는다: 순수성이 [offline] 을 시험 가능하게 한다.
7
7
  */
@@ -35,7 +35,7 @@ var __importStar = (this && this.__importStar) || (function () {
35
35
  Object.defineProperty(exports, "__esModule", { value: true });
36
36
  exports.mcpLaunchVersion = mcpLaunchVersion;
37
37
  exports.versionDriftVerdict = versionDriftVerdict;
38
- // @implements A-SPEC-1405
38
+ // @implements A-SPEC-251.2
39
39
  const path = __importStar(require("node:path"));
40
40
  /** npx 인자에서 핀 버전을 뽑는 정규식: `@holmes-lab/holmes-kit@<X>` 의 X. `@` 없는 스펙은 안 잡힌다. */
41
41
  const NPX_PIN = /^@holmes-lab\/holmes-kit@(.+)$/;
@@ -90,7 +90,7 @@ function disableMcpServer(existing, name) {
90
90
  * put it there.
91
91
  */
92
92
  exports.HOLMES_OWNED_MCP_ENV = ['HOLMES_SPECS'];
93
- // @implements A-SPEC-1404
93
+ // @implements A-SPEC-251.1
94
94
  // The server entry (command/args) is COMPUTED by the caller (mcp-launcher: npx-pin for installs,
95
95
  // node for source checkouts) rather than assembled here, so all three harness wirings share one
96
96
  // launch contract. This merge owns only env preservation, not how the server is launched.
@@ -138,6 +138,7 @@ const ignore_1 = require("../project/ignore");
138
138
  // @implements A-SPEC-126
139
139
  const scan_1 = require("../reverse/scan");
140
140
  const draft_1 = require("../reverse/draft");
141
+ const spec_id_guard_1 = require("./spec-id-guard");
141
142
  const anchor_1 = require("../reverse/anchor");
142
143
  // @implements A-SPEC-128
143
144
  // Where a project begins, for tools that only walk the tree.
@@ -593,6 +594,16 @@ function makeRawHandlers(store) {
593
594
  return { ok: false, reason: `depends_on "${String(pid)}"는 ${ptype}입니다 — ${def.type}의 부모는 ${def.parents.join('|') || '없음(빈 depends_on)'}이어야 합니다.` };
594
595
  }
595
596
  }
597
+ // @implements A-SPEC-252
598
+ // A new id may not LEAP past the sequence. spec_slice_init assigns nextId = max(base)+1, which
599
+ // is only predictable while numbering stays sequential; a hand-picked wild id (REQ-1403) once
600
+ // dragged max to 1403 and every slice after jumped to 1404. Refused here, at the one door that
601
+ // lets a manual id in — so max+1 can never be poisoned again. After the shape check on purpose:
602
+ // an id whose base cannot be read is the shape check's to name, not this guard's.
603
+ const corpus = await store.list();
604
+ const seq = (0, spec_id_guard_1.sequentialIdVerdict)(String(id), corpus.map((s) => s.id));
605
+ if (!seq.ok)
606
+ return { ok: false, reason: seq.reason };
596
607
  // @implements A-SPEC-188
597
608
  // Creation creates. Probed before this check existed: spec_create over an approved+sealed
598
609
  // REQ returned {"created"} while the disk went draft / seal gone / prose gone — the sanctioned
@@ -600,7 +611,7 @@ function makeRawHandlers(store) {
600
611
  // refusals on purpose: a malformed id must keep its own message (order is pinned by test).
601
612
  // Legacy (typeless) documents count as existing too — overwriting one makes the store's
602
613
  // orphan removal delete the differently-named original file along with its history.
603
- const existing = (await store.list()).find((s) => s.id === id);
614
+ const existing = corpus.find((s) => s.id === id);
604
615
  if (existing) {
605
616
  const sealed = existing.status === 'approved' || Boolean(existing.frontmatter?.approved_digest);
606
617
  const kind = existing.type ? `${existing.type} (status: ${existing.status})` : `옛 형식 문서 (status: ${existing.status})`;
@@ -0,0 +1,26 @@
1
+ /**
2
+ * 넘버링 오염을 생성 시점에 막는 순수 가드.
3
+ *
4
+ * spec_slice_init 의 `nextId = max(base) + 1` 은 번호가 항상 순차 배정될 때만 예측 가능하다. 그런데
5
+ * spec_create 는 id 형태만 보고 순번은 보지 않아, 손으로 심은 wild id(REQ-1403) 하나가 max 를 끌어올려
6
+ * 이후 모든 슬라이스를 오염시켰다(실측: 249→1404). 이 가드는 그 하나의 구멍을 막는다 — 새 base 는
7
+ * 현재 max 를 한 칸 넘게 뛸 수 없다. 막으면 max+1 은 다시 오염되지 않고 넘버링이 영구히 예측 가능하다.
8
+ */
9
+ /** id 의 base 숫자. "REQ-1403"→1403, "A-SPEC-250.1"→250(서브번호 무시), 레거시 "H-SPEC-050"→50. 못 뽑으면 null. */
10
+ export declare function specIdBase(id: string): number | null;
11
+ export type IdVerdict = {
12
+ ok: true;
13
+ } | {
14
+ ok: false;
15
+ reason: string;
16
+ nextAvailable: number;
17
+ };
18
+ /**
19
+ * 새 id 의 base 가 코퍼스 max base 를 한 칸 넘게 뛰면 거부한다.
20
+ *
21
+ * - 빈 코퍼스, 또는 base 를 못 뽑는 id → 통과(비교 대상이 없거나, 형태 검증은 별도 소관이라 한 결함에
22
+ * 두 이름을 주지 않는다).
23
+ * - base ≤ maxBase(갭 메우기·체인 완성) 또는 base == maxBase+1(새 체인) → 통과.
24
+ * - base > maxBase+1(leap) → 거부, 다음 가용 번호를 문면과 필드에 댄다.
25
+ */
26
+ export declare function sequentialIdVerdict(newId: string, existingIds: string[]): IdVerdict;
@@ -0,0 +1,50 @@
1
+ "use strict";
2
+ // @implements A-SPEC-252
3
+ /**
4
+ * 넘버링 오염을 생성 시점에 막는 순수 가드.
5
+ *
6
+ * spec_slice_init 의 `nextId = max(base) + 1` 은 번호가 항상 순차 배정될 때만 예측 가능하다. 그런데
7
+ * spec_create 는 id 형태만 보고 순번은 보지 않아, 손으로 심은 wild id(REQ-1403) 하나가 max 를 끌어올려
8
+ * 이후 모든 슬라이스를 오염시켰다(실측: 249→1404). 이 가드는 그 하나의 구멍을 막는다 — 새 base 는
9
+ * 현재 max 를 한 칸 넘게 뛸 수 없다. 막으면 max+1 은 다시 오염되지 않고 넘버링이 영구히 예측 가능하다.
10
+ */
11
+ Object.defineProperty(exports, "__esModule", { value: true });
12
+ exports.specIdBase = specIdBase;
13
+ exports.sequentialIdVerdict = sequentialIdVerdict;
14
+ /** id 의 base 숫자. "REQ-1403"→1403, "A-SPEC-250.1"→250(서브번호 무시), 레거시 "H-SPEC-050"→50. 못 뽑으면 null. */
15
+ function specIdBase(id) {
16
+ if (typeof id !== 'string')
17
+ return null;
18
+ // 마지막 하이픈 뒤의 숫자 토막(점 앞)을 base 로 본다.
19
+ const m = /-(\d+)(?:\.\d+)?\s*$/.exec(id.trim());
20
+ if (!m)
21
+ return null;
22
+ const n = parseInt(m[1], 10);
23
+ return Number.isNaN(n) ? null : n;
24
+ }
25
+ /**
26
+ * 새 id 의 base 가 코퍼스 max base 를 한 칸 넘게 뛰면 거부한다.
27
+ *
28
+ * - 빈 코퍼스, 또는 base 를 못 뽑는 id → 통과(비교 대상이 없거나, 형태 검증은 별도 소관이라 한 결함에
29
+ * 두 이름을 주지 않는다).
30
+ * - base ≤ maxBase(갭 메우기·체인 완성) 또는 base == maxBase+1(새 체인) → 통과.
31
+ * - base > maxBase+1(leap) → 거부, 다음 가용 번호를 문면과 필드에 댄다.
32
+ */
33
+ function sequentialIdVerdict(newId, existingIds) {
34
+ const base = specIdBase(newId);
35
+ if (base === null)
36
+ return { ok: true };
37
+ const bases = (existingIds ?? []).map(specIdBase).filter((n) => n !== null);
38
+ if (bases.length === 0)
39
+ return { ok: true };
40
+ const maxBase = Math.max(...bases);
41
+ if (base > maxBase + 1) {
42
+ const nextAvailable = maxBase + 1;
43
+ return {
44
+ ok: false,
45
+ reason: `${newId} 는 순번을 벗어납니다 — 현재 최대 번호는 ${maxBase}, 다음 가용 번호는 ${nextAvailable} 입니다. 순차 번호나 기존 base 의 서브슬라이스만 만들 수 있습니다(넘버링 오염 방지).`,
46
+ nextAvailable,
47
+ };
48
+ }
49
+ return { ok: true };
50
+ }
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.15",
4
+ "version": "0.1.16",
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",