@holmes-lab/holmes-kit 0.10.0 → 0.11.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.
package/CHANGELOG.md CHANGED
@@ -5,7 +5,44 @@ 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.10.0] - 2026-09-04
8
+ ## [0.11.0] - 2026-09-05
9
+
10
+ Zero-config upgrades — preparation is automatic, execution is your explicit choice. Additive and
11
+ backward-compatible; nothing changes at the default settings until you run `holmes-kit upgrade`.
12
+
13
+ ### Added
14
+
15
+ - **`holmes-kit upgrade`** (REQ-543): one command to move every wired workspace to the latest — it
16
+ shows a plan (current → latest, and which workspaces it will re-pin), asks for confirmation, then
17
+ installs per install-mode, clears the stale npx cache, re-pins each recorded workspace, and prints
18
+ a per-workspace result table. `--dry-run` prints the plan and changes nothing; `--yes` skips the
19
+ prompt (required in a non-interactive shell — the execution is a deliberate choice, never a silent
20
+ auto-install of a gate-enforcing engine). A source checkout is told to upgrade via git.
21
+ - **Workspace registry** (REQ-543): every successful `init` now records where holmes-kit is wired
22
+ (`~/.holmes/workspaces.json` — target, agents, version), so `upgrade` knows every workspace to
23
+ re-pin. Best-effort and side-channel: a registry problem never affects `init`.
24
+ - **Session drift nudge** (REQ-543): when a session starts in a workspace whose `.mcp.json` pin is
25
+ OLDER than the installed package, the banner adds one line pointing at `holmes-kit upgrade`.
26
+ Detection is automatic; the re-pin **write** stays behind your `upgrade`/`init --force` choice —
27
+ a session-start force-write would bypass the governance-wiring approval, so it is not done silently.
28
+ Opt out with `HOLMES_NO_AUTO_REPIN`; no drift, opt-out, or unreadable pin leaves the banner
29
+ byte-identical.
30
+
31
+ ### Fixed
32
+
33
+ - **Antigravity hooks: 0.10.0's `command`+`args` form regressed on the real launcher — reverted to an
34
+ unquoted single command string** (REQ-541). Field measurement on Windows/agy showed the hook
35
+ launcher **ignores `args`** and runs only the `command` string: 0.10.0's `command:"node"` +
36
+ `args:[path]` launched bare `node`, which evaluated the stdin JSON payload as code
37
+ (`SyntaxError: Unexpected token ':'`, exit 1) — every tool blocked. A second measurement had already
38
+ shown it keeps quotes **literal** (the pre-0.10.0 `node "<path>"` form died with MODULE_NOT_FOUND).
39
+ The only form the launcher runs is an unquoted single string, whitespace-split. `.agents/hooks.json`
40
+ now emits `command: "node <forward-slash path>"` (no quotes, no `args`); on win32 a path containing
41
+ a space is converted to its 8.3 short name, best-effort (lookup failure keeps the original path).
42
+ `doctor` continues to judge old (args) and new (string) wirings alike. **Re-run
43
+ `holmes-kit init --target <dir> --agent antigravity --force` after upgrading** — the fix changes
44
+ what init writes, not files already on disk. *Windows/agy real runtime and the 8.3 branch remain
45
+ unverified in this environment; validated by emitted shape + a naive-split execution simulation.*
9
46
 
10
47
  Governance UX tools for multi-agent round-trips and observability, a cross-harness approval fix, and
11
48
  an Antigravity portability fix. All additive and backward-compatible; existing behaviour is unchanged
package/README.md CHANGED
@@ -19,6 +19,7 @@
19
19
  - 📋 **Requirements & Specification Governance**: Strict **"No Spec, No Code"** enforcement with 4-tier spec chain traceability (`REQ ➔ H-SPEC ➔ A-SPEC ➔ T-SPEC`) and `// @implements A-SPEC-XXX` code anchors (comma-lists and every anchor in a file participate in the gate).
20
20
  - 🔴 **Inbuilt TDD — RED-first, enforced not asked** *(new in 0.9.0)*: the test-first discipline is a holmes-installed `holmes-tdd-slice` skill **and** a new constitution article **ART-8**. A changed A-SPEC must show a recorded `red-assertion → green` sequence in the ledger; a `red-error` (a test that could not run) is not a valid RED, so "the covering test failed *correctly*" is judged mechanically, not on trust. `test_run` classifies each covered file (`red-assertion`/`red-error`/`green`) and records per-A-SPEC outcomes the Stop hook reads. Ships at `redFirstEvidence: track` (observe-first, non-blocking; `strict`/`off` per repo), evidence-gated and jest-only for now. A T-SPEC may also declare `kills:` mutations and `test_run --mutate` reports which SURVIVED (a coverage gap). Where superpowers *asks* for RED-first and discriminating power, holmes-kit *proves* them.
21
21
  - 🧰 **Governance UX tools** *(new in 0.10.0)*: `spec_unseal` (the inverse of `spec_approve` — return a sealed spec to editable `draft` in one act, out-of-band approval required, refuses approved dependents), `approval_status` and `ledger_timeline` (read-only observability into a spec's seal state and the governance history), and a structured `conflict` on `spec_approve`'s optimistic-concurrency refusal (read vs. current version + retry). See CHANGELOG for details.
22
+ - ⬆️ **Zero-config upgrades** *(new in 0.11.0)*: `holmes-kit upgrade` moves **every** wired workspace to the latest in one command — plan → confirm → install → re-pin all recorded workspaces (`--dry-run`/`--yes` supported). Preparation is automatic (each `init` records the workspace; a session whose pin is behind nudges you to upgrade); the re-pin **write** stays your explicit choice, never a silent auto-install. Opt out of the nudge with `HOLMES_NO_AUTO_REPIN`.
22
23
  - 🤖 **Autonomous Approval** *(new in 0.8.0)*: with the out-of-band `HOLMES_AUTONOMOUS_APPROVAL` switch on, an agent seals **low/mid-risk** specs itself (ledgered under an `autonomous:<client>` actor) — while `gate-behavior` changes, architecture/gate/taint files, and every upstream `REQ`/`H-SPEC`/`C-SPEC` still ask a human through the in-session TUI. The switch is env-only; a session cannot set it (blocked like `HOLMES_ROLE`). Off = byte-identical to before. *(new in 0.10.0)* Set **`HOLMES_ELICIT=off`** to skip the in-session prompt entirely and route every decision to the out-of-band `holmes-kit approve` queue instead — one decision surface that behaves identically across Claude Code / Antigravity / Codex.
23
24
  - 🪧 **Session Banner + Update Notice** *(new in 0.8.0)*: every session start emits an English intro (version + governance rule + npm URL) to both the human transcript and the agent context (SessionStart hook + MCP `instructions`); when a newer published version is cached, an install-mode-aware update command is appended. Registry check is detached, fail-silent, and opts out via `HOLMES_NO_UPDATE_CHECK`/`CI`.
24
25
  - 🧱 **Deterministic Gate, Hardened** *(new in 0.8.0)*: shell writes are judged at the segment's **effective working directory** (`cd sub && cat > ../src/x.ts` is sealed, legitimate out-of-tree scratch writes are freed); the governing anchor is the **whole set**, not the first match. Every gate change ships with two consecutive clean adversarial rounds.
package/dist/.build-id CHANGED
@@ -1 +1 @@
1
- 0d70dec-mtn1jk14
1
+ 2fb1cad-mtnmh3yo
@@ -49,6 +49,19 @@ export interface AgentFile {
49
49
  path: string;
50
50
  content: string;
51
51
  }
52
+ /**
53
+ * Antigravity 훅 배선.
54
+ *
55
+ * 매처는 `*` 다. 좁히면 그 밖의 도구가 게이트를 지나가고, Antigravity 의 도구 목록은 우리가
56
+ * 통제하지 않는다 — 이름을 열거하는 순간 다음에 추가되는 도구가 무료 통행권을 얻는다.
57
+ * (Claude 배선이 `guardrail` 모드에서 좁은 매처를 쓰는 것과 다른 선택인데, 그쪽은 그 좁힘이
58
+ * 무엇을 뜻하는지 문서화된 모드 선택이고 여기는 기본 배선이기 때문이다.)
59
+ */
60
+ /** `node <forward-slash path>` — unquoted, single string; the only form agy's launcher runs. */
61
+ export declare function antigravityHookCommand(scriptPath: string): string;
62
+ /** win32-only, best-effort: a spaced path becomes its 8.3 short name (no spaces); anything else — or
63
+ * any lookup failure — returns the input unchanged. */
64
+ export declare function shortPathIfSpaced(p: string): string;
52
65
  /**
53
66
  * 이 하네스에 써야 할 파일들. **쓰지는 않는다** — 무엇을 쓸지만 말한다.
54
67
  *
@@ -34,11 +34,14 @@ var __importStar = (this && this.__importStar) || (function () {
34
34
  })();
35
35
  Object.defineProperty(exports, "__esModule", { value: true });
36
36
  exports.CODEX_MARKETPLACE = exports.CODEX_PLUGIN_DIR = exports.HARNESS_ENFORCES = exports.AGENTS = void 0;
37
+ exports.antigravityHookCommand = antigravityHookCommand;
38
+ exports.shortPathIfSpaced = shortPathIfSpaced;
37
39
  exports.agentFiles = agentFiles;
38
40
  exports.agentLinks = agentLinks;
39
41
  // @implements A-SPEC-442
40
42
  // @implements A-SPEC-193
41
43
  const path = __importStar(require("node:path"));
44
+ const node_child_process_1 = require("node:child_process");
42
45
  const mcp_launcher_1 = require("./mcp-launcher");
43
46
  /**
44
47
  * 하네스별 배선물.
@@ -107,19 +110,37 @@ const mcpConfig = (packageRoot, specsDir, launcher) => {
107
110
  * (Claude 배선이 `guardrail` 모드에서 좁은 매처를 쓰는 것과 다른 선택인데, 그쪽은 그 좁힘이
108
111
  * 무엇을 뜻하는지 문서화된 모드 선택이고 여기는 기본 배선이기 때문이다.)
109
112
  */
110
- // @implements A-SPEC-539.1
111
- // agy's hook launcher tokenizes the `command` string on whitespace and does NOT honor quotes, so an
112
- // earlier quoted `node "<path>"` (A-SPEC-193 §2b-2) still broke on a path with spaces: measured on a
113
- // Windows machine whose Node lives at `C:\Program Files\nodejs`, reported from agy 2026-09-04 — the
114
- // launcher split `node "C:\Program Files\...` at the space, node took `"C:\Program` for a module
115
- // (MODULE_NOT_FOUND), and the gate loaded not at all (silently off). Passing the script path as an
116
- // `args` ELEMENT the same array form mcp_config already uses sidesteps tokenization entirely, on
117
- // every platform. This supersedes the quoted-string form for the antigravity wiring; quoting only
118
- // ever helped launchers that honor quotes, which agy's does not.
113
+ // @implements A-SPEC-541.1
114
+ // Two field measurements (both 2026-09-04, Windows/agy) pinned the launcher's real contract, each by
115
+ // killing one prior form:
116
+ // · `node "C:\...js"` (quoted, A-SPEC-193 §2b-2) the quotes stayed LITERAL and node got
117
+ // `"C:\...` as a module path MODULE_NOT_FOUND. The launcher does not honor quotes.
118
+ // · `command:'node', args:[path]` (A-SPEC-539.1 / 0.10.0) the launcher IGNORED `args` and ran
119
+ // bare `node`, which took the stdin JSON payload as code `SyntaxError: Unexpected token ':'`
120
+ // (exit 1), every tool blocked. The launcher executes only the single `command` string.
121
+ // So the only form it can run is an UNQUOTED single command string, whitespace-split which means
122
+ // the path itself must carry no space. Forward slashes (node accepts them on Windows) avoid JSON
123
+ // backslash escaping; a spaced path is converted to its 8.3 short name on win32, best-effort.
124
+ /** `node <forward-slash path>` — unquoted, single string; the only form agy's launcher runs. */
125
+ function antigravityHookCommand(scriptPath) {
126
+ return `node ${scriptPath.replace(/\\/g, '/')}`;
127
+ }
128
+ /** win32-only, best-effort: a spaced path becomes its 8.3 short name (no spaces); anything else — or
129
+ * any lookup failure — returns the input unchanged. */
130
+ function shortPathIfSpaced(p) {
131
+ if (process.platform !== 'win32' || !p.includes(' '))
132
+ return p;
133
+ try {
134
+ const out = (0, node_child_process_1.execFileSync)('cmd', ['/d', '/c', `for %I in ("${p}") do @echo %~sI`], { encoding: 'utf8' }).trim();
135
+ return out !== '' && !out.includes(' ') ? out : p;
136
+ }
137
+ catch {
138
+ return p;
139
+ }
140
+ }
119
141
  const antigravityHookEntry = (packageRoot, script) => ({
120
142
  type: 'command',
121
- command: 'node',
122
- args: [path.join(packageRoot, 'bin', script)],
143
+ command: antigravityHookCommand(shortPathIfSpaced(path.join(packageRoot, 'bin', script))),
123
144
  timeout: 30,
124
145
  });
125
146
  /**
@@ -46,6 +46,7 @@ exports.renderNonTtyHint = renderNonTtyHint;
46
46
  exports.runInteractive = runInteractive;
47
47
  // @implements A-SPEC-246
48
48
  const fs = __importStar(require("node:fs"));
49
+ const npx_bin_1 = require("../project/npx-bin");
49
50
  const path = __importStar(require("node:path"));
50
51
  const approval_queue_1 = require("../governance/approval-queue");
51
52
  const screen_safe_1 = require("./screen-safe");
@@ -143,7 +144,7 @@ function grantRequest(root, id, opts) {
143
144
  }
144
145
  const { entry } = findPending(root, id);
145
146
  if (!entry)
146
- return { ok: false, reason: `대기 중인 요청 ${(0, screen_safe_1.rowField)(id, 40)} 가 없습니다 — npx holmes-kit approve --list 로 확인하십시오` };
147
+ return { ok: false, reason: `대기 중인 요청 ${(0, screen_safe_1.rowField)(id, 40)} 가 없습니다 — ${(0, npx_bin_1.npxBin)()} holmes-kit approve --list 로 확인하십시오` };
147
148
  if (!recordIsSelfConsistent(entry))
148
149
  return { ok: false, reason: TAMPERED };
149
150
  // A "does this match what the operator saw" parameter was written here too and then removed: the
@@ -49,6 +49,7 @@ exports.detectTreeKeyTemporary = detectTreeKeyTemporary;
49
49
  // @implements A-SPEC-207
50
50
  // @implements A-SPEC-100.2
51
51
  const fs = __importStar(require("node:fs"));
52
+ const npx_bin_1 = require("../project/npx-bin");
52
53
  const tier_1 = require("../semantic/tier");
53
54
  const path = __importStar(require("node:path"));
54
55
  const role_policy_1 = require("../governance/role-policy");
@@ -756,7 +757,7 @@ async function runDoctor(packageRoot, target, opts, extraChecks) {
756
757
  const launch = (0, mcp_version_1.mcpLaunchVersion)({ command: server.command, args: server.args }, readPkgVersionAt);
757
758
  const verdict = (0, mcp_version_1.versionDriftVerdict)(launch, cli);
758
759
  if (verdict === 'drift') {
759
- add('mcp server version', 'WARN', `이 프로젝트의 MCP 서버는 holmes-kit ${launch} 를 띄우지만 이 CLI 는 ${cli} 입니다 — 재배선 전까지 옛 서버가 계속 뜹니다`, 'npx @holmes-lab/holmes-kit@latest init 을 다시 실행해 .mcp.json 을 최신 버전으로 재배선하십시오 (전역 -g 업그레이드 불필요).');
760
+ add('mcp server version', 'WARN', `이 프로젝트의 MCP 서버는 holmes-kit ${launch} 를 띄우지만 이 CLI 는 ${cli} 입니다 — 재배선 전까지 옛 서버가 계속 뜹니다`, `${(0, npx_bin_1.npxBin)()} @holmes-lab/holmes-kit@latest init 을 다시 실행해 .mcp.json 을 최신 버전으로 재배선하십시오 (전역 -g 업그레이드 불필요).`);
760
761
  }
761
762
  else if (verdict === 'match') {
762
763
  add('mcp server version', 'PASS', `MCP 서버 버전 ${launch} 가 이 CLI 와 일치합니다`);
@@ -810,7 +811,7 @@ async function runDoctor(packageRoot, target, opts, extraChecks) {
810
811
  const h = JSON.parse(fs.readFileSync(agyHooks, 'utf8'));
811
812
  const entry = h['holmes-kit'];
812
813
  const pre = entry?.PreToolUse?.[0];
813
- // @implements A-SPEC-539.1 — the hook script path now lives in args[0] (command is bare
814
+ // @implements A-SPEC-541.1 — the hook script path now lives in args[0] (command is bare
814
815
  // 'node'); older wirings joined it into the command string. Read args first, fall back to
815
816
  // parsing the string so a pre-539 hooks.json is still judged correctly.
816
817
  const scriptOf = (e) => (Array.isArray(e?.args) && typeof e?.args[0] === 'string') ? e.args[0] : (0, settings_merge_1.hookScriptPath)(e?.command ?? '');
@@ -1280,7 +1281,7 @@ function pushGateCheck(target) {
1280
1281
  : {
1281
1282
  name: 'push gate', level: 'WARN',
1282
1283
  detail: 'pre-push 증빙 게이트가 설치되어 있지 않습니다 — 미검증 HEAD 가 다른 에이전트의 기준선이 될 수 있습니다',
1283
- fix: 'npx holmes-kit install-push-gate 를 실행하십시오 (기존 훅은 덮지 않습니다)',
1284
+ fix: `${(0, npx_bin_1.npxBin)()} holmes-kit install-push-gate 를 실행하십시오 (기존 훅은 덮지 않습니다)`,
1284
1285
  };
1285
1286
  }
1286
1287
  catch {
@@ -82,6 +82,8 @@ const KNOWN_FLAGS = {
82
82
  'semantic-key': ['help'],
83
83
  // @implements A-SPEC-509.1 — the explicit installation act for the pre-push evidence gate.
84
84
  'install-push-gate': ['help', 'target'],
85
+ // @implements A-SPEC-543.2 — one-command upgrade: install + re-pin every registered workspace.
86
+ upgrade: ['help', 'dry-run', 'yes'],
85
87
  };
86
88
  // @implements A-SPEC-171 — subcommands that render their OWN usage on `--help`. A-SPEC-171 governs
87
89
  // "help before any side effect"; a command whose usage lives past this handler (approve, whose
@@ -212,6 +214,10 @@ const USAGE = `holmes-kit — deterministic ASE governance for a project
212
214
  --target <dir> also verify that target's wiring points at this install
213
215
  --json machine-readable output
214
216
 
217
+ holmes-kit upgrade [flags] install the latest and re-pin every recorded workspace (one command)
218
+ --dry-run show the plan (current→latest, workspaces to re-pin), change nothing
219
+ --yes skip the confirmation prompt (required in a non-interactive shell)
220
+
215
221
  holmes-kit skills refresh [flags] refresh the recovery skills holmes-kit owns
216
222
  --target <dir> target project (default: cwd; must exist). Touches ONLY marker-owned
217
223
  skill files (installs absent ones, REWRITES all marker-owned ones,
@@ -1038,9 +1044,52 @@ async function main(argv) {
1038
1044
  process.stdout.write(c.after.split('\n').map((l) => ` | ${l}`).join('\n') + '\n');
1039
1045
  }
1040
1046
  }
1047
+ // @implements A-SPEC-543.1 — zero-config upgrade PREPARATION: a successful real wiring records
1048
+ // WHERE holmes-kit is wired, so `upgrade` later knows every workspace to re-pin. Best-effort by
1049
+ // contract: a registry problem must never change init's output or exit code.
1050
+ if (!opts.dryRun && !opts.remove) {
1051
+ try {
1052
+ const { recordWorkspace } = require('../update/workspaces');
1053
+ const os2 = require('node:os');
1054
+ const pkgVersion = (() => {
1055
+ try {
1056
+ return String(JSON.parse(fs.readFileSync(path.join(opts.packageRoot, 'package.json'), 'utf8')).version ?? '');
1057
+ }
1058
+ catch {
1059
+ return '';
1060
+ }
1061
+ })();
1062
+ recordWorkspace(os2.homedir(), { target: opts.target, agents: [...agents], version: pkgVersion, ts: new Date().toISOString() });
1063
+ }
1064
+ catch { /* registry failure never fails init */ }
1065
+ }
1041
1066
  }
1042
1067
  return res.exitCode;
1043
1068
  }
1069
+ // @implements A-SPEC-543.2 — one-command upgrade. Preparation is zero-config (init records the
1070
+ // registry); the EXECUTION is the user's explicit choice — a plan is shown and confirmed (or a
1071
+ // non-TTY passes --yes), never a silent auto-install of a gate-enforcing engine.
1072
+ if (cmd === 'upgrade') {
1073
+ const { runUpgrade } = require('./upgrade');
1074
+ return await runUpgrade({
1075
+ dryRun: flags['dry-run'] === true,
1076
+ yes: flags.yes === true,
1077
+ stdout: (s) => process.stdout.write(s),
1078
+ stderr: (s) => process.stderr.write(s),
1079
+ confirm: async (prompt) => {
1080
+ if (!process.stdin.isTTY || !process.stdout.isTTY)
1081
+ return false;
1082
+ const rl = require('node:readline/promises').createInterface({ input: process.stdin, output: process.stdout });
1083
+ try {
1084
+ const ans = (await rl.question(prompt)).trim().toLowerCase();
1085
+ return ans === 'y' || ans === 'yes';
1086
+ }
1087
+ finally {
1088
+ rl.close();
1089
+ }
1090
+ },
1091
+ });
1092
+ }
1044
1093
  // round-8: `holmes-kit --target x` 나 `holmes-kit --` 처럼 명령 자리에 플래그가 오면 exit 1,
1045
1094
  // 같은 토큰이 하위 명령 뒤에 오면 exit 2 였다 — 같은 실수에 두 가지 진단.
1046
1095
  process.stderr.write(cmd.startsWith('-')
@@ -0,0 +1,14 @@
1
+ export interface UpgradeIO {
2
+ dryRun: boolean;
3
+ yes: boolean;
4
+ stdout: (s: string) => void;
5
+ stderr: (s: string) => void;
6
+ confirm: (prompt: string) => Promise<boolean>;
7
+ }
8
+ /**
9
+ * @implements A-SPEC-543.2
10
+ * Thin orchestration around the pure `planUpgrade`. Resolves latest from the update cache, plans,
11
+ * shows/confirms (the user's choice), then installs + re-pins every registered workspace. Install
12
+ * failure aborts BEFORE any re-pin (never re-pin to a version that did not install).
13
+ */
14
+ export declare function runUpgrade(io: UpgradeIO): Promise<number>;
@@ -0,0 +1,177 @@
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.runUpgrade = runUpgrade;
37
+ // @implements A-SPEC-543.2
38
+ const fs = __importStar(require("node:fs"));
39
+ const os = __importStar(require("node:os"));
40
+ const path = __importStar(require("node:path"));
41
+ const node_child_process_1 = require("node:child_process");
42
+ const update_notice_1 = require("../update/update-notice");
43
+ const workspaces_1 = require("../update/workspaces");
44
+ const upgrade_plan_1 = require("../update/upgrade-plan");
45
+ const session_start_1 = require("../hooks/session-start");
46
+ const npx_bin_1 = require("../project/npx-bin");
47
+ const PKG = '@holmes-lab/holmes-kit';
48
+ // Local copy of index.ts's packageRoot (dist/holmes/cli/upgrade.js → 3 up) — avoids a require cycle,
49
+ // since index.ts lazily requires THIS module.
50
+ function packageRoot() {
51
+ return path.resolve(__dirname, '..', '..', '..');
52
+ }
53
+ function currentVersion(root) {
54
+ try {
55
+ return String(JSON.parse(fs.readFileSync(path.join(root, 'package.json'), 'utf8')).version ?? '0.0.0');
56
+ }
57
+ catch {
58
+ return '0.0.0';
59
+ }
60
+ }
61
+ /** `npm` on POSIX, `npm.cmd` on win32 — the shim a spawn resolves (same binary-name reasoning as npxBin). */
62
+ function npmBin() { return process.platform === 'win32' ? 'npm.cmd' : 'npm'; }
63
+ /** Render the plan for the human, one line per fact. */
64
+ function renderPlan(plan) {
65
+ const lines = [
66
+ `Holmes-Kit upgrade: ${plan.current} → ${plan.latest} (install mode: ${plan.installMode})`,
67
+ plan.repins.length
68
+ ? `Will re-pin ${plan.repins.length} workspace(s):`
69
+ : 'No wired workspaces recorded yet — install only.',
70
+ ...plan.repins.map((w) => ` • ${w.target} [${w.agents.join(', ')}]`),
71
+ ];
72
+ return lines.join('\n') + '\n';
73
+ }
74
+ /**
75
+ * @implements A-SPEC-543.2
76
+ * Thin orchestration around the pure `planUpgrade`. Resolves latest from the update cache, plans,
77
+ * shows/confirms (the user's choice), then installs + re-pins every registered workspace. Install
78
+ * failure aborts BEFORE any re-pin (never re-pin to a version that did not install).
79
+ */
80
+ async function runUpgrade(io) {
81
+ const root = packageRoot();
82
+ const current = currentVersion(root);
83
+ const mode = (0, session_start_1.detectInstallMode)(root);
84
+ const cached = (0, update_notice_1.readCache)(os.homedir(), (p) => fs.readFileSync(p, 'utf8'));
85
+ const registry = (() => {
86
+ try {
87
+ return (0, workspaces_1.readRegistry)(fs.readFileSync(path.join(os.homedir(), '.holmes', 'workspaces.json'), 'utf8'));
88
+ }
89
+ catch {
90
+ return (0, workspaces_1.readRegistry)(null);
91
+ }
92
+ })();
93
+ const plan = (0, upgrade_plan_1.planUpgrade)({ current, latest: cached?.latest ?? null, registry, installMode: mode });
94
+ if (plan.kind === 'noop') {
95
+ io.stdout(`${plan.reason}\n`);
96
+ return 0;
97
+ }
98
+ io.stdout(renderPlan(plan));
99
+ if (io.dryRun) {
100
+ io.stdout('(dry-run — nothing installed or re-pinned)\n');
101
+ return 0;
102
+ }
103
+ if (plan.installMode === 'source') {
104
+ io.stderr('source checkout: upgrade via git (git pull && npm run build), not npm install.\n');
105
+ return 1;
106
+ }
107
+ const go = io.yes || await io.confirm('계속합니까? [y/N] ');
108
+ if (!go) {
109
+ io.stdout(io.yes ? '' : 'aborted — nothing installed or re-pinned.\n');
110
+ if (!io.yes && (!process.stdin.isTTY))
111
+ io.stderr('non-interactive: pass --yes to upgrade without a prompt.\n');
112
+ return io.yes ? 0 : (process.stdin.isTTY ? 0 : 1);
113
+ }
114
+ // (1) install — failure aborts before any re-pin.
115
+ try {
116
+ const args = plan.installMode === 'global-npx'
117
+ ? ['i', '-g', `${PKG}@${plan.latest}`]
118
+ : ['i', '-D', `${PKG}@${plan.latest}`];
119
+ (0, node_child_process_1.execFileSync)(npmBin(), args, { stdio: 'inherit', cwd: process.cwd() });
120
+ }
121
+ catch {
122
+ io.stderr(`install failed — nothing re-pinned. Try manually: ${npmBin()} i ${plan.installMode === 'global-npx' ? '-g' : '-D'} ${PKG}@${plan.latest}\n`);
123
+ return 1;
124
+ }
125
+ // (2) best-effort: clear our stale npx cache so a subsequent npx picks up the new version.
126
+ try {
127
+ const npxDir = path.join(os.homedir(), '.npm', '_npx');
128
+ for (const d of fs.readdirSync(npxDir)) {
129
+ const pkgJson = path.join(npxDir, d, 'node_modules', PKG, 'package.json');
130
+ if (fs.existsSync(pkgJson))
131
+ fs.rmSync(path.join(npxDir, d), { recursive: true, force: true });
132
+ }
133
+ }
134
+ catch { /* cache clear is best-effort */ }
135
+ // (3) re-pin every registered workspace that still exists.
136
+ const { runInit } = require('./init');
137
+ const newRoot = packageRoot(); // may differ after a global install; re-resolve
138
+ let repinned = 0, skipped = 0, failed = 0;
139
+ io.stdout('\nRe-pinning workspaces:\n');
140
+ for (const w of registry.workspaces) {
141
+ if (!fs.existsSync(w.target)) {
142
+ io.stdout(` 건너뜀 (경로 없음): ${w.target}\n`);
143
+ skipped++;
144
+ continue;
145
+ }
146
+ try {
147
+ const res = runInit({
148
+ target: w.target,
149
+ packageRoot: newRoot,
150
+ mode: 'guardrail',
151
+ specsDir: '.ax/specs',
152
+ settingsFile: 'local',
153
+ mcp: true,
154
+ dryRun: false,
155
+ force: true,
156
+ remove: false,
157
+ agents: w.agents,
158
+ allowAdditive: true,
159
+ });
160
+ if (res.ok) {
161
+ io.stdout(` 재핀: ${w.target}\n`);
162
+ repinned++;
163
+ }
164
+ else {
165
+ io.stdout(` 실패: ${w.target}\n`);
166
+ failed++;
167
+ }
168
+ }
169
+ catch {
170
+ io.stdout(` 실패: ${w.target}\n`);
171
+ failed++;
172
+ }
173
+ }
174
+ io.stdout(`\nUpgraded to ${plan.latest}. 재핀 ${repinned} · 건너뜀 ${skipped} · 실패 ${failed}. 새 세션에서 훅/MCP 가 새 배선을 읽습니다.\n`);
175
+ io.stdout(`(개별 워크스페이스는 언제든 ${(0, npx_bin_1.npxBin)()} holmes-kit init --agent <…> --force 로 재배선할 수 있습니다.)\n`);
176
+ return 0;
177
+ }
@@ -41,6 +41,7 @@ exports.readQueue = readQueue;
41
41
  exports.queueHint = queueHint;
42
42
  // @implements A-SPEC-244
43
43
  const node_crypto_1 = require("node:crypto");
44
+ const npx_bin_1 = require("../project/npx-bin");
44
45
  const fs = __importStar(require("node:fs"));
45
46
  const path = __importStar(require("node:path"));
46
47
  const screen_safe_1 = require("../cli/screen-safe");
@@ -281,13 +282,13 @@ function queueHint(root, req) {
281
282
  // template-owned margin; this producer, read by the operator AND relayed by the agent, never got
282
283
  // one. The first row keeps the template's own opening; the rest sit under the margin.
283
284
  return (0, screen_safe_1.wrapColumns)(`\n[보류 — 운영자 질문: ${(0, screen_safe_1.flattenField)(held.question ?? '(질문 없음)', 300)}] 게이트는 닫혀 있습니다.`
284
- + ' 이 질문에 대한 답을 사용자에게 보고하십시오. 결정은 운영자가 npx holmes-kit approve 에서 내립니다.', 76, ' \u2502 ');
285
+ + ` 이 질문에 대한 답을 사용자에게 보고하십시오. 결정은 운영자가 ${(0, npx_bin_1.npxBin)()} holmes-kit approve 에서 내립니다.`, 76, ' \u2502 ');
285
286
  }
286
287
  const decided = state.decisions[id];
287
288
  const denialLine = decided?.event === 'denied'
288
289
  ? (0, screen_safe_1.wrapColumns)(`\n[거부됨${decided.reason ? `: ${(0, screen_safe_1.flattenField)(decided.reason, 300)}` : ''}] 같은 요청을 반복하지 말고 거부 사유를 해소해 사용자와 상의하십시오.`, 76, ' \u2502 ')
289
290
  : '';
290
291
  return enqueueApprovalRequest(root, req)
291
- ? `${denialLine}\n[승인 대기 ${id}] 운영자: npx holmes-kit approve`
292
+ ? `${denialLine}\n[승인 대기 ${id}] 운영자: ${(0, npx_bin_1.npxBin)()} holmes-kit approve`
292
293
  : denialLine;
293
294
  }
@@ -76,6 +76,41 @@ function pkgVersion() {
76
76
  return '0.0.0';
77
77
  }
78
78
  }
79
+ /**
80
+ * @implements A-SPEC-543.3
81
+ * If this workspace's holmes pin is behind the installed package, return a one-line NUDGE to run
82
+ * `holmes-kit upgrade`; otherwise return ''. Detection is zero-config (automatic every session), but
83
+ * the re-pin WRITE is NOT done here: `runInit({force:true})` is gated by a governance-wiring approval,
84
+ * so a silent session-start write would bypass it. The write stays the user's explicit choice via
85
+ * `upgrade`/`init --force`. Forward-drift + opt-out judgment is the pure `repinDecision`; every
86
+ * failure returns '' (fail-open — a banner is never a gate).
87
+ */
88
+ function driftNudge(cwd, installed, env) {
89
+ try {
90
+ const { mcpLaunchVersion } = require('../cli/mcp-version');
91
+ const { repinDecision } = require('../update/upgrade-plan');
92
+ const { npxBin } = require('../project/npx-bin');
93
+ const mcpPath = path.join(cwd, '.mcp.json');
94
+ if (!fs.existsSync(mcpPath))
95
+ return '';
96
+ const mcp = JSON.parse(fs.readFileSync(mcpPath, 'utf8'));
97
+ const entry = mcp.mcpServers?.['holmes-kit'];
98
+ if (!entry || typeof entry.command !== 'string' || !Array.isArray(entry.args))
99
+ return '';
100
+ const pinned = mcpLaunchVersion({ command: entry.command, args: entry.args }, (dir) => { try {
101
+ return String(JSON.parse(fs.readFileSync(path.join(dir, 'package.json'), 'utf8')).version);
102
+ }
103
+ catch {
104
+ return undefined;
105
+ } });
106
+ if (!repinDecision({ pinned, installed, env }).repin)
107
+ return '';
108
+ return `\n[Holmes-Kit] this workspace is pinned to ${pinned}, but ${installed} is installed — run \`${npxBin()} holmes-kit upgrade\` to re-pin.`;
109
+ }
110
+ catch {
111
+ return '';
112
+ }
113
+ }
79
114
  // CLI entry: read the hook JSON on stdin (unused beyond triggering), emit the banner, and — when
80
115
  // allowed — fire the detached refresh. Wrapped so any failure is an empty output + exit 0.
81
116
  if (require.main === module) {
@@ -91,6 +126,16 @@ if (require.main === module) {
91
126
  readFile: (p) => fs.readFileSync(p, 'utf8'),
92
127
  mode: detectInstallMode(pkgRootFromEntry()),
93
128
  });
129
+ // @implements A-SPEC-543.3 — self-healing re-pin: when this workspace's holmes pin is OLDER
130
+ // than the installed package, the user already chose to install; complete that choice by
131
+ // re-pinning the derived artifacts and telling the operator to restart. Forward-only, opt-out
132
+ // via HOLMES_NO_AUTO_REPIN, and FAIL-OPEN — any failure leaves the banner exactly as it was.
133
+ try {
134
+ const notice = driftNudge(process.cwd(), pkgVersion(), process.env);
135
+ if (notice)
136
+ out.hookSpecificOutput.additionalContext += notice;
137
+ }
138
+ catch { /* the banner is never a gate */ }
94
139
  process.stdout.write(JSON.stringify({ hookSpecificOutput: out.hookSpecificOutput }));
95
140
  if (out.shouldRefresh) {
96
141
  // Detached, unref'd child so the session start does not wait on the network. The refresh
@@ -49,6 +49,7 @@ exports.guardCountOrZero = guardCountOrZero;
49
49
  exports.readGuardCount = readGuardCount;
50
50
  exports.writeGuardCount = writeGuardCount;
51
51
  const fs = __importStar(require("node:fs"));
52
+ const npx_bin_1 = require("../project/npx-bin");
52
53
  const risk_gate_1 = require("../guardrail/risk-gate");
53
54
  const json_state_1 = require("../project/json-state");
54
55
  const node_child_process_1 = require("node:child_process");
@@ -813,7 +814,7 @@ if (require.main === module) {
813
814
  else if (ackWaiting.length > 0) {
814
815
  // @implements A-SPEC-247 — acknowledged, not clean: name what is waiting so the user sees
815
816
  // the standing approval debt exactly once, and the agent knows the ball is not in its court.
816
- process.stderr.write(`[Holmes-Kit] 승인 대기 중 — 오너 결정을 기다리는 항목: ${ackWaiting.join(', ')}. npx holmes-kit approve 에서 결재하십시오. (이 부채로는 재차단하지 않습니다)\n`);
817
+ process.stderr.write(`[Holmes-Kit] 승인 대기 중 — 오너 결정을 기다리는 항목: ${ackWaiting.join(', ')}. ${(0, npx_bin_1.npxBin)()} holmes-kit approve 에서 결재하십시오. (이 부채로는 재차단하지 않습니다)\n`);
817
818
  }
818
819
  else if (guard.capped) {
819
820
  process.stderr.write(`[Holmes-Kit] governance gate YIELDING after ${exports.MAX_CONSECUTIVE_BLOCKS} consecutive blocks — issues remain UNRESOLVED:\n${out.reason ?? ''}\n`);
@@ -1,5 +1,4 @@
1
1
  "use strict";
2
- // @implements A-SPEC-263.1
3
2
  Object.defineProperty(exports, "__esModule", { value: true });
4
3
  exports.MAX_PRESENTATIONS = exports.ELICIT_TIMEOUT_MS = exports.ELICITABLE_KINDS = void 0;
5
4
  exports.elicitationDisabled = elicitationDisabled;
@@ -10,6 +9,8 @@ exports.buildDecisionForm = buildDecisionForm;
10
9
  exports.buildReasonForm = buildReasonForm;
11
10
  exports.runElicitFlow = runElicitFlow;
12
11
  exports.interpretElicitResult = interpretElicitResult;
12
+ // @implements A-SPEC-263.1
13
+ const npx_bin_1 = require("../project/npx-bin");
13
14
  /**
14
15
  * The decision comes INTO the session (REQ-263): an approval-shaped refusal (spec_approve without a
15
16
  * covering HOLMES_APPROVAL) becomes an in-session question when the client advertises the MCP
@@ -89,7 +90,7 @@ function classifyElicitError(e, timeoutMs = exports.ELICIT_TIMEOUT_MS) {
89
90
  * Template-owned text with no interpolated attacker data.
90
91
  */
91
92
  function expiredNotice(waitedMs) {
92
- return `[세션 승인 다이얼로그 만료 — ${Math.round(waitedMs / 1000)}초 무응답] 요청은 승인 큐로 회송되었습니다. 결정: npx holmes-kit approve. `;
93
+ return `[세션 승인 다이얼로그 만료 — ${Math.round(waitedMs / 1000)}초 무응답] 요청은 승인 큐로 회송되었습니다. 결정: ${(0, npx_bin_1.npxBin)()} holmes-kit approve. `;
93
94
  }
94
95
  // FLATTEN attacker-influenced text (round-1): the model chooses spec titles and finding ids, and
95
96
  // interpolating them raw let a crafted title inject fake lines ("[시스템] … approve.") into the
@@ -111,7 +112,7 @@ function buildDecisionForm(req, attempt, timeoutMs) {
111
112
  // @implements A-SPEC-497.1 — the forewarning is the LAST line and template-owned: it survives a
112
113
  // cap-filling summary (appended after the caps) and tells the human, before the clock runs out,
113
114
  // where an undecided request goes and where the decision still lives.
114
- message: `${redisplay}[Holmes-Kit 승인 요청] ${flat(req.kind, 40)} — ${flat(req.target, 80)}\n${flat(req.summary, 200)}\n승인(approve) / 거부(deny) / 질문(question) 을 선택하세요. 사유는 거부·질문을 고르면 이어서 묻습니다.\n⏱ ${timeoutMs / 1000}초 내 미결정 시 승인 큐로 회송됩니다(운영자: npx holmes-kit approve).`,
115
+ message: `${redisplay}[Holmes-Kit 승인 요청] ${flat(req.kind, 40)} — ${flat(req.target, 80)}\n${flat(req.summary, 200)}\n승인(approve) / 거부(deny) / 질문(question) 을 선택하세요. 사유는 거부·질문을 고르면 이어서 묻습니다.\n⏱ ${timeoutMs / 1000}초 내 미결정 시 승인 큐로 회송됩니다(운영자: ${(0, npx_bin_1.npxBin)()} holmes-kit approve).`,
115
116
  requestedSchema: {
116
117
  type: 'object',
117
118
  properties: {
@@ -458,7 +458,7 @@ declare function makeRawHandlers(store: SpecStore, opts?: ElicitOpts): {
458
458
  baselineRecorded?: string | undefined;
459
459
  scopeFallback?: "full" | undefined;
460
460
  tier: import("../rtm/test-scope").RegressionTier;
461
- mode: "full" | "scoped" | "noop";
461
+ mode: "full" | "noop" | "scoped";
462
462
  passed: boolean;
463
463
  skipped: boolean;
464
464
  ranFiles: string[];
@@ -0,0 +1,10 @@
1
+ /**
2
+ * @implements A-SPEC-542.1
3
+ * The npx binary an OPERATOR can actually run on the platform this process runs on — which is the
4
+ * platform of the machine whose terminal reads the emitted hint. Field-measured 2026-09-05 (codex on
5
+ * Windows): a locked-down PowerShell execution policy blocks the `npx` call because the `.ps1` shim
6
+ * is resolved first (PSSecurityException); `npx.cmd` runs. Everywhere else `npx` is correct, so every
7
+ * non-win32 emission stays byte-identical. Only the exact 'win32' token switches — a typo or unknown
8
+ * platform must never flip a POSIX hint to a Windows-only form.
9
+ */
10
+ export declare function npxBin(platform?: string): string;
@@ -0,0 +1,16 @@
1
+ "use strict";
2
+ // @implements A-SPEC-542.1
3
+ Object.defineProperty(exports, "__esModule", { value: true });
4
+ exports.npxBin = npxBin;
5
+ /**
6
+ * @implements A-SPEC-542.1
7
+ * The npx binary an OPERATOR can actually run on the platform this process runs on — which is the
8
+ * platform of the machine whose terminal reads the emitted hint. Field-measured 2026-09-05 (codex on
9
+ * Windows): a locked-down PowerShell execution policy blocks the `npx` call because the `.ps1` shim
10
+ * is resolved first (PSSecurityException); `npx.cmd` runs. Everywhere else `npx` is correct, so every
11
+ * non-win32 emission stays byte-identical. Only the exact 'win32' token switches — a typo or unknown
12
+ * platform must never flip a POSIX hint to a Windows-only form.
13
+ */
14
+ function npxBin(platform = process.platform) {
15
+ return platform === 'win32' ? 'npx.cmd' : 'npx';
16
+ }
@@ -0,0 +1,43 @@
1
+ import { InstallMode } from './update-notice';
2
+ import { Registry, WorkspaceEntry } from './workspaces';
3
+ /** What `holmes-kit upgrade` would do — pure facts; wording and refusals belong to the CLI. */
4
+ export type UpgradePlan = {
5
+ kind: 'noop';
6
+ reason: string;
7
+ } | {
8
+ kind: 'run';
9
+ current: string;
10
+ latest: string;
11
+ installMode: InstallMode;
12
+ repins: WorkspaceEntry[];
13
+ };
14
+ /**
15
+ * @implements A-SPEC-543.2
16
+ * Pure: decide whether an upgrade runs and what it touches. Unknown latest is a noop (silence is not
17
+ * an install), and so is an equal or OLDER latest — upgrade never plans a downgrade. A 'run' plan
18
+ * carries the registry's workspaces verbatim as the re-pin list; an empty list is still a valid run
19
+ * (install-only, first machine).
20
+ */
21
+ export declare function planUpgrade(a: {
22
+ current: string;
23
+ latest: string | null;
24
+ registry: Registry;
25
+ installMode: InstallMode;
26
+ }): UpgradePlan;
27
+ /** The env switch that turns SessionStart's forward self-heal re-pin OFF. */
28
+ export declare const NO_AUTO_REPIN_ENV = "HOLMES_NO_AUTO_REPIN";
29
+ /**
30
+ * @implements A-SPEC-543.3
31
+ * Pure: should a session auto re-pin this workspace's derived artifacts? Only when the pin is a real
32
+ * version, the installed package is strictly NEWER (forward-only — never a downgrade or a match), and
33
+ * the operator has not opted out. The install was the user's explicit choice; this completes it at
34
+ * the workspace level. A decision always says why.
35
+ */
36
+ export declare function repinDecision(a: {
37
+ pinned: string | null;
38
+ installed: string;
39
+ env: NodeJS.ProcessEnv;
40
+ }): {
41
+ repin: boolean;
42
+ reason: string;
43
+ };
@@ -0,0 +1,45 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.NO_AUTO_REPIN_ENV = void 0;
4
+ exports.planUpgrade = planUpgrade;
5
+ exports.repinDecision = repinDecision;
6
+ // @implements A-SPEC-543.2
7
+ const update_notice_1 = require("./update-notice");
8
+ /**
9
+ * @implements A-SPEC-543.2
10
+ * Pure: decide whether an upgrade runs and what it touches. Unknown latest is a noop (silence is not
11
+ * an install), and so is an equal or OLDER latest — upgrade never plans a downgrade. A 'run' plan
12
+ * carries the registry's workspaces verbatim as the re-pin list; an empty list is still a valid run
13
+ * (install-only, first machine).
14
+ */
15
+ function planUpgrade(a) {
16
+ if (a.latest === null) {
17
+ return { kind: 'noop', reason: 'latest version unknown (registry unreachable and no cache) — nothing to plan' };
18
+ }
19
+ if ((0, update_notice_1.compareSemver)(a.latest, a.current) <= 0) {
20
+ return { kind: 'noop', reason: `already up to date (current ${a.current}, latest ${a.latest})` };
21
+ }
22
+ return { kind: 'run', current: a.current, latest: a.latest, installMode: a.installMode, repins: a.registry.workspaces };
23
+ }
24
+ /** The env switch that turns SessionStart's forward self-heal re-pin OFF. */
25
+ exports.NO_AUTO_REPIN_ENV = 'HOLMES_NO_AUTO_REPIN';
26
+ /**
27
+ * @implements A-SPEC-543.3
28
+ * Pure: should a session auto re-pin this workspace's derived artifacts? Only when the pin is a real
29
+ * version, the installed package is strictly NEWER (forward-only — never a downgrade or a match), and
30
+ * the operator has not opted out. The install was the user's explicit choice; this completes it at
31
+ * the workspace level. A decision always says why.
32
+ */
33
+ function repinDecision(a) {
34
+ const optOut = a.env[exports.NO_AUTO_REPIN_ENV];
35
+ if (typeof optOut === 'string' && optOut !== '') {
36
+ return { repin: false, reason: `${exports.NO_AUTO_REPIN_ENV} set — auto re-pin disabled` };
37
+ }
38
+ if (a.pinned === null) {
39
+ return { repin: false, reason: 'workspace pin unreadable — no re-pin on an unknown pin' };
40
+ }
41
+ if ((0, update_notice_1.compareSemver)(a.installed, a.pinned) > 0) {
42
+ return { repin: true, reason: `installed ${a.installed} is newer than pin ${a.pinned} — re-pin forward` };
43
+ }
44
+ return { repin: false, reason: `pin ${a.pinned} is up to date with installed ${a.installed} — no re-pin` };
45
+ }
@@ -0,0 +1,35 @@
1
+ /** One wired workspace, as the registry answers "what is wired NOW" (not a history). */
2
+ export interface WorkspaceEntry {
3
+ target: string;
4
+ agents: string[];
5
+ version: string;
6
+ ts: string;
7
+ }
8
+ export interface Registry {
9
+ version: 1;
10
+ workspaces: WorkspaceEntry[];
11
+ }
12
+ export declare const EMPTY_REGISTRY: Registry;
13
+ /**
14
+ * @implements A-SPEC-543.1
15
+ * Parse the registry file's content. `null` (absent), broken JSON, or a wrong shape all yield the
16
+ * empty registry — best-effort by design: a broken registry restarts empty rather than ever breaking
17
+ * wiring (the recorder that feeds it is swallowed too).
18
+ */
19
+ export declare function readRegistry(raw: string | null): Registry;
20
+ /**
21
+ * @implements A-SPEC-543.1
22
+ * Pure: replace the entry whose `target` matches (path-identity dedup — the registry answers what is
23
+ * wired NOW), append otherwise, preserve order, never mutate the inputs.
24
+ */
25
+ export declare function mergeWorkspaceEntry(reg: Registry, entry: WorkspaceEntry): Registry;
26
+ /**
27
+ * @implements A-SPEC-543.1
28
+ * Thin best-effort recorder: read → merge → write `~/.holmes/workspaces.json`. Every failure is
29
+ * swallowed (returns false) — a registry problem must never fail the init that feeds it.
30
+ */
31
+ export declare function recordWorkspace(home: string, entry: WorkspaceEntry, io?: {
32
+ read: (p: string) => string;
33
+ write: (p: string, c: string) => void;
34
+ mkdir: (p: string) => void;
35
+ }): boolean;
@@ -0,0 +1,109 @@
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.EMPTY_REGISTRY = void 0;
37
+ exports.readRegistry = readRegistry;
38
+ exports.mergeWorkspaceEntry = mergeWorkspaceEntry;
39
+ exports.recordWorkspace = recordWorkspace;
40
+ // @implements A-SPEC-543.1
41
+ const fs = __importStar(require("node:fs"));
42
+ const path = __importStar(require("node:path"));
43
+ exports.EMPTY_REGISTRY = { version: 1, workspaces: [] };
44
+ /**
45
+ * @implements A-SPEC-543.1
46
+ * Parse the registry file's content. `null` (absent), broken JSON, or a wrong shape all yield the
47
+ * empty registry — best-effort by design: a broken registry restarts empty rather than ever breaking
48
+ * wiring (the recorder that feeds it is swallowed too).
49
+ */
50
+ function readRegistry(raw) {
51
+ if (raw === null)
52
+ return { version: 1, workspaces: [] };
53
+ try {
54
+ const parsed = JSON.parse(raw);
55
+ if (!Array.isArray(parsed?.workspaces))
56
+ return { version: 1, workspaces: [] };
57
+ const workspaces = parsed.workspaces.filter((w) => !!w && typeof w.target === 'string'
58
+ && Array.isArray(w.agents)
59
+ && typeof w.version === 'string'
60
+ && typeof w.ts === 'string');
61
+ return { version: 1, workspaces };
62
+ }
63
+ catch {
64
+ return { version: 1, workspaces: [] };
65
+ }
66
+ }
67
+ /**
68
+ * @implements A-SPEC-543.1
69
+ * Pure: replace the entry whose `target` matches (path-identity dedup — the registry answers what is
70
+ * wired NOW), append otherwise, preserve order, never mutate the inputs.
71
+ */
72
+ function mergeWorkspaceEntry(reg, entry) {
73
+ const replaced = reg.workspaces.some((w) => w.target === entry.target);
74
+ return {
75
+ version: 1,
76
+ workspaces: replaced
77
+ ? reg.workspaces.map((w) => (w.target === entry.target ? entry : w))
78
+ : [...reg.workspaces, entry],
79
+ };
80
+ }
81
+ /**
82
+ * @implements A-SPEC-543.1
83
+ * Thin best-effort recorder: read → merge → write `~/.holmes/workspaces.json`. Every failure is
84
+ * swallowed (returns false) — a registry problem must never fail the init that feeds it.
85
+ */
86
+ function recordWorkspace(home, entry, io = {
87
+ read: (p) => fs.readFileSync(p, 'utf8'),
88
+ write: (p, c) => fs.writeFileSync(p, c),
89
+ mkdir: (p) => { fs.mkdirSync(p, { recursive: true }); },
90
+ }) {
91
+ try {
92
+ const dir = path.join(home, '.holmes');
93
+ const file = path.join(dir, 'workspaces.json');
94
+ let raw = null;
95
+ try {
96
+ raw = io.read(file);
97
+ }
98
+ catch {
99
+ raw = null;
100
+ }
101
+ const merged = mergeWorkspaceEntry(readRegistry(raw), entry);
102
+ io.mkdir(dir);
103
+ io.write(file, `${JSON.stringify(merged, null, 2)}\n`);
104
+ return true;
105
+ }
106
+ catch {
107
+ return false;
108
+ }
109
+ }
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.10.0",
4
+ "version": "0.11.0",
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",