@massu/core 2.5.5 → 2.5.6

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,39 @@ 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.6] - 2026-11-09
14
+
15
+ ### Security
16
+
17
+ - **The safety gate blocks again.** In 2.5.2 through 2.5.5 the PreToolUse gate that `massu init`
18
+ installs warned about everything it should have blocked and then let the call run: commands
19
+ that read `.env` and other secret files, piping a download into a shell, `rm -rf /`, disk
20
+ formatting and writes to `/etc/passwd`. Claude Code blocks a tool call only when the hook exits
21
+ with code 2; the gate printed "BLOCKED" and exited 0. It now exits 2 for every blocking
22
+ finding. **Upgrade, then run `massu init` again** so your settings pick up the change below.
23
+ - **A gate that fails now blocks.** `massu init` sets Claude Code's `"onFailure": "block"`
24
+ (Claude Code 2.1.295 or later) on the gate, so a gate that crashes, times out or prints invalid
25
+ output blocks the call instead of letting it through. On older Claude Code, the `massu-hook`
26
+ launcher and `hook-runner` turn a crashed gate into a block.
27
+ - **The older hook names decide too.** Settings written before 1.12.0 may still name the
28
+ `security-gate` and `pre-delete-check` hooks. They used to warn only; they now make the same
29
+ decision as the gate.
30
+ - **More secret files are recognised:** secrets data files under any name, such as
31
+ `config.secrets.yaml` or `app.secret.json`. Source files such as `secrets.ts` are still allowed.
32
+
33
+ ### Fixed
34
+
35
+ - **The gate no longer refuses good calls under load.** A slow-arriving tool call could be
36
+ denied as "Unexpected end of JSON input" and pass on retry. The gate now reads its whole
37
+ input before deciding.
38
+
39
+ ### Added
40
+
41
+ - **`massu_cost_feature` reports real data.** Each session's priced cost is split across the
42
+ registered features whose files it touched, in proportion to those touches; activity on files
43
+ that belong to no registered feature is shown as `(no registered feature)`, so the rows add up
44
+ to the sessions' total. Sessions whose cost is unknown are left out.
45
+
13
46
  ## [2.5.5] - 2026-11-09
14
47
 
15
48
  ### Fixed
package/dist/cli.js CHANGED
@@ -5893,6 +5893,42 @@ var init_hook_timeouts = __esm({
5893
5893
  }
5894
5894
  });
5895
5895
 
5896
+ // src/lib/hook-registry.ts
5897
+ function getExpectedHookFiles() {
5898
+ return REGISTERED_HOOKS.map((name) => `${name}.js`);
5899
+ }
5900
+ var REGISTERED_HOOKS, DECISION_HOOKS;
5901
+ var init_hook_registry = __esm({
5902
+ "src/lib/hook-registry.ts"() {
5903
+ "use strict";
5904
+ REGISTERED_HOOKS = [
5905
+ "auto-learning-pipeline",
5906
+ "classify-failure",
5907
+ "cost-tracker",
5908
+ "fix-detector",
5909
+ "incident-pipeline",
5910
+ "intent-suggester",
5911
+ "memory-recall",
5912
+ "post-edit-context",
5913
+ "post-tool-use",
5914
+ "pre-compact",
5915
+ // P-E-019 (1.12.0): security-gate + pre-delete-check are kept registered
5916
+ // for back-compat (operator settings.json may still reference them
5917
+ // individually), but new installs emit only `pre-tool-use-gate` —
5918
+ // the consolidated single-spawn PreToolUse hook.
5919
+ "pre-delete-check",
5920
+ "pre-tool-use-gate",
5921
+ "quality-event",
5922
+ "rule-enforcement-pipeline",
5923
+ "security-gate",
5924
+ "session-end",
5925
+ "session-start",
5926
+ "user-prompt"
5927
+ ];
5928
+ DECISION_HOOKS = ["pre-delete-check", "pre-tool-use-gate", "security-gate"];
5929
+ }
5930
+ });
5931
+
5896
5932
  // src/detect/manifest-registry.ts
5897
5933
  var manifest_registry_exports = {};
5898
5934
  __export(manifest_registry_exports, {
@@ -17102,6 +17138,22 @@ fi
17102
17138
  MASSU_VERSION="$1"
17103
17139
  shift
17104
17140
  MASSU_CLI="\${HOME}/.massu/runtime/\${MASSU_VERSION}/node_modules/@massu/core/dist/cli.js"
17141
+ # A DECIDING hook's exit code is its verdict: 0 allows, 2 denies. If the launch itself fails
17142
+ # (a runtime that cannot load exits 1), Claude Code would RUN the call, so for these hooks
17143
+ # any other status becomes a deny. Every other subcommand keeps the exec path below.
17144
+ if [ "\${1:-}" = hook-runner ]; then
17145
+ case "\${2:-}" in
17146
+ ${DECISION_HOOKS.join("|")})
17147
+ if [ -f "$MASSU_CLI" ]; then node "$MASSU_CLI" "$@"; else npx -y "@massu/core@\${MASSU_VERSION}" "$@"; fi
17148
+ st=$?
17149
+ if [ "$st" -ne 0 ] && [ "$st" -ne 2 ]; then
17150
+ echo "MASSU SECURITY GATE \u2014 BLOCKED: the $2 hook ended without a verdict (exit $st)" >&2
17151
+ exit 2
17152
+ fi
17153
+ exit "$st"
17154
+ ;;
17155
+ esac
17156
+ fi
17105
17157
  if [ -f "$MASSU_CLI" ]; then
17106
17158
  exec node "$MASSU_CLI" "$@"
17107
17159
  fi
@@ -17149,6 +17201,7 @@ function getHookTimeout(name) {
17149
17201
  function hookEntry(version, name, shim) {
17150
17202
  const entry = { type: "command", command: hookCmd(version, name, shim), timeout: getHookTimeout(name) };
17151
17203
  if (BACKGROUND_HOOKS.has(name)) entry.async = true;
17204
+ if (DECISION_HOOKS.includes(name)) entry.onFailure = "block";
17152
17205
  return entry;
17153
17206
  }
17154
17207
  function buildHooksConfig(_hooksDir, shim = resolveMassuShim()) {
@@ -17705,6 +17758,7 @@ var init_init = __esm({
17705
17758
  init_settings_local();
17706
17759
  init_memory_path();
17707
17760
  init_hook_timeouts();
17761
+ init_hook_registry();
17708
17762
  init_detect();
17709
17763
  init_drift();
17710
17764
  __filename2 = fileURLToPath3(import.meta.url);
@@ -18291,41 +18345,6 @@ var init_license = __esm({
18291
18345
  }
18292
18346
  });
18293
18347
 
18294
- // src/lib/hook-registry.ts
18295
- function getExpectedHookFiles() {
18296
- return REGISTERED_HOOKS.map((name) => `${name}.js`);
18297
- }
18298
- var REGISTERED_HOOKS;
18299
- var init_hook_registry = __esm({
18300
- "src/lib/hook-registry.ts"() {
18301
- "use strict";
18302
- REGISTERED_HOOKS = [
18303
- "auto-learning-pipeline",
18304
- "classify-failure",
18305
- "cost-tracker",
18306
- "fix-detector",
18307
- "incident-pipeline",
18308
- "intent-suggester",
18309
- "memory-recall",
18310
- "post-edit-context",
18311
- "post-tool-use",
18312
- "pre-compact",
18313
- // P-E-019 (1.12.0): security-gate + pre-delete-check are kept registered
18314
- // for back-compat (operator settings.json may still reference them
18315
- // individually), but new installs emit only `pre-tool-use-gate` —
18316
- // the consolidated single-spawn PreToolUse hook.
18317
- "pre-delete-check",
18318
- "pre-tool-use-gate",
18319
- "quality-event",
18320
- "rule-enforcement-pipeline",
18321
- "security-gate",
18322
- "session-end",
18323
- "session-start",
18324
- "user-prompt"
18325
- ];
18326
- }
18327
- });
18328
-
18329
18348
  // src/preflight.ts
18330
18349
  import { existsSync as existsSync22 } from "fs";
18331
18350
  function assertCodegraphUsable(dbPath) {
@@ -18412,6 +18431,7 @@ var init_preflight = __esm({
18412
18431
  var hook_runner_exports = {};
18413
18432
  __export(hook_runner_exports, {
18414
18433
  HOOK_NAME_TO_FILE: () => HOOK_NAME_TO_FILE,
18434
+ decisionExitCode: () => decisionExitCode,
18415
18435
  resolveHookFile: () => resolveHookFile,
18416
18436
  runHookRunner: () => runHookRunner
18417
18437
  });
@@ -18443,6 +18463,16 @@ function resolveHookFile(hookName) {
18443
18463
  `Hook file not found for "${hookName}". Searched: ${candidates.join(", ")}. This indicates a broken @massu/core install. Re-run \`npx -y @massu/core init\`.`
18444
18464
  );
18445
18465
  }
18466
+ function decisionExitCode(hookName, code) {
18467
+ if (!DECISION_HOOKS.includes(hookName) || code === 0 || code === 2) return code;
18468
+ process.stderr.write(
18469
+ `MASSU SECURITY GATE \u2014 BLOCKED
18470
+
18471
+ The ${hookName} hook ended without a verdict (exit ${code}). A gate that did not decide has not approved this call.
18472
+ `
18473
+ );
18474
+ return 2;
18475
+ }
18446
18476
  async function runHookRunner(args2) {
18447
18477
  const hookName = args2[0];
18448
18478
  if (!hookName) {
@@ -18471,11 +18501,7 @@ Recognized: ${Object.keys(HOOK_NAME_TO_FILE).join(", ")}
18471
18501
  env: { ...process.env, MASSU_HOOK_RUNTIME: "1" }
18472
18502
  });
18473
18503
  child.on("exit", (code, signal) => {
18474
- if (signal) {
18475
- resolvePromise({ exitCode: 128 });
18476
- return;
18477
- }
18478
- resolvePromise({ exitCode: code ?? 0 });
18504
+ resolvePromise({ exitCode: decisionExitCode(hookName, signal ? 128 : code ?? 0) });
18479
18505
  });
18480
18506
  child.on("error", (err) => {
18481
18507
  process.stderr.write(`massu hook-runner: failed to spawn hook "${hookName}": ${err.message}
@@ -33605,12 +33631,13 @@ function handleCostFeature(args2, db) {
33605
33631
  ORDER BY total_cost DESC
33606
33632
  `).all(`-${days} days`);
33607
33633
  if (rows.length === 0) {
33608
- return text7(`No feature cost data found in the last ${days} days. Feature costs are attributed automatically when sessions work on registered features. Ensure features are registered, then costs are tracked per-session. Try: ${p8("cost_trend")} { days: 30 } for session-level cost trends instead.`);
33634
+ return text7(`No feature cost data found in the last ${days} days. Each session's priced cost is allocated across registered features (massu_sentinel_register) by the files it touched; sessions with unknown cost are skipped. Try: ${p8("cost_trend")} { days: 30 } for session-level cost trends instead.`);
33609
33635
  }
33610
33636
  const totalCost = rows.reduce((sum, r3) => sum + r3.total_cost, 0);
33611
33637
  const lines = [
33612
33638
  `## Feature Cost Attribution (${days} days)`,
33613
- `Total: $${totalCost.toFixed(2)} across ${rows.length} features`,
33639
+ `Total: $${totalCost.toFixed(2)} across ${rows.length} feature row(s)`,
33640
+ "Each session's priced cost is ALLOCATED across the registered features whose files it touched, in proportion to those touches. It is an allocation by activity, not a measured per-feature cost. Sessions with unknown cost are not included.",
33614
33641
  "",
33615
33642
  "| Feature | Cost | Tokens | Sessions | % of Total |",
33616
33643
  "|---------|------|--------|----------|------------|"