@massu/core 2.3.1 → 2.5.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
@@ -10,6 +10,137 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
10
10
  > redistributed under the **Apache License 2.0**. Full license + NOTICE:
11
11
  > `packages/core/assets/embedder/MODEL-LICENSE`.
12
12
 
13
+ ## [2.5.0] - 2026-08-01
14
+
15
+ ### Fixed
16
+
17
+ - **`install-hooks` ADDED a hook registration instead of REPLACING it, so every version bump
18
+ doubled the hooks — and that defect BLOCKED ITS OWN REMEDY.** `mergeHookEntries` deduped by
19
+ EXACT COMMAND STRING, and the emitted command embeds the version, so
20
+ `npx -y @massu/core@1.16.2 hook-runner user-prompt` and the `@2.4.0` form were two different
21
+ strings, two different hooks, BOTH kept. The command was simultaneously the payload and the
22
+ identity, so any payload change forged a new identity.
23
+
24
+ Measured, not theoretical: one workspace carried `1.15.5` x16 **and** `1.15.2` x15 at once,
25
+ with 15 hook names firing TWICE per event and writing double telemetry indefinitely. Worse,
26
+ because upgrading duplicated, nobody upgraded — 9 of 10 workspaces on this machine sat on a
27
+ pre-2.0.0 version whose `better-sqlite3` ABI did not match the running Node, and one had
28
+ **26,119 `NODE_MODULE_VERSION` hook failures**: a total massu outage. It stayed invisible
29
+ because a crashing hook still **exits 0**.
30
+
31
+ Registration identity is now the `hook-runner` subcommand — a stable CLI contract (`cli.ts`
32
+ dispatches on exactly that token), invariant under both a version bump and a launch-mechanism
33
+ change. Non-massu entries keep the historical exact-command semantics, so a customer's own
34
+ hooks are never touched or collapsed.
35
+
36
+ Proof it can fail (CR-72, planted in the real tree, restored byte-identical by sha256):
37
+ reverting the identity branch to exact-command dedup turns
38
+ `installer-registration-identity-drift-guard.test.ts` RED — that guard drives the REAL
39
+ `installHooks()` over a real temp project seeded with an older-version block, a node-direct
40
+ block and a customer hook.
41
+
42
+ ### Added
43
+
44
+ - **A version-stable launcher shim, cutting ~0.86s of npm bootstrap off each hook fire.**
45
+ `npx -y @massu/core@<v> …` spends ~0.86s starting npm before any massu code runs. Measured
46
+ interleaved, min-of-5: `npx` 0.97s, `npx --offline` 0.88s, `node <cli.js>` 0.11s — so the
47
+ cost is npm's own startup, NOT the registry, and no npx flag addresses it.
48
+
49
+ `install-hooks` now writes `~/.massu/bin/massu-hook` and emits
50
+ `"~/.massu/bin/massu-hook" <version> hook-runner <name>`. That path is VERSION-STABLE, and
51
+ the shim re-resolves the runtime AT FIRE TIME, exec'ing npx when it is absent.
52
+
53
+ This deliberately does NOT bake a versioned absolute path, which would reintroduce the class
54
+ P-003 (v1.9.4) eliminated: such a path is "invalidated by cache clears, global-install
55
+ relocations, or npx upgrades — silently 404-ing the hooks". Probing at emit time says nothing
56
+ about invalidation after emit. Three fail-safe layers instead — shim missing at emit emits
57
+ npx; runtime missing at fire execs npx; both present takes the fast path — so no single
58
+ missing artifact is fatal and a stale registration self-heals.
59
+
60
+ Measured after: fast path **0.11s vs npx 1.07s (9.7x)**. The fallback is mutation-proven — a
61
+ shim with its npx branch removed FAILS the same no-runtime case (exit 27), so the test is not
62
+ vacuous. `exec` in both branches preserves stdin, stdout and the exit code.
63
+
64
+ Semver MINOR: `massuHookIdentity`, `massuShimPath`, `massuShimBody`, `installMassuShim`,
65
+ `resolveMassuShim`, `massuRuntimeDir`, `massuRuntimeCliPath`, `resolveMassuRuntimeCli` and
66
+ `materializeMassuRuntime` are new exports. `MASSU_NO_NODE_DIRECT=1` forces the npx form.
67
+
68
+ ### Changed
69
+
70
+ - **CR-70 Layer 3 amended: installer symmetry is no longer npx-specific.** Both emitters read
71
+ one resolver and agree on mechanism AND pinned version. The drift-guard previously asserted
72
+ the literal `'npx'`, which pinned an implementation detail rather than the property — it
73
+ would go RED on a correct mechanism change and GREEN on a wrong one that used npx on both
74
+ sides. Three assertions were amended on the same grounds
75
+ (`init-hook-paths-no-absolute`, `init-mcp-json-pin`, `cli.test.ts`); each keeps its original
76
+ protection of a visible pinned version and a known launch mechanism.
77
+
78
+ ## [2.4.0] - 2026-07-31
79
+
80
+ ### Fixed
81
+
82
+ - **Hooks silently lost telemetry whenever two sessions wrote at once — `db-driver.ts` never
83
+ armed `PRAGMA busy_timeout`, so SQLite's default of 0ms made a blocked writer fail
84
+ INSTANTLY.** Both engine paths (`openNodeSqlite`, `openBetterSqlite3`) opened a connection
85
+ and handed it out without a busy-timeout, so a writer that met a competing writer's lock
86
+ got `SQLITE_BUSY` ("database is locked") with no wait at all. Every hook catches that,
87
+ appends to `.massu/hook-failures.jsonl`, and **exits 0** — so the observation, cost, or
88
+ session row was gone with no failing signal anywhere. Measured before the fix across two
89
+ active workspaces: **2,169 lost rows in one and 156 in the other**, still accruing. In one
90
+ of them the throw landed in `session-start`'s `createSession`, which also skipped
91
+ `pullTeamPromotions` further down the same `try` — so team/enterprise promotions silently
92
+ stopped pulling on any session that lost the race. Concurrency is the NORMAL state whenever
93
+ more than one agent session shares a repo, not an edge case.
94
+
95
+ `PRAGMA busy_timeout` is set — rather than the ctor `timeout` option — because it is
96
+ engine-agnostic across `node:sqlite` and better-sqlite3, connection-local, and needs no
97
+ lock of its own. It is applied in `db-driver.ts`, the CR-69 sole open chokepoint, so
98
+ **every** opener (hooks, MCP server, CLI) inherits it. Default **5000ms**, overridable via
99
+ `MASSU_DB_BUSY_TIMEOUT_MS`; `0` is legal and restores the old fail-instantly behaviour,
100
+ which is how the tests prove they can still go RED. A malformed value falls back to the
101
+ default rather than disabling the wait (fail-safe, not fail-open).
102
+
103
+ Semver MINOR, not PATCH: `resolveBusyTimeoutMs()`, `DEFAULT_BUSY_TIMEOUT_MS` and
104
+ `DB_BUSY_TIMEOUT_ENV` are new exports. This release also carries the unpublished 2.3.2.
105
+
106
+ Proof it can fail (CR-72, planted in the real tree, restored byte-identical by sha256):
107
+ removing the pragma from `openNodeSqlite` turns the behavioural test RED
108
+ (`db-busy-timeout.test.ts` — a `worker_threads` lock-holder, because the plan audit proved
109
+ a same-thread holder is impossible: SQLite's busy-wait blocks the event loop, so the
110
+ release timer never fires); removing it from `openBetterSqlite3` turns drift-guard clause
111
+ (e) RED; hard-coding the value turns clause (e2) RED.
112
+ New gate registered in `scripts/lib/gate-registry.json` (416 → 417).
113
+ Incident: `docs/incidents/2026-07-23-hook-db-busy-timeout-silent-loss.md`.
114
+ (plan-2026-07-23-hook-latency-silent-loss-fixes, P1)
115
+
116
+ ## [2.3.2] - 2026-07-24
117
+
118
+ ### Fixed
119
+
120
+ - **`sync-public.sh` self-exempts its bulk `git rm -rf .` / `git clean -fd` from the machine
121
+ deletion circuit breaker.** The public-mirror sync wipes the mirror with `git rm -rf .` +
122
+ `git clean -fd` — hundreds of unlinks under `$HOME`, whose bulk-delete signature the breaker
123
+ SIGSTOPs — freezing the git child mid-sync on every run. Both bulk deletes now route through a
124
+ `cb_exempt_git` helper that registers each git child pid plus a start-time identity token in
125
+ `~/.claude/cb-exempt-pids.txt` for the duration of that one command, scoped to only the pids this
126
+ script adds, fail-open (the sync never depends on the breaker), with an `EXIT`/`INT`/`TERM`
127
+ cleanup trap that de-registers them even on interrupt. The start-time token pairs with the
128
+ breaker's identity-verifying exempt list so a reused pid cannot inherit a stale exemption.
129
+ Drift-guarded (`sync-public-cb-exempt-drift-guard.test.ts`, registered as a DEFEAT-proven G-6
130
+ anti-vacuity guard).
131
+
132
+ ### Changed
133
+
134
+ - **Synced `packages/core/CHANGELOG.md` with the previously-missing `[2.3.1]` entry.** The 2.3.1
135
+ release added `[2.3.1]` to the root and website CHANGELOGs but omitted
136
+ `packages/core/CHANGELOG.md`, leaving the three copies out of sync. Parity restored.
137
+ - **Trimmed `CLAUDE.md` back under its size budget**, relocating the removed detail into
138
+ `.claude/reference/patterns-quickref.md` so `.claude/CLAUDE.md` stays under its 55,000-character
139
+ cap.
140
+
141
+ No runtime behaviour of `@massu/core` changes in this release; it is repo-tooling and documentation
142
+ only. Published because CR-64 forbids commits sitting on top of a published, tagged release.
143
+
13
144
  ## [2.3.1] - 2026-07-23
14
145
 
15
146
  ### Fixed
package/dist/cli.js CHANGED
@@ -560,6 +560,12 @@ import { join as join2 } from "path";
560
560
  function resolveDbEngine() {
561
561
  return process.env[DB_ENGINE_ENV] === "better-sqlite3" ? "better-sqlite3" : DEFAULT_DB_ENGINE;
562
562
  }
563
+ function resolveBusyTimeoutMs() {
564
+ const raw = process.env[DB_BUSY_TIMEOUT_ENV];
565
+ if (raw === void 0 || raw === "") return DEFAULT_BUSY_TIMEOUT_MS;
566
+ const n = Number(raw);
567
+ return Number.isFinite(n) && n >= 0 ? Math.floor(n) : DEFAULT_BUSY_TIMEOUT_MS;
568
+ }
563
569
  function nodeSqliteCtor() {
564
570
  if (!_nodeCtor) _nodeCtor = req2("node:sqlite").DatabaseSync;
565
571
  return _nodeCtor;
@@ -581,6 +587,7 @@ function openNodeSqlite(dbPath, opts) {
581
587
  enableForeignKeyConstraints: true,
582
588
  allowExtension: false
583
589
  });
590
+ raw.exec(`PRAGMA busy_timeout = ${resolveBusyTimeoutMs()}`);
584
591
  let savepointSeq = 0;
585
592
  const wrapStmt = (sql) => {
586
593
  const st = raw.prepare(sql);
@@ -664,7 +671,9 @@ function openNodeSqlite(dbPath, opts) {
664
671
  return handle;
665
672
  }
666
673
  function openBetterSqlite3(dbPath, opts) {
667
- return openDatabase(dbPath, opts);
674
+ const db = openDatabase(dbPath, opts);
675
+ db.exec(`PRAGMA busy_timeout = ${resolveBusyTimeoutMs()}`);
676
+ return db;
668
677
  }
669
678
  function openDatabase2(dbPath, opts = {}) {
670
679
  return resolveDbEngine() === "better-sqlite3" ? openBetterSqlite3(dbPath, opts) : openNodeSqlite(dbPath, opts);
@@ -708,7 +717,7 @@ function probeMemoryDbUsable2(opts = {}) {
708
717
  }
709
718
  return { ok: true };
710
719
  }
711
- var req2, DEFAULT_DB_ENGINE, DB_ENGINE_ENV, _nodeCtor, NODE_SQLITE_REMEDY;
720
+ var req2, DEFAULT_DB_ENGINE, DB_ENGINE_ENV, DEFAULT_BUSY_TIMEOUT_MS, DB_BUSY_TIMEOUT_ENV, _nodeCtor, NODE_SQLITE_REMEDY;
712
721
  var init_db_driver = __esm({
713
722
  "src/db-driver.ts"() {
714
723
  "use strict";
@@ -717,6 +726,8 @@ var init_db_driver = __esm({
717
726
  req2 = createRequire2(import.meta.url);
718
727
  DEFAULT_DB_ENGINE = "node-sqlite";
719
728
  DB_ENGINE_ENV = "MASSU_DB_ENGINE";
729
+ DEFAULT_BUSY_TIMEOUT_MS = 5e3;
730
+ DB_BUSY_TIMEOUT_ENV = "MASSU_DB_BUSY_TIMEOUT_MS";
720
731
  _nodeCtor = null;
721
732
  NODE_SQLITE_REMEDY = "Node's built-in node:sqlite failed to open the database. Ensure Node >= 22.16.0 (`node --version`) and that this Node build includes SQLite, then restart your MCP client / Claude Code.";
722
733
  }
@@ -12946,23 +12957,23 @@ function introspectPython(detection, projectRoot) {
12946
12957
  for (const path of candidates) {
12947
12958
  const body = readSafe(path);
12948
12959
  if (body === null) continue;
12949
- const authRegex = /\bDepends\s*\(\s*([A-Za-z_][A-Za-z0-9_]*)\s*\)/gu;
12960
+ const authRegex = new RegExp(PY_AUTH_DEP_PATTERN, "gu");
12950
12961
  forEachMatch(authRegex, body, (m3) => {
12951
12962
  const name = m3[1];
12952
12963
  if (!authDeps.has(name)) authDeps.set(name, path);
12953
12964
  });
12954
- const djangoAuthRegex = /^@\s*([a-z_][a-z0-9_]*(?:_required|_login))\b/gmu;
12965
+ const djangoAuthRegex = new RegExp(PY_DJANGO_AUTH_PATTERN, "gmu");
12955
12966
  forEachMatch(djangoAuthRegex, body, (m3) => {
12956
12967
  const name = m3[1];
12957
12968
  if (!authDeps.has(name)) authDeps.set(name, path);
12958
12969
  });
12959
- const prefixRegex = /\bAPIRouter\s*\(\s*[^)]*?prefix\s*=\s*["']([^"']+)["']/gu;
12970
+ const prefixRegex = new RegExp(PY_API_PREFIX_PATTERN, "gu");
12960
12971
  forEachMatch(prefixRegex, body, (m3) => {
12961
12972
  const fullPrefix = m3[1];
12962
12973
  const base = extractPrefixBase(fullPrefix);
12963
12974
  if (base && !prefixBases.has(base)) prefixBases.set(base, path);
12964
12975
  });
12965
- const asyncRegex = /^(@pytest\.mark\.asyncio(?:\s*\([^)]*\))?)/gmu;
12976
+ const asyncRegex = new RegExp(PY_TEST_ASYNC_PATTERN, "gmu");
12966
12977
  forEachMatch(asyncRegex, body, (m3) => {
12967
12978
  const pat = m3[1].trim();
12968
12979
  if (!testAsyncPatterns.has(pat)) testAsyncPatterns.set(pat, path);
@@ -13167,10 +13178,14 @@ function relativeTo(projectRoot, absPath) {
13167
13178
  }
13168
13179
  return basename4(absPath);
13169
13180
  }
13170
- var MAX_FILE_BYTES, MAX_SAMPLES_PER_ADAPTER, MAX_DIR_DEPTH;
13181
+ var PY_AUTH_DEP_PATTERN, PY_DJANGO_AUTH_PATTERN, PY_API_PREFIX_PATTERN, PY_TEST_ASYNC_PATTERN, MAX_FILE_BYTES, MAX_SAMPLES_PER_ADAPTER, MAX_DIR_DEPTH;
13171
13182
  var init_regex_fallback = __esm({
13172
13183
  "src/detect/regex-fallback.ts"() {
13173
13184
  "use strict";
13185
+ PY_AUTH_DEP_PATTERN = String.raw`\bDepends\s*\(\s*([A-Za-z_][A-Za-z0-9_]*)\s*\)`;
13186
+ PY_DJANGO_AUTH_PATTERN = String.raw`^@\s*([a-z_][a-z0-9_]*(?:_required|_login))\b`;
13187
+ PY_API_PREFIX_PATTERN = String.raw`\bAPIRouter\s*\(\s*[^)]*?prefix\s*=\s*["']([^"']+)["']`;
13188
+ PY_TEST_ASYNC_PATTERN = String.raw`^(@pytest\.mark\.asyncio(?:\s*\([^)]*\))?)`;
13174
13189
  MAX_FILE_BYTES = 256 * 1024;
13175
13190
  MAX_SAMPLES_PER_ADAPTER = 3;
13176
13191
  MAX_DIR_DEPTH = 6;
@@ -15965,14 +15980,23 @@ __export(init_exports, {
15965
15980
  getInstallerVersion: () => getInstallerVersion,
15966
15981
  initMemoryDir: () => initMemoryDir,
15967
15982
  installHooks: () => installHooks,
15983
+ installMassuShim: () => installMassuShim,
15968
15984
  isTemplateName: () => isTemplateName,
15969
15985
  listTemplates: () => listTemplates,
15986
+ massuHookIdentity: () => massuHookIdentity,
15987
+ massuRuntimeCliPath: () => massuRuntimeCliPath,
15988
+ massuRuntimeDir: () => massuRuntimeDir,
15989
+ massuShimBody: () => massuShimBody,
15990
+ massuShimPath: () => massuShimPath,
15991
+ materializeMassuRuntime: () => materializeMassuRuntime,
15970
15992
  mergeHooksConfig: () => mergeHooksConfig,
15971
15993
  parseInitArgs: () => parseInitArgs,
15972
15994
  printInitHelp: () => printInitHelp,
15973
15995
  registerMcpServer: () => registerMcpServer,
15974
15996
  renderConfigYaml: () => renderConfigYaml,
15975
15997
  resolveHooksDir: () => resolveHooksDir,
15998
+ resolveMassuRuntimeCli: () => resolveMassuRuntimeCli,
15999
+ resolveMassuShim: () => resolveMassuShim,
15976
16000
  resolveTemplatesDir: () => resolveTemplatesDir,
15977
16001
  runInit: () => runInit,
15978
16002
  validateWrittenConfig: () => validateWrittenConfig,
@@ -15982,6 +16006,7 @@ import { closeSync as closeSync4, existsSync as existsSync19, fsyncSync as fsync
15982
16006
  import { resolve as resolve9, basename as basename6, dirname as dirname12 } from "path";
15983
16007
  import { fileURLToPath as fileURLToPath3 } from "url";
15984
16008
  import { homedir as homedir7 } from "os";
16009
+ import { execFileSync as execFileSync2 } from "child_process";
15985
16010
  import { stringify as yamlStringify, parse as yamlParse } from "yaml";
15986
16011
  function detectFramework(projectRoot) {
15987
16012
  const result = {
@@ -16555,11 +16580,8 @@ function registerMcpServer(projectRoot) {
16555
16580
  return false;
16556
16581
  }
16557
16582
  const version = getInstallerVersion();
16558
- servers.massu = {
16559
- type: "stdio",
16560
- command: "npx",
16561
- args: ["-y", `@massu/core@${version}`]
16562
- };
16583
+ const shim = resolveMassuShim();
16584
+ servers.massu = shim !== null ? { type: "stdio", command: shim, args: [version] } : { type: "stdio", command: "npx", args: ["-y", `@massu/core@${version}`] };
16563
16585
  existing.mcpServers = servers;
16564
16586
  writeFileSync5(mcpPath, JSON.stringify(existing, null, 2) + "\n", "utf-8");
16565
16587
  return true;
@@ -16576,7 +16598,98 @@ function resolveHooksDir() {
16576
16598
  }
16577
16599
  return "node_modules/@massu/core/dist/hooks";
16578
16600
  }
16579
- function hookCmd(version, hookName) {
16601
+ function massuRuntimeDir(version) {
16602
+ return resolve9(homedir7(), ".massu", "runtime", version);
16603
+ }
16604
+ function massuRuntimeCliPath(version) {
16605
+ return resolve9(massuRuntimeDir(version), "node_modules", "@massu", "core", "dist", "cli.js");
16606
+ }
16607
+ function resolveMassuRuntimeCli(version) {
16608
+ if (process.env.MASSU_NO_NODE_DIRECT === "1") return null;
16609
+ const cli = massuRuntimeCliPath(version);
16610
+ if (!existsSync19(cli)) return null;
16611
+ try {
16612
+ execFileSync2(process.execPath, [cli, "--version"], { stdio: "ignore", timeout: 2e4 });
16613
+ return cli;
16614
+ } catch {
16615
+ return null;
16616
+ }
16617
+ }
16618
+ function materializeMassuRuntime(version) {
16619
+ if (process.env.MASSU_NO_NODE_DIRECT === "1") return false;
16620
+ if (resolveMassuRuntimeCli(version) !== null) return true;
16621
+ const dir = massuRuntimeDir(version);
16622
+ try {
16623
+ mkdirSync10(dir, { recursive: true });
16624
+ execFileSync2(
16625
+ "npm",
16626
+ ["install", "--prefix", dir, `@massu/core@${version}`, "--no-audit", "--no-fund", "--loglevel=error"],
16627
+ { stdio: "ignore", timeout: 3e5 }
16628
+ );
16629
+ } catch {
16630
+ return false;
16631
+ }
16632
+ return resolveMassuRuntimeCli(version) !== null;
16633
+ }
16634
+ function massuShimPath() {
16635
+ return resolve9(homedir7(), ".massu", "bin", "massu-hook");
16636
+ }
16637
+ function massuShimBody() {
16638
+ return `#!/bin/sh
16639
+ # massu launcher shim \u2014 GENERATED, do not edit. Recreated by \`massu install-hooks\`.
16640
+ #
16641
+ # usage: massu-hook <version> <subcommand...>
16642
+ #
16643
+ # Resolves the massu runtime AT FIRE TIME so a deleted/moved runtime degrades to npx
16644
+ # instead of silently 404-ing the hook (the P-003 failure class). exec preserves stdin,
16645
+ # stdout, stderr and the exit code.
16646
+ set -u
16647
+ if [ "$#" -lt 1 ]; then
16648
+ echo "massu-hook: missing <version> argument" >&2
16649
+ exit 2
16650
+ fi
16651
+ MASSU_VERSION="$1"
16652
+ shift
16653
+ MASSU_CLI="\${HOME}/.massu/runtime/\${MASSU_VERSION}/node_modules/@massu/core/dist/cli.js"
16654
+ if [ -f "$MASSU_CLI" ]; then
16655
+ exec node "$MASSU_CLI" "$@"
16656
+ fi
16657
+ exec npx -y "@massu/core@\${MASSU_VERSION}" "$@"
16658
+ `;
16659
+ }
16660
+ function installMassuShim() {
16661
+ if (process.env.MASSU_NO_NODE_DIRECT === "1") return false;
16662
+ if (process.platform === "win32") return false;
16663
+ const shim = massuShimPath();
16664
+ try {
16665
+ mkdirSync10(dirname12(shim), { recursive: true });
16666
+ writeFileSync5(shim, massuShimBody(), "utf-8");
16667
+ chmodSync5(shim, 493);
16668
+ } catch {
16669
+ return false;
16670
+ }
16671
+ try {
16672
+ execFileSync2(shim, [], { stdio: "ignore", timeout: 1e4 });
16673
+ return false;
16674
+ } catch (e2) {
16675
+ const status = e2.status;
16676
+ return status === 2;
16677
+ }
16678
+ }
16679
+ function resolveMassuShim() {
16680
+ if (process.env.MASSU_NO_NODE_DIRECT === "1") return null;
16681
+ if (process.platform === "win32") return null;
16682
+ const shim = massuShimPath();
16683
+ if (!existsSync19(shim)) return null;
16684
+ try {
16685
+ execFileSync2(shim, [], { stdio: "ignore", timeout: 1e4 });
16686
+ return null;
16687
+ } catch (e2) {
16688
+ return e2.status === 2 ? shim : null;
16689
+ }
16690
+ }
16691
+ function hookCmd(version, hookName, shim) {
16692
+ if (shim !== null) return `"${shim}" ${version} hook-runner ${hookName}`;
16580
16693
  return `npx -y @massu/core@${version} hook-runner ${hookName}`;
16581
16694
  }
16582
16695
  function getHookTimeout(name) {
@@ -16584,11 +16697,12 @@ function getHookTimeout(name) {
16584
16697
  }
16585
16698
  function buildHooksConfig(_hooksDir) {
16586
16699
  const version = getInstallerVersion();
16700
+ const shim = resolveMassuShim();
16587
16701
  return {
16588
16702
  SessionStart: [
16589
16703
  {
16590
16704
  hooks: [
16591
- { type: "command", command: hookCmd(version, "session-start"), timeout: getHookTimeout("session-start") }
16705
+ { type: "command", command: hookCmd(version, "session-start", shim), timeout: getHookTimeout("session-start") }
16592
16706
  ]
16593
16707
  }
16594
16708
  ],
@@ -16603,26 +16717,26 @@ function buildHooksConfig(_hooksDir) {
16603
16717
  {
16604
16718
  matcher: "Bash|Write|Edit",
16605
16719
  hooks: [
16606
- { type: "command", command: hookCmd(version, "pre-tool-use-gate"), timeout: getHookTimeout("pre-tool-use-gate") }
16720
+ { type: "command", command: hookCmd(version, "pre-tool-use-gate", shim), timeout: getHookTimeout("pre-tool-use-gate") }
16607
16721
  ]
16608
16722
  }
16609
16723
  ],
16610
16724
  PostToolUse: [
16611
16725
  {
16612
16726
  hooks: [
16613
- { type: "command", command: hookCmd(version, "post-tool-use"), timeout: getHookTimeout("post-tool-use") },
16614
- { type: "command", command: hookCmd(version, "quality-event"), timeout: getHookTimeout("quality-event") },
16615
- { type: "command", command: hookCmd(version, "cost-tracker"), timeout: getHookTimeout("cost-tracker") }
16727
+ { type: "command", command: hookCmd(version, "post-tool-use", shim), timeout: getHookTimeout("post-tool-use") },
16728
+ { type: "command", command: hookCmd(version, "quality-event", shim), timeout: getHookTimeout("quality-event") },
16729
+ { type: "command", command: hookCmd(version, "cost-tracker", shim), timeout: getHookTimeout("cost-tracker") }
16616
16730
  ]
16617
16731
  },
16618
16732
  {
16619
16733
  matcher: "Edit|Write",
16620
16734
  hooks: [
16621
- { type: "command", command: hookCmd(version, "post-edit-context"), timeout: getHookTimeout("post-edit-context") },
16735
+ { type: "command", command: hookCmd(version, "post-edit-context", shim), timeout: getHookTimeout("post-edit-context") },
16622
16736
  // Auto-learning pipeline — classifies failures and detects fixes on
16623
16737
  // file changes. See Phase 5-6 of the autodetect plan.
16624
- { type: "command", command: hookCmd(version, "fix-detector"), timeout: getHookTimeout("fix-detector") },
16625
- { type: "command", command: hookCmd(version, "classify-failure"), timeout: getHookTimeout("classify-failure") }
16738
+ { type: "command", command: hookCmd(version, "fix-detector", shim), timeout: getHookTimeout("fix-detector") },
16739
+ { type: "command", command: hookCmd(version, "classify-failure", shim), timeout: getHookTimeout("classify-failure") }
16626
16740
  ]
16627
16741
  },
16628
16742
  {
@@ -16630,35 +16744,35 @@ function buildHooksConfig(_hooksDir) {
16630
16744
  hooks: [
16631
16745
  // Incident + rule enforcement pipelines fire on Write-only (incidents
16632
16746
  // are authored as .md files; rules are enforced after new-file drops).
16633
- { type: "command", command: hookCmd(version, "incident-pipeline"), timeout: getHookTimeout("incident-pipeline") },
16634
- { type: "command", command: hookCmd(version, "rule-enforcement-pipeline"), timeout: getHookTimeout("rule-enforcement-pipeline") }
16747
+ { type: "command", command: hookCmd(version, "incident-pipeline", shim), timeout: getHookTimeout("incident-pipeline") },
16748
+ { type: "command", command: hookCmd(version, "rule-enforcement-pipeline", shim), timeout: getHookTimeout("rule-enforcement-pipeline") }
16635
16749
  ]
16636
16750
  }
16637
16751
  ],
16638
16752
  Stop: [
16639
16753
  {
16640
16754
  hooks: [
16641
- { type: "command", command: hookCmd(version, "session-end"), timeout: getHookTimeout("session-end") },
16755
+ { type: "command", command: hookCmd(version, "session-end", shim), timeout: getHookTimeout("session-end") },
16642
16756
  // Session-end auto-learning aggregation (failure-class roll-up).
16643
- { type: "command", command: hookCmd(version, "auto-learning-pipeline"), timeout: getHookTimeout("auto-learning-pipeline") }
16757
+ { type: "command", command: hookCmd(version, "auto-learning-pipeline", shim), timeout: getHookTimeout("auto-learning-pipeline") }
16644
16758
  ]
16645
16759
  }
16646
16760
  ],
16647
16761
  PreCompact: [
16648
16762
  {
16649
16763
  hooks: [
16650
- { type: "command", command: hookCmd(version, "pre-compact"), timeout: getHookTimeout("pre-compact") }
16764
+ { type: "command", command: hookCmd(version, "pre-compact", shim), timeout: getHookTimeout("pre-compact") }
16651
16765
  ]
16652
16766
  }
16653
16767
  ],
16654
16768
  UserPromptSubmit: [
16655
16769
  {
16656
16770
  hooks: [
16657
- { type: "command", command: hookCmd(version, "user-prompt"), timeout: getHookTimeout("user-prompt") },
16658
- { type: "command", command: hookCmd(version, "intent-suggester"), timeout: getHookTimeout("intent-suggester") },
16771
+ { type: "command", command: hookCmd(version, "user-prompt", shim), timeout: getHookTimeout("user-prompt") },
16772
+ { type: "command", command: hookCmd(version, "intent-suggester", shim), timeout: getHookTimeout("intent-suggester") },
16659
16773
  // plan-living-memory-slice-1 P3-002: automatic relevant-recall.
16660
16774
  // Injects a compact "🧠 Relevant memory" block; fails open (empty).
16661
- { type: "command", command: hookCmd(version, "memory-recall"), timeout: getHookTimeout("memory-recall") }
16775
+ { type: "command", command: hookCmd(version, "memory-recall", shim), timeout: getHookTimeout("memory-recall") }
16662
16776
  ]
16663
16777
  }
16664
16778
  ]
@@ -16696,21 +16810,31 @@ function mergeHooksConfig(existing, additions) {
16696
16810
  }
16697
16811
  return merged;
16698
16812
  }
16813
+ function massuHookIdentity(command) {
16814
+ if (!/@massu\/core|[/\\]\.?massu[/\\]/.test(command)) return null;
16815
+ const m3 = /\bhook-runner\s+([A-Za-z0-9._-]+)/.exec(command);
16816
+ return m3 ? m3[1] : null;
16817
+ }
16699
16818
  function mergeHookEntries(existing, additions) {
16700
- const seen = /* @__PURE__ */ new Set();
16819
+ const seenCommands = /* @__PURE__ */ new Set();
16820
+ const seenIdentities = /* @__PURE__ */ new Set();
16701
16821
  const result = [];
16702
- for (const entry of additions ?? []) {
16703
- if (!entry || typeof entry.command !== "string") continue;
16704
- if (seen.has(entry.command)) continue;
16705
- seen.add(entry.command);
16706
- result.push(entry);
16707
- }
16708
- for (const entry of existing ?? []) {
16709
- if (!entry || typeof entry.command !== "string") continue;
16710
- if (seen.has(entry.command)) continue;
16711
- seen.add(entry.command);
16822
+ const take = (entry) => {
16823
+ if (!entry || typeof entry.command !== "string") return;
16824
+ const identity = massuHookIdentity(entry.command);
16825
+ if (identity !== null) {
16826
+ if (seenIdentities.has(identity)) return;
16827
+ seenIdentities.add(identity);
16828
+ seenCommands.add(entry.command);
16829
+ result.push(entry);
16830
+ return;
16831
+ }
16832
+ if (seenCommands.has(entry.command)) return;
16833
+ seenCommands.add(entry.command);
16712
16834
  result.push(entry);
16713
- }
16835
+ };
16836
+ for (const entry of additions ?? []) take(entry);
16837
+ for (const entry of existing ?? []) take(entry);
16714
16838
  return result;
16715
16839
  }
16716
16840
  function installHooks(projectRoot) {
@@ -18701,6 +18825,16 @@ async function runInstallHooks() {
18701
18825
  console.log("Massu AI - Hook Installation");
18702
18826
  console.log("============================");
18703
18827
  console.log("");
18828
+ const version = getInstallerVersion();
18829
+ const shimOk = installMassuShim();
18830
+ const runtimeOk = materializeMassuRuntime(version);
18831
+ if (shimOk && runtimeOk) {
18832
+ console.log(" Launcher: shim + runtime ready (fast path, ~9x cheaper per hook)");
18833
+ } else if (shimOk) {
18834
+ console.log(" Launcher: shim ready, runtime unavailable \u2014 shim will fall back to npx at fire time");
18835
+ } else {
18836
+ console.log(" Launcher: npx (shim unavailable \u2014 works, ~0.9s slower per hook)");
18837
+ }
18704
18838
  const { count } = installHooks(projectRoot);
18705
18839
  console.log(` Installed ${count} hooks in .claude/settings.local.json`);
18706
18840
  console.log("");
@@ -1187,6 +1187,12 @@ import { join as join2 } from "path";
1187
1187
  function resolveDbEngine() {
1188
1188
  return process.env[DB_ENGINE_ENV] === "better-sqlite3" ? "better-sqlite3" : DEFAULT_DB_ENGINE;
1189
1189
  }
1190
+ function resolveBusyTimeoutMs() {
1191
+ const raw = process.env[DB_BUSY_TIMEOUT_ENV];
1192
+ if (raw === void 0 || raw === "") return DEFAULT_BUSY_TIMEOUT_MS;
1193
+ const n = Number(raw);
1194
+ return Number.isFinite(n) && n >= 0 ? Math.floor(n) : DEFAULT_BUSY_TIMEOUT_MS;
1195
+ }
1190
1196
  function nodeSqliteCtor() {
1191
1197
  if (!_nodeCtor) _nodeCtor = req2("node:sqlite").DatabaseSync;
1192
1198
  return _nodeCtor;
@@ -1208,6 +1214,7 @@ function openNodeSqlite(dbPath, opts) {
1208
1214
  enableForeignKeyConstraints: true,
1209
1215
  allowExtension: false
1210
1216
  });
1217
+ raw.exec(`PRAGMA busy_timeout = ${resolveBusyTimeoutMs()}`);
1211
1218
  let savepointSeq = 0;
1212
1219
  const wrapStmt = (sql) => {
1213
1220
  const st = raw.prepare(sql);
@@ -1291,12 +1298,14 @@ function openNodeSqlite(dbPath, opts) {
1291
1298
  return handle;
1292
1299
  }
1293
1300
  function openBetterSqlite3(dbPath, opts) {
1294
- return openDatabase(dbPath, opts);
1301
+ const db = openDatabase(dbPath, opts);
1302
+ db.exec(`PRAGMA busy_timeout = ${resolveBusyTimeoutMs()}`);
1303
+ return db;
1295
1304
  }
1296
1305
  function openDatabase2(dbPath, opts = {}) {
1297
1306
  return resolveDbEngine() === "better-sqlite3" ? openBetterSqlite3(dbPath, opts) : openNodeSqlite(dbPath, opts);
1298
1307
  }
1299
- var req2, DEFAULT_DB_ENGINE, DB_ENGINE_ENV, _nodeCtor;
1308
+ var req2, DEFAULT_DB_ENGINE, DB_ENGINE_ENV, DEFAULT_BUSY_TIMEOUT_MS, DB_BUSY_TIMEOUT_ENV, _nodeCtor;
1300
1309
  var init_db_driver = __esm({
1301
1310
  "src/db-driver.ts"() {
1302
1311
  "use strict";
@@ -1305,6 +1314,8 @@ var init_db_driver = __esm({
1305
1314
  req2 = createRequire2(import.meta.url);
1306
1315
  DEFAULT_DB_ENGINE = "node-sqlite";
1307
1316
  DB_ENGINE_ENV = "MASSU_DB_ENGINE";
1317
+ DEFAULT_BUSY_TIMEOUT_MS = 5e3;
1318
+ DB_BUSY_TIMEOUT_ENV = "MASSU_DB_BUSY_TIMEOUT_MS";
1308
1319
  _nodeCtor = null;
1309
1320
  }
1310
1321
  });
@@ -4132,3 +4143,6 @@ function readStdin() {
4132
4143
  });
4133
4144
  }
4134
4145
  main();
4146
+ export {
4147
+ MAX_FULL_DIFF_BYTES
4148
+ };
@@ -1187,6 +1187,12 @@ import { join as join2 } from "path";
1187
1187
  function resolveDbEngine() {
1188
1188
  return process.env[DB_ENGINE_ENV] === "better-sqlite3" ? "better-sqlite3" : DEFAULT_DB_ENGINE;
1189
1189
  }
1190
+ function resolveBusyTimeoutMs() {
1191
+ const raw = process.env[DB_BUSY_TIMEOUT_ENV];
1192
+ if (raw === void 0 || raw === "") return DEFAULT_BUSY_TIMEOUT_MS;
1193
+ const n = Number(raw);
1194
+ return Number.isFinite(n) && n >= 0 ? Math.floor(n) : DEFAULT_BUSY_TIMEOUT_MS;
1195
+ }
1190
1196
  function nodeSqliteCtor() {
1191
1197
  if (!_nodeCtor) _nodeCtor = req2("node:sqlite").DatabaseSync;
1192
1198
  return _nodeCtor;
@@ -1208,6 +1214,7 @@ function openNodeSqlite(dbPath, opts) {
1208
1214
  enableForeignKeyConstraints: true,
1209
1215
  allowExtension: false
1210
1216
  });
1217
+ raw.exec(`PRAGMA busy_timeout = ${resolveBusyTimeoutMs()}`);
1211
1218
  let savepointSeq = 0;
1212
1219
  const wrapStmt = (sql) => {
1213
1220
  const st = raw.prepare(sql);
@@ -1291,12 +1298,14 @@ function openNodeSqlite(dbPath, opts) {
1291
1298
  return handle;
1292
1299
  }
1293
1300
  function openBetterSqlite3(dbPath, opts) {
1294
- return openDatabase(dbPath, opts);
1301
+ const db = openDatabase(dbPath, opts);
1302
+ db.exec(`PRAGMA busy_timeout = ${resolveBusyTimeoutMs()}`);
1303
+ return db;
1295
1304
  }
1296
1305
  function openDatabase2(dbPath, opts = {}) {
1297
1306
  return resolveDbEngine() === "better-sqlite3" ? openBetterSqlite3(dbPath, opts) : openNodeSqlite(dbPath, opts);
1298
1307
  }
1299
- var req2, DEFAULT_DB_ENGINE, DB_ENGINE_ENV, _nodeCtor;
1308
+ var req2, DEFAULT_DB_ENGINE, DB_ENGINE_ENV, DEFAULT_BUSY_TIMEOUT_MS, DB_BUSY_TIMEOUT_ENV, _nodeCtor;
1300
1309
  var init_db_driver = __esm({
1301
1310
  "src/db-driver.ts"() {
1302
1311
  "use strict";
@@ -1305,6 +1314,8 @@ var init_db_driver = __esm({
1305
1314
  req2 = createRequire2(import.meta.url);
1306
1315
  DEFAULT_DB_ENGINE = "node-sqlite";
1307
1316
  DB_ENGINE_ENV = "MASSU_DB_ENGINE";
1317
+ DEFAULT_BUSY_TIMEOUT_MS = 5e3;
1318
+ DB_BUSY_TIMEOUT_ENV = "MASSU_DB_BUSY_TIMEOUT_MS";
1308
1319
  _nodeCtor = null;
1309
1320
  }
1310
1321
  });