@holmes-lab/holmes-kit 0.10.1 → 0.12.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,64 @@ 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.1] - 2026-09-04
8
+ ## [0.12.0] - 2026-09-05
9
+
10
+ World-top-tier RTM dashboard, in flight. The `holmes-kit serve` dashboard stops being a table of
11
+ accordions and becomes a real coverage matrix, gains a one-call launcher, and learns to serve CFG/PDG
12
+ for a function. Additive and backward-compatible — every existing `/api/*` payload keeps its shape and
13
+ values; these are new fields, a new endpoint, a new tool, and a rebuilt heatmap view.
14
+
15
+ ### Added
16
+
17
+ - **2D coverage matrix** (REQ-545): the heatmap tab is now a single **requirement × pipeline-stage
18
+ grid** — rows are requirements ordered by completeness (seriation), each cell a sequential-ramp
19
+ colour with the coverage percent printed in it (so magnitude never rides on colour alone). Replaces
20
+ the per-REQ accordion-of-tables. `/api/rtm/heatmap` gains an additive `matrix` field
21
+ (`{reqs, stages, values, order, buckets}`) built by pure, unit-tested `buildStageMatrix` +
22
+ `seriateRows` + `coverageBucket`.
23
+ - **`rtm_dashboard` MCP tool** (REQ-545): ask to *see* the RTM heatmap / coverage matrix and the
24
+ server launches (idempotently — a second call reuses the live one) and returns `{ url, running,
25
+ census }`, where `census` is an honesty summary (`reqCount, pipelineCount, coveredCount,
26
+ coveragePct, retiredCount, findingsScanned`). No more starting `serve` by hand.
27
+ - **`/api/cfg` — control-flow + program-dependence for one function** (REQ-545): `?file=&symbol=`
28
+ returns that function's CFG (blocks with source line/label, control-flow edges) and PDG (block-level
29
+ control deps + reaching-def data deps), consumed from the same `cfgOf`/`pdgOf` the taint lane uses.
30
+ A non-CFG language is named as `unsupported`, never faked as an empty graph. (Graph-view rendering
31
+ of this lands next.)
32
+
33
+ ### Changed
34
+
35
+ - **Dashboard design tokens** (REQ-545): the palette moved off the Tailwind-default + emoji-marker
36
+ look to a tokenised system — a single-hue sequential coverage ramp, reserved status colours, a
37
+ UI/mono type pairing (data in tabular-nums), surfaces and ink, with a light and a dark palette via
38
+ `prefers-color-scheme`. Applied to the new matrix view and the tab bar; structure emoji removed
39
+ (meaning markers only). The rest of the dashboard tokenises incrementally in the follow-on slice.
40
+ - **Upgrade nudges point at one command** (REQ-544): the session banner's drift line and doctor's
41
+ version-drift fix hint now name `holmes-kit upgrade` (the right binary per platform — `npx.cmd` on
42
+ win32) instead of spelling out a manual `npm i` + `init --force`.
43
+
44
+ ## [0.11.0] - 2026-09-05
45
+
46
+ Zero-config upgrades — preparation is automatic, execution is your explicit choice. Additive and
47
+ backward-compatible; nothing changes at the default settings until you run `holmes-kit upgrade`.
48
+
49
+ ### Added
50
+
51
+ - **`holmes-kit upgrade`** (REQ-543): one command to move every wired workspace to the latest — it
52
+ shows a plan (current → latest, and which workspaces it will re-pin), asks for confirmation, then
53
+ installs per install-mode, clears the stale npx cache, re-pins each recorded workspace, and prints
54
+ a per-workspace result table. `--dry-run` prints the plan and changes nothing; `--yes` skips the
55
+ prompt (required in a non-interactive shell — the execution is a deliberate choice, never a silent
56
+ auto-install of a gate-enforcing engine). A source checkout is told to upgrade via git.
57
+ - **Workspace registry** (REQ-543): every successful `init` now records where holmes-kit is wired
58
+ (`~/.holmes/workspaces.json` — target, agents, version), so `upgrade` knows every workspace to
59
+ re-pin. Best-effort and side-channel: a registry problem never affects `init`.
60
+ - **Session drift nudge** (REQ-543): when a session starts in a workspace whose `.mcp.json` pin is
61
+ OLDER than the installed package, the banner adds one line pointing at `holmes-kit upgrade`.
62
+ Detection is automatic; the re-pin **write** stays behind your `upgrade`/`init --force` choice —
63
+ a session-start force-write would bypass the governance-wiring approval, so it is not done silently.
64
+ Opt out with `HOLMES_NO_AUTO_REPIN`; no drift, opt-out, or unreadable pin leaves the banner
65
+ byte-identical.
9
66
 
10
67
  ### Fixed
11
68
 
package/README.md CHANGED
@@ -19,6 +19,8 @@
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`.
23
+ - 📊 **World-top-tier RTM dashboard** *(new in 0.12.0, in flight)*: `holmes-kit serve` — ask to *see* the RTM heatmap and the `rtm_dashboard` MCP tool launches the server idempotently and hands back the URL plus an honesty **census** (requirement/pipeline counts, coverage %, what's excluded). The heatmap is now a real **2D coverage matrix** (requirements × pipeline stages, rows seriated by completeness, sequential-ramp cells with the percent printed in each), and `/api/cfg?file=&symbol=` serves a function's **CFG + PDG** (control-flow blocks + data/control dependence) from the same engine the taint lane uses — a non-CFG language is named, never faked. Tokenised palette + light/dark; graph-view rendering of CFG/PDG lands next.
22
24
  - 🤖 **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
25
  - 🪧 **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
26
  - 🧱 **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
- b8567d9-mtn379s6
1
+ 85d7b53-mtnr6dpp
@@ -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,9 @@ 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} 입니다 — 재배선 전까지 옛 서버가 계속 뜹니다`,
761
+ // @implements A-SPEC-544.1 — one command installs + re-pins every workspace; init --force is the per-workspace path.
762
+ `${(0, npx_bin_1.npxBin)()} holmes-kit upgrade 로 최신 설치 + 전 워크스페이스 재핀 (이 워크스페이스만 고치려면 ${(0, npx_bin_1.npxBin)()} holmes-kit init --force).`);
760
763
  }
761
764
  else if (verdict === 'match') {
762
765
  add('mcp server version', 'PASS', `MCP 서버 버전 ${launch} 가 이 CLI 와 일치합니다`);
@@ -810,7 +813,7 @@ async function runDoctor(packageRoot, target, opts, extraChecks) {
810
813
  const h = JSON.parse(fs.readFileSync(agyHooks, 'utf8'));
811
814
  const entry = h['holmes-kit'];
812
815
  const pre = entry?.PreToolUse?.[0];
813
- // @implements A-SPEC-539.1 — the hook script path now lives in args[0] (command is bare
816
+ // @implements A-SPEC-541.1 — the hook script path now lives in args[0] (command is bare
814
817
  // 'node'); older wirings joined it into the command string. Read args first, fall back to
815
818
  // parsing the string so a pre-539 hooks.json is still judged correctly.
816
819
  const scriptOf = (e) => (Array.isArray(e?.args) && typeof e?.args[0] === 'string') ? e.args[0] : (0, settings_merge_1.hookScriptPath)(e?.command ?? '');
@@ -1280,7 +1283,7 @@ function pushGateCheck(target) {
1280
1283
  : {
1281
1284
  name: 'push gate', level: 'WARN',
1282
1285
  detail: 'pre-push 증빙 게이트가 설치되어 있지 않습니다 — 미검증 HEAD 가 다른 에이전트의 기준선이 될 수 있습니다',
1283
- fix: 'npx holmes-kit install-push-gate 를 실행하십시오 (기존 훅은 덮지 않습니다)',
1286
+ fix: `${(0, npx_bin_1.npxBin)()} holmes-kit install-push-gate 를 실행하십시오 (기존 훅은 덮지 않습니다)`,
1284
1287
  };
1285
1288
  }
1286
1289
  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: {
@@ -237,6 +237,22 @@ declare function makeRawHandlers(store: SpecStore, opts?: ElicitOpts): {
237
237
  events: import("../governance/ledger-timeline").TimelineEntry[];
238
238
  reason?: undefined;
239
239
  }>;
240
+ rtm_dashboard(a: {
241
+ root?: string;
242
+ port?: number;
243
+ }): Promise<{
244
+ ok: boolean;
245
+ reason: string;
246
+ url?: undefined;
247
+ running?: undefined;
248
+ census?: undefined;
249
+ } | {
250
+ ok: boolean;
251
+ url: string;
252
+ running: boolean;
253
+ census: import("../server/dashboard-launcher").DashboardCensus;
254
+ reason?: undefined;
255
+ }>;
240
256
  spec_approve(a: {
241
257
  root?: string;
242
258
  id: string;
@@ -458,7 +474,7 @@ declare function makeRawHandlers(store: SpecStore, opts?: ElicitOpts): {
458
474
  baselineRecorded?: string | undefined;
459
475
  scopeFallback?: "full" | undefined;
460
476
  tier: import("../rtm/test-scope").RegressionTier;
461
- mode: "full" | "scoped" | "noop";
477
+ mode: "full" | "noop" | "scoped";
462
478
  passed: boolean;
463
479
  skipped: boolean;
464
480
  ranFiles: string[];
@@ -45,6 +45,7 @@ exports.makeHandlers = makeHandlers;
45
45
  // @implements A-SPEC-269
46
46
  // @implements A-SPEC-267
47
47
  const fs = __importStar(require("node:fs"));
48
+ const http = __importStar(require("node:http"));
48
49
  const assoc_arm_1 = require("../assoc/assoc-arm");
49
50
  // @implements A-SPEC-478 — the semantic tier runtime, resolved HERE (the async edge) and injected
50
51
  // into the sync analysis; the replay harness injects nothing, keeping every pin untouched.
@@ -213,6 +214,21 @@ class HandlerRefusal extends Error {
213
214
  constructor(message) { super(message); this.name = 'HandlerRefusal'; }
214
215
  }
215
216
  exports.HandlerRefusal = HandlerRefusal;
217
+ /** GET a localhost JSON endpoint (used by rtm_dashboard to read the census off the server it launched). */
218
+ function fetchJson(url) {
219
+ return new Promise((resolve, reject) => {
220
+ http.get(url, (res) => {
221
+ let data = '';
222
+ res.on('data', (c) => (data += c));
223
+ res.on('end', () => { try {
224
+ resolve(JSON.parse(data));
225
+ }
226
+ catch (e) {
227
+ reject(e);
228
+ } });
229
+ }).on('error', reject);
230
+ });
231
+ }
216
232
  /** Marker test that survives realm boundaries (jest's instanceof does not). */
217
233
  function isHandlerRefusal(e) {
218
234
  return !!e && typeof e === 'object' && e.holmesRefusal === true;
@@ -1322,6 +1338,24 @@ function makeRawHandlers(store, opts) {
1322
1338
  const events = fs.existsSync(dir) ? new ledger_store_1.FileLedgerStore(dir).loadAll() : [];
1323
1339
  return { ok: true, events: (0, ledger_timeline_1.timelineFrom)(events, a.id) };
1324
1340
  },
1341
+ // @implements A-SPEC-545.3 — "show me the RTM dashboard" launches (idempotently) the server the
1342
+ // agent would otherwise start by hand, and returns its URL plus an honesty census of what it shows.
1343
+ async rtm_dashboard(a) {
1344
+ const dest = resolveLedgerRoot(a.root);
1345
+ if (!dest.ok)
1346
+ return { ok: false, reason: dest.reason };
1347
+ try {
1348
+ const { startDashboardServer } = await Promise.resolve().then(() => __importStar(require('../server/dashboard')));
1349
+ const { ensureDashboard, dashboardCensus } = await Promise.resolve().then(() => __importStar(require('../server/dashboard-launcher')));
1350
+ const launch = await ensureDashboard(dest.root, a.port, (opts) => startDashboardServer(opts));
1351
+ const rtm = await fetchJson(`${launch.url}/api/rtm`);
1352
+ const heatmap = await fetchJson(`${launch.url}/api/rtm/heatmap`);
1353
+ return { ok: true, url: launch.url, running: launch.running, census: dashboardCensus(rtm, heatmap) };
1354
+ }
1355
+ catch (err) {
1356
+ return { ok: false, reason: `대시보드 기동 실패: ${err?.message ?? String(err)}` };
1357
+ }
1358
+ },
1325
1359
  async spec_approve(a) {
1326
1360
  const approvalRaw = process.env.HOLMES_APPROVAL;
1327
1361
  let approval;