@massu/core 2.5.5 → 2.5.7

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.
@@ -1619,6 +1619,77 @@ var init_rule_candidate_store = __esm({
1619
1619
  }
1620
1620
  });
1621
1621
 
1622
+ // src/hooks/lib/hook-failure-signal.ts
1623
+ import { appendFileSync as appendFileSync2, mkdirSync as mkdirSync5, existsSync as existsSync6 } from "fs";
1624
+ import { join as join4, dirname as dirname5 } from "path";
1625
+ function resolveFailureLogPath() {
1626
+ const explicit = process.env.MASSU_HOOK_FAILURE_LOG;
1627
+ if (explicit) return explicit;
1628
+ let dir = process.cwd();
1629
+ for (let i = 0; i < 12; i++) {
1630
+ if (existsSync6(join4(dir, ".massu")) || existsSync6(join4(dir, "massu.config.yaml"))) {
1631
+ return join4(dir, ".massu", "hook-failures.jsonl");
1632
+ }
1633
+ const parent = dirname5(dir);
1634
+ if (parent === dir) break;
1635
+ dir = parent;
1636
+ }
1637
+ return join4(process.cwd(), ".massu", "hook-failures.jsonl");
1638
+ }
1639
+ function recordHookFailure(hook, error, context) {
1640
+ const record = {
1641
+ hook,
1642
+ error: error instanceof Error ? error.message : String(error),
1643
+ stack: error instanceof Error ? error.stack?.split("\n").slice(0, 6).join("\n") : void 0,
1644
+ context,
1645
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
1646
+ };
1647
+ let wroteSomething = false;
1648
+ try {
1649
+ const path = resolveFailureLogPath();
1650
+ mkdirSync5(dirname5(path), { recursive: true });
1651
+ appendFileSync2(path, JSON.stringify(record) + "\n", "utf-8");
1652
+ wroteSomething = true;
1653
+ } catch {
1654
+ }
1655
+ try {
1656
+ process.stderr.write(
1657
+ `[massu] HOOK FAILURE in ${hook}: ${record.error}
1658
+ [massu] This is a bug in Massu, not in your code. See .massu/hook-failures.jsonl
1659
+ `
1660
+ );
1661
+ wroteSomething = true;
1662
+ } catch {
1663
+ }
1664
+ try {
1665
+ recordHookHealthRow(record);
1666
+ } catch {
1667
+ }
1668
+ return wroteSomething;
1669
+ }
1670
+ function recordHookHealthRow(record) {
1671
+ const { getMemoryDb: getMemoryDb2 } = (init_memory_db(), __toCommonJS(memory_db_exports));
1672
+ const db = getMemoryDb2();
1673
+ try {
1674
+ db.prepare(
1675
+ `INSERT INTO hook_health (hook, error, context_json, occurred_at)
1676
+ VALUES (?, ?, ?, ?)`
1677
+ ).run(
1678
+ record.hook,
1679
+ record.error,
1680
+ record.context ? JSON.stringify(record.context) : null,
1681
+ record.timestamp
1682
+ );
1683
+ } finally {
1684
+ db.close();
1685
+ }
1686
+ }
1687
+ var init_hook_failure_signal = __esm({
1688
+ "src/hooks/lib/hook-failure-signal.ts"() {
1689
+ "use strict";
1690
+ }
1691
+ });
1692
+
1622
1693
  // src/memory-embedder-tokenizer.ts
1623
1694
  import { readFileSync as readFileSync5 } from "fs";
1624
1695
  function loadVocab(vocabPath) {
@@ -1742,8 +1813,8 @@ var init_memory_embedder_tokenizer = __esm({
1742
1813
 
1743
1814
  // src/memory-embedder.ts
1744
1815
  import { fileURLToPath } from "url";
1745
- import { dirname as dirname5, join as join4 } from "path";
1746
- import { existsSync as existsSync6 } from "fs";
1816
+ import { dirname as dirname6, join as join5 } from "path";
1817
+ import { existsSync as existsSync7 } from "fs";
1747
1818
  import { createRequire as createRequire3 } from "module";
1748
1819
  function getActiveEmbedModel() {
1749
1820
  return _activeModel;
@@ -1765,21 +1836,21 @@ function embeddingsDisabled() {
1765
1836
  }
1766
1837
  function resolveModelDir() {
1767
1838
  try {
1768
- let dir = dirname5(fileURLToPath(import.meta.url));
1839
+ let dir = dirname6(fileURLToPath(import.meta.url));
1769
1840
  let root = null;
1770
1841
  for (let i = 0; i < 8; i++) {
1771
- if (existsSync6(join4(dir, "package.json"))) {
1842
+ if (existsSync7(join5(dir, "package.json"))) {
1772
1843
  root = dir;
1773
1844
  break;
1774
1845
  }
1775
- const parent = dirname5(dir);
1846
+ const parent = dirname6(dir);
1776
1847
  if (parent === dir) break;
1777
1848
  dir = parent;
1778
1849
  }
1779
1850
  if (!root) return null;
1780
1851
  for (const rel of ["dist/embedder", "assets/embedder"]) {
1781
- const candidate = join4(root, rel);
1782
- if (existsSync6(join4(candidate, "model_quantized.onnx"))) return candidate;
1852
+ const candidate = join5(root, rel);
1853
+ if (existsSync7(join5(candidate, "model_quantized.onnx"))) return candidate;
1783
1854
  }
1784
1855
  return null;
1785
1856
  } catch {
@@ -1789,8 +1860,8 @@ function resolveModelDir() {
1789
1860
  function resolveWasmDir() {
1790
1861
  try {
1791
1862
  const req3 = createRequire3(import.meta.url);
1792
- const main4 = req3.resolve(ORT_PKG);
1793
- return dirname5(main4);
1863
+ const main2 = req3.resolve(ORT_PKG);
1864
+ return dirname6(main2);
1794
1865
  } catch {
1795
1866
  return null;
1796
1867
  }
@@ -1810,8 +1881,8 @@ async function getSession() {
1810
1881
  ort.env.wasm.proxy = false;
1811
1882
  ort.env.wasm.wasmPaths = wasmDir.endsWith("/") ? wasmDir : wasmDir + "/";
1812
1883
  _ortTensor = ort.Tensor;
1813
- const modelPath = join4(modelDir, "model_quantized.onnx");
1814
- const vocabPath = join4(modelDir, "vocab.txt");
1884
+ const modelPath = join5(modelDir, "model_quantized.onnx");
1885
+ const vocabPath = join5(modelDir, "vocab.txt");
1815
1886
  _vocab = loadVocab(vocabPath);
1816
1887
  const session = await ort.InferenceSession.create(modelPath, {
1817
1888
  executionProviders: ["wasm"],
@@ -2086,6 +2157,8 @@ __export(memory_db_exports, {
2086
2157
  MEMORY_FILE_TITLE_LIKE: () => MEMORY_FILE_TITLE_LIKE,
2087
2158
  MEMORY_FILE_TITLE_PREFIX: () => MEMORY_FILE_TITLE_PREFIX,
2088
2159
  MEMORY_SCHEMA_VERSION: () => MEMORY_SCHEMA_VERSION,
2160
+ PENDING_SYNC_MAX_AGE_DAYS: () => PENDING_SYNC_MAX_AGE_DAYS,
2161
+ PENDING_SYNC_MAX_ITEMS: () => PENDING_SYNC_MAX_ITEMS,
2089
2162
  TOOL_COST_EVENTS_RETENTION_DAYS: () => TOOL_COST_EVENTS_RETENTION_DAYS,
2090
2163
  USAGE_COUNTER_ARMED_KEY: () => USAGE_COUNTER_ARMED_KEY,
2091
2164
  addConversationTurn: () => addConversationTurn,
@@ -2094,6 +2167,7 @@ __export(memory_db_exports, {
2094
2167
  addSummary: () => addSummary,
2095
2168
  addToolCallDetail: () => addToolCallDetail,
2096
2169
  addUserPrompt: () => addUserPrompt,
2170
+ allPendingSync: () => allPendingSync,
2097
2171
  appendIncidentToFailureClass: () => appendIncidentToFailureClass,
2098
2172
  armUsageCounter: () => armUsageCounter,
2099
2173
  assignImportance: () => assignImportance,
@@ -2101,6 +2175,7 @@ __export(memory_db_exports, {
2101
2175
  createSession: () => createSession,
2102
2176
  deduplicateFailedAttempt: () => deduplicateFailedAttempt,
2103
2177
  dequeuePendingSync: () => dequeuePendingSync,
2178
+ discardPendingSync: () => discardPendingSync,
2104
2179
  embedMissingObservations: () => embedMissingObservations,
2105
2180
  endSession: () => endSession,
2106
2181
  enqueueRulePromotionEvent: () => enqueueRulePromotionEvent,
@@ -2151,8 +2226,8 @@ __export(memory_db_exports, {
2151
2226
  upsertObservationEmbedding: () => upsertObservationEmbedding,
2152
2227
  usageWarmupElapsed: () => usageWarmupElapsed
2153
2228
  });
2154
- import { dirname as dirname6, basename as basename2 } from "path";
2155
- import { existsSync as existsSync7, mkdirSync as mkdirSync5 } from "fs";
2229
+ import { dirname as dirname7, basename as basename2 } from "path";
2230
+ import { existsSync as existsSync8, mkdirSync as mkdirSync6 } from "fs";
2156
2231
  function sanitizeFts5Query(raw) {
2157
2232
  const trimmed = raw.trim();
2158
2233
  if (!trimmed) return '""';
@@ -2168,11 +2243,11 @@ function sanitizeFts5QueryOr(raw) {
2168
2243
  }
2169
2244
  function getMemoryDb() {
2170
2245
  const dbPath = getResolvedPaths().memoryDbPath;
2171
- const dir = dirname6(dbPath);
2172
- if (!existsSync7(dir)) {
2173
- mkdirSync5(dir, { recursive: true });
2246
+ const dir = dirname7(dbPath);
2247
+ if (!existsSync8(dir)) {
2248
+ mkdirSync6(dir, { recursive: true });
2174
2249
  }
2175
- const preExisting = existsSync7(dbPath);
2250
+ const preExisting = existsSync8(dbPath);
2176
2251
  const db = openDatabase2(dbPath);
2177
2252
  db.pragma("journal_mode = WAL");
2178
2253
  db.pragma("foreign_keys = ON");
@@ -3083,6 +3158,35 @@ function initMemorySchema(db) {
3083
3158
  }
3084
3159
  function enqueueSyncPayload(db, payload) {
3085
3160
  db.prepare("INSERT INTO pending_sync (payload) VALUES (?)").run(payload);
3161
+ const overflow = db.prepare(
3162
+ `SELECT id, retry_count, last_error, payload FROM pending_sync
3163
+ WHERE created_at < datetime('now', ?)
3164
+ OR id NOT IN (SELECT id FROM pending_sync ORDER BY created_at DESC, id DESC LIMIT ?)
3165
+ LIMIT 10000`
3166
+ ).all(`-${PENDING_SYNC_MAX_AGE_DAYS} days`, PENDING_SYNC_MAX_ITEMS);
3167
+ if (overflow.length > 0) {
3168
+ discardPendingSync(db, overflow, `over the queue limit (${PENDING_SYNC_MAX_ITEMS} items / ${PENDING_SYNC_MAX_AGE_DAYS} days)`);
3169
+ }
3170
+ }
3171
+ function discardPendingSync(db, rows, why) {
3172
+ if (rows.length === 0) return 0;
3173
+ const rescued = rescueLearningFromStalePayloads(db, rows);
3174
+ for (let i = 0; i < rows.length; i += 500) {
3175
+ const ids = rows.slice(i, i + 500).map((r) => r.id);
3176
+ db.prepare(`DELETE FROM pending_sync WHERE id IN (${ids.map(() => "?").join(",")})`).run(...ids);
3177
+ }
3178
+ if (rescued > 0) {
3179
+ process.stderr.write(
3180
+ `[massu] RESCUED ${rescued} learned rule(s) from ${rows.length} discarded payload(s) \u2014 they were re-queued for delivery, NOT discarded.
3181
+ `
3182
+ );
3183
+ }
3184
+ process.stderr.write(`[massu] cloud sync: discarded ${rows.length} queued upload(s): ${why}.
3185
+ `);
3186
+ return rows.length;
3187
+ }
3188
+ function allPendingSync(db) {
3189
+ return db.prepare("SELECT id, retry_count, last_error, payload FROM pending_sync").all();
3086
3190
  }
3087
3191
  function getMemoryMeta(db, key) {
3088
3192
  const row = db.prepare("SELECT value FROM memory_meta WHERE key = ?").get(key);
@@ -3285,7 +3389,9 @@ function dequeuePendingSync(db, limit = 10) {
3285
3389
  }
3286
3390
  }
3287
3391
  return db.prepare(
3288
- "SELECT id, payload, retry_count FROM pending_sync ORDER BY created_at ASC LIMIT ?"
3392
+ // Least-retried first: oldest-first retried the same 10 items until they were discarded
3393
+ // while every newer item waited at retry 0 (2026-11-09: 43,492 never attempted).
3394
+ "SELECT id, payload, retry_count FROM pending_sync ORDER BY retry_count ASC, created_at ASC LIMIT ?"
3289
3395
  ).all(limit);
3290
3396
  }
3291
3397
  function removePendingSync(db, id) {
@@ -3958,7 +4064,7 @@ function scoreFailureClasses(db, matchText, filePath, promptContext, weights) {
3958
4064
  }
3959
4065
  return bestMatch;
3960
4066
  }
3961
- var MEMORY_SCHEMA_VERSION, TOOL_COST_EVENTS_RETENTION_DAYS, MAX_DRAFT_TEXT_LEN, FUNNEL_EVENT_OUTBOX_CAP, _temporalColCache, CONSOLIDATION_LESSON_EVIDENCE, MEMORY_FILE_TITLE_PREFIX, MEMORY_FILE_TITLE_LIKE, USAGE_COUNTER_ARMED_KEY;
4067
+ var MEMORY_SCHEMA_VERSION, TOOL_COST_EVENTS_RETENTION_DAYS, PENDING_SYNC_MAX_ITEMS, PENDING_SYNC_MAX_AGE_DAYS, MAX_DRAFT_TEXT_LEN, FUNNEL_EVENT_OUTBOX_CAP, _temporalColCache, CONSOLIDATION_LESSON_EVIDENCE, MEMORY_FILE_TITLE_PREFIX, MEMORY_FILE_TITLE_LIKE, USAGE_COUNTER_ARMED_KEY;
3962
4068
  var init_memory_db = __esm({
3963
4069
  "src/memory-db.ts"() {
3964
4070
  "use strict";
@@ -3973,6 +4079,8 @@ var init_memory_db = __esm({
3973
4079
  init_memory_embed_sweep();
3974
4080
  MEMORY_SCHEMA_VERSION = 2;
3975
4081
  TOOL_COST_EVENTS_RETENTION_DAYS = 90;
4082
+ PENDING_SYNC_MAX_ITEMS = 500;
4083
+ PENDING_SYNC_MAX_AGE_DAYS = 30;
3976
4084
  MAX_DRAFT_TEXT_LEN = 16384;
3977
4085
  FUNNEL_EVENT_OUTBOX_CAP = 2e4;
3978
4086
  _temporalColCache = /* @__PURE__ */ new WeakMap();
@@ -3983,129 +4091,7 @@ var init_memory_db = __esm({
3983
4091
  }
3984
4092
  });
3985
4093
 
3986
- // src/hooks/lib/hook-failure-signal.ts
3987
- import { appendFileSync as appendFileSync2, mkdirSync as mkdirSync6, existsSync as existsSync8 } from "fs";
3988
- import { join as join5, dirname as dirname7 } from "path";
3989
- function resolveFailureLogPath() {
3990
- const explicit = process.env.MASSU_HOOK_FAILURE_LOG;
3991
- if (explicit) return explicit;
3992
- let dir = process.cwd();
3993
- for (let i = 0; i < 12; i++) {
3994
- if (existsSync8(join5(dir, ".massu")) || existsSync8(join5(dir, "massu.config.yaml"))) {
3995
- return join5(dir, ".massu", "hook-failures.jsonl");
3996
- }
3997
- const parent = dirname7(dir);
3998
- if (parent === dir) break;
3999
- dir = parent;
4000
- }
4001
- return join5(process.cwd(), ".massu", "hook-failures.jsonl");
4002
- }
4003
- function recordHookFailure(hook, error, context) {
4004
- const record = {
4005
- hook,
4006
- error: error instanceof Error ? error.message : String(error),
4007
- stack: error instanceof Error ? error.stack?.split("\n").slice(0, 6).join("\n") : void 0,
4008
- context,
4009
- timestamp: (/* @__PURE__ */ new Date()).toISOString()
4010
- };
4011
- let wroteSomething = false;
4012
- try {
4013
- const path = resolveFailureLogPath();
4014
- mkdirSync6(dirname7(path), { recursive: true });
4015
- appendFileSync2(path, JSON.stringify(record) + "\n", "utf-8");
4016
- wroteSomething = true;
4017
- } catch {
4018
- }
4019
- try {
4020
- process.stderr.write(
4021
- `[massu] HOOK FAILURE in ${hook}: ${record.error}
4022
- [massu] This is a bug in Massu, not in your code. See .massu/hook-failures.jsonl
4023
- `
4024
- );
4025
- wroteSomething = true;
4026
- } catch {
4027
- }
4028
- try {
4029
- recordHookHealthRow(record);
4030
- } catch {
4031
- }
4032
- return wroteSomething;
4033
- }
4034
- function recordHookHealthRow(record) {
4035
- const { getMemoryDb: getMemoryDb2 } = (init_memory_db(), __toCommonJS(memory_db_exports));
4036
- const db = getMemoryDb2();
4037
- try {
4038
- db.prepare(
4039
- `INSERT INTO hook_health (hook, error, context_json, occurred_at)
4040
- VALUES (?, ?, ?, ?)`
4041
- ).run(
4042
- record.hook,
4043
- record.error,
4044
- record.context ? JSON.stringify(record.context) : null,
4045
- record.timestamp
4046
- );
4047
- } finally {
4048
- db.close();
4049
- }
4050
- }
4051
- var init_hook_failure_signal = __esm({
4052
- "src/hooks/lib/hook-failure-signal.ts"() {
4053
- "use strict";
4054
- }
4055
- });
4056
-
4057
- // src/hooks/lib/write-hook-message.ts
4058
- var HOOK_EVENTS = [
4059
- "UserPromptSubmit",
4060
- "UserPromptExpansion",
4061
- "SessionStart",
4062
- "PreToolUse",
4063
- "PostToolUse",
4064
- "Stop",
4065
- "SubagentStop",
4066
- "PreCompact"
4067
- ];
4068
- var ADDITIONAL_CONTEXT_MAX_CHARS = 1e4;
4069
- function isHookEvent(value) {
4070
- return typeof value === "string" && HOOK_EVENTS.includes(value);
4071
- }
4072
- function writeHookContext(hookEventName, message) {
4073
- if (!isHookEvent(hookEventName)) {
4074
- process.stderr.write(
4075
- `[massu] writeHookContext: refusing to emit \u2014 unrecognised hook event ${JSON.stringify(hookEventName)}. Known: ${HOOK_EVENTS.join(", ")}.
4076
- `
4077
- );
4078
- return;
4079
- }
4080
- if (typeof message !== "string" || message.length === 0) return;
4081
- let additionalContext = message;
4082
- if (additionalContext.length > ADDITIONAL_CONTEXT_MAX_CHARS) {
4083
- const marker = `
4084
- [massu] \u2026truncated at ${ADDITIONAL_CONTEXT_MAX_CHARS} chars (spec cap).`;
4085
- additionalContext = additionalContext.slice(0, ADDITIONAL_CONTEXT_MAX_CHARS - marker.length) + marker;
4086
- }
4087
- process.stdout.write(
4088
- JSON.stringify({ hookSpecificOutput: { hookEventName, additionalContext } }) + "\n"
4089
- );
4090
- }
4091
-
4092
- // src/hooks/security-gate.ts
4093
- init_hook_failure_signal();
4094
-
4095
- // src/hooks/lib/is-direct-invocation.ts
4096
- import { pathToFileURL } from "url";
4097
- function isDirectInvocation(moduleUrl) {
4098
- const entry = process.argv[1];
4099
- if (!entry) return false;
4100
- try {
4101
- return moduleUrl === pathToFileURL(entry).href;
4102
- } catch {
4103
- return false;
4104
- }
4105
- }
4106
-
4107
- // src/hooks/security-gate.ts
4108
- var HOOK_EVENT = "PreToolUse";
4094
+ // src/hooks/lib/security-gate-checks.ts
4109
4095
  var DANGEROUS_BASH_PATTERNS = [
4110
4096
  // BLOCK — destructive and irreversible, or arbitrary remote code execution.
4111
4097
  { pattern: /rm\s+-[a-z]*r[a-z]*f[a-z]*\s+\/(?:\s|$)/, label: "rm -rf /", severity: "block" },
@@ -4157,6 +4143,7 @@ var SECRET_EXACT_BASENAMES = [
4157
4143
  ];
4158
4144
  var CREDENTIALS_PATH_SUFFIX = "/credentials";
4159
4145
  var DOTENV = ".env";
4146
+ var SECRET_DATA_EXTENSIONS = ["yaml", "yml", "json", "toml", "ini", "env", "conf", "properties", "txt"];
4160
4147
  function looksLikeCode(token) {
4161
4148
  return /[()[\]{};=`$<>|&*?!"']/.test(token) || /\s/.test(token);
4162
4149
  }
@@ -4175,8 +4162,13 @@ function namesSecretFile(token) {
4175
4162
  return DOTENV_TEMPLATE_SUFFIXES.includes(suffix) ? null : `${base} file`;
4176
4163
  }
4177
4164
  if (base.endsWith(DOTENV) && clean.includes("/")) return `${base} file`;
4178
- for (const ext of SECRET_BASENAME_EXTENSIONS) {
4179
- if (base.endsWith(`.${ext}`)) return `${base} file`;
4165
+ for (const ext2 of SECRET_BASENAME_EXTENSIONS) {
4166
+ if (base.endsWith(`.${ext2}`)) return `${base} file`;
4167
+ }
4168
+ const parts = base.toLowerCase().split(".");
4169
+ const ext = parts.length > 1 ? parts[parts.length - 1] : "";
4170
+ if (SECRET_DATA_EXTENSIONS.includes(ext) && parts.slice(0, -1).some((p) => p === "secret" || p === "secrets")) {
4171
+ return `${base} file`;
4180
4172
  }
4181
4173
  return null;
4182
4174
  }
@@ -4335,38 +4327,8 @@ Review carefully before proceeding.`
4335
4327
  }
4336
4328
  return findings;
4337
4329
  }
4338
- function runSecurityGateChecks(hookInput) {
4339
- return runSecurityGateFindings(hookInput).map((f) => f.message);
4340
- }
4341
- async function main() {
4342
- try {
4343
- const input = await readStdin();
4344
- const hookInput = JSON.parse(input);
4345
- const messages = runSecurityGateChecks(hookInput);
4346
- for (const msg of messages) writeHookContext(HOOK_EVENT, msg);
4347
- } catch (err) {
4348
- recordHookFailure("security-gate", err);
4349
- process.stderr.write("MASSU SECURITY GATE \u2014 BLOCKED (gate could not evaluate this call)\n");
4350
- process.exit(2);
4351
- }
4352
- process.exit(0);
4353
- }
4354
- function readStdin() {
4355
- return new Promise((resolve5) => {
4356
- let data = "";
4357
- process.stdin.setEncoding("utf-8");
4358
- process.stdin.on("data", (chunk) => {
4359
- data += chunk;
4360
- });
4361
- process.stdin.on("end", () => resolve5(data));
4362
- setTimeout(() => resolve5(data), 400);
4363
- });
4364
- }
4365
- if (isDirectInvocation(import.meta.url)) {
4366
- main();
4367
- }
4368
4330
 
4369
- // src/hooks/pre-delete-check.ts
4331
+ // src/hooks/lib/pre-delete-checks.ts
4370
4332
  init_db_driver();
4371
4333
  import { existsSync as existsSync9 } from "fs";
4372
4334
 
@@ -4462,7 +4424,7 @@ function getFeatureImpact(db, filePaths) {
4462
4424
  };
4463
4425
  }
4464
4426
 
4465
- // src/hooks/pre-delete-check.ts
4427
+ // src/hooks/lib/pre-delete-checks.ts
4466
4428
  init_config();
4467
4429
 
4468
4430
  // src/lib/source-layout.ts
@@ -4512,9 +4474,7 @@ function isUnderSourceDir(path, layout = getSourceLayout()) {
4512
4474
  return layout.sourceDirs.some((d) => path.startsWith(`${d}/`));
4513
4475
  }
4514
4476
 
4515
- // src/hooks/pre-delete-check.ts
4516
- init_hook_failure_signal();
4517
- var HOOK_EVENT2 = "PreToolUse";
4477
+ // src/hooks/lib/pre-delete-checks.ts
4518
4478
  var PROJECT_ROOT2 = getProjectRoot();
4519
4479
  var KNOWLEDGE_PROTECTED_FILES = [
4520
4480
  "knowledge-db.ts",
@@ -4653,32 +4613,40 @@ function runPreDeleteChecks(hookInput) {
4653
4613
  }
4654
4614
  return messages;
4655
4615
  }
4656
- async function main2() {
4657
- try {
4658
- const input = await readStdin2();
4659
- const hookInput = JSON.parse(input);
4660
- const messages = runPreDeleteChecks(hookInput);
4661
- for (const msg of messages) writeHookContext(HOOK_EVENT2, msg);
4662
- } catch (err) {
4663
- recordHookFailure("pre-delete-check", err);
4664
- process.stderr.write("MASSU SECURITY GATE \u2014 BLOCKED (gate could not evaluate this call)\n");
4665
- process.exit(2);
4666
- }
4667
- process.exit(0);
4668
- }
4669
- function readStdin2() {
4670
- return new Promise((resolve5) => {
4671
- let data = "";
4672
- process.stdin.setEncoding("utf-8");
4673
- process.stdin.on("data", (chunk) => {
4674
- data += chunk;
4675
- });
4676
- process.stdin.on("end", () => resolve5(data));
4677
- setTimeout(() => resolve5(data), 400);
4678
- });
4616
+
4617
+ // src/hooks/lib/write-hook-message.ts
4618
+ var HOOK_EVENTS = [
4619
+ "UserPromptSubmit",
4620
+ "UserPromptExpansion",
4621
+ "SessionStart",
4622
+ "PreToolUse",
4623
+ "PostToolUse",
4624
+ "Stop",
4625
+ "SubagentStop",
4626
+ "PreCompact"
4627
+ ];
4628
+ var ADDITIONAL_CONTEXT_MAX_CHARS = 1e4;
4629
+ function isHookEvent(value) {
4630
+ return typeof value === "string" && HOOK_EVENTS.includes(value);
4679
4631
  }
4680
- if (isDirectInvocation(import.meta.url)) {
4681
- main2();
4632
+ function writeHookContext(hookEventName, message) {
4633
+ if (!isHookEvent(hookEventName)) {
4634
+ process.stderr.write(
4635
+ `[massu] writeHookContext: refusing to emit \u2014 unrecognised hook event ${JSON.stringify(hookEventName)}. Known: ${HOOK_EVENTS.join(", ")}.
4636
+ `
4637
+ );
4638
+ return;
4639
+ }
4640
+ if (typeof message !== "string" || message.length === 0) return;
4641
+ let additionalContext = message;
4642
+ if (additionalContext.length > ADDITIONAL_CONTEXT_MAX_CHARS) {
4643
+ const marker = `
4644
+ [massu] \u2026truncated at ${ADDITIONAL_CONTEXT_MAX_CHARS} chars (spec cap).`;
4645
+ additionalContext = additionalContext.slice(0, ADDITIONAL_CONTEXT_MAX_CHARS - marker.length) + marker;
4646
+ }
4647
+ process.stdout.write(
4648
+ JSON.stringify({ hookSpecificOutput: { hookEventName, additionalContext } }) + "\n"
4649
+ );
4682
4650
  }
4683
4651
 
4684
4652
  // src/hooks/lib/read-stdin.ts
@@ -4716,9 +4684,8 @@ function readStdinToEof(deadlineMs = 4e3) {
4716
4684
  });
4717
4685
  }
4718
4686
 
4719
- // src/hooks/pre-tool-use-gate.ts
4687
+ // src/hooks/lib/pre-tool-use-decision.ts
4720
4688
  init_hook_failure_signal();
4721
- var HOOK_EVENT3 = "PreToolUse";
4722
4689
  var EXIT_DENY = 2;
4723
4690
  var EXIT_ALLOW = 0;
4724
4691
  function deny(reason, detail) {
@@ -4734,19 +4701,22 @@ The gate denies by default when it cannot verify a command is safe.
4734
4701
  );
4735
4702
  process.exit(EXIT_DENY);
4736
4703
  }
4737
- async function main3() {
4704
+ async function runPreToolUseDecision(hookName, event) {
4738
4705
  let hookInput;
4706
+ let input = "";
4739
4707
  try {
4740
- const input = await readStdinToEof();
4708
+ input = await readStdinToEof();
4741
4709
  if (!input.trim()) {
4742
- recordHookFailure("pre-tool-use-gate", new Error("empty stdin payload"));
4743
- deny(
4744
- "The gate received an EMPTY payload and could not determine what tool call was requested."
4745
- );
4710
+ recordHookFailure(hookName, new Error("empty stdin payload"));
4711
+ deny("The gate received an EMPTY payload and could not determine what tool call was requested.");
4746
4712
  }
4747
4713
  hookInput = JSON.parse(input);
4748
4714
  } catch (err) {
4749
- recordHookFailure("pre-tool-use-gate", err, { phase: "read-parse" });
4715
+ recordHookFailure(hookName, err, {
4716
+ phase: "read-parse",
4717
+ bytesRead: input.length,
4718
+ endsWithBrace: input.trimEnd().endsWith("}")
4719
+ });
4750
4720
  deny(
4751
4721
  "The gate could not READ or PARSE the tool-call payload, so it cannot know what it was being asked to approve.",
4752
4722
  err instanceof Error ? `Cause: ${err.message}` : void 0
@@ -4755,26 +4725,70 @@ async function main3() {
4755
4725
  let findings;
4756
4726
  try {
4757
4727
  const security = runSecurityGateFindings(hookInput);
4758
- const deletes = runPreDeleteChecks(
4759
- hookInput
4760
- ).map((message) => ({ severity: "warn", message }));
4728
+ const deletes = runPreDeleteChecks(hookInput).map(
4729
+ (message) => ({ severity: "warn", message })
4730
+ );
4761
4731
  findings = [...security, ...deletes];
4762
4732
  } catch (err) {
4763
- recordHookFailure("pre-tool-use-gate", err, { phase: "evaluate" });
4733
+ recordHookFailure(hookName, err, { phase: "evaluate" });
4764
4734
  deny(
4765
4735
  "A security check CRASHED while evaluating this tool call. A check that did not run has not passed.",
4766
4736
  err instanceof Error ? `Cause: ${err.message}` : void 0
4767
4737
  );
4768
4738
  }
4769
- const blocking = findings.filter((f) => f.severity === "block");
4770
4739
  for (const f of findings.filter((x) => x.severity === "warn")) {
4771
- writeHookContext(HOOK_EVENT3, f.message);
4772
- }
4773
- if (blocking.length > 0) {
4774
- deny(blocking.map((f) => f.message).join("\n\n"));
4740
+ writeHookContext(event, f.message);
4775
4741
  }
4742
+ const blocking = findings.filter((f) => f.severity === "block");
4743
+ if (blocking.length > 0) deny(blocking.map((f) => f.message).join("\n\n"));
4776
4744
  process.exit(EXIT_ALLOW);
4777
4745
  }
4746
+
4747
+ // src/lib/safe-write.ts
4748
+ import {
4749
+ writeFileSync as writeFileSync3,
4750
+ renameSync,
4751
+ openSync as openSync2,
4752
+ fsyncSync as fsyncSync2,
4753
+ closeSync as closeSync2,
4754
+ existsSync as existsSync10,
4755
+ realpathSync,
4756
+ unlinkSync as unlinkSync2,
4757
+ accessSync as accessSync2,
4758
+ statSync as statSync2,
4759
+ chmodSync as chmodSync3,
4760
+ constants
4761
+ } from "fs";
4762
+ function isSameRealPath(a, b) {
4763
+ try {
4764
+ return realpathSync(a) === realpathSync(b);
4765
+ } catch {
4766
+ return false;
4767
+ }
4768
+ }
4769
+
4770
+ // src/hooks/lib/is-direct-invocation.ts
4771
+ import { fileURLToPath as fileURLToPath2, pathToFileURL } from "url";
4772
+ function isDirectInvocation(moduleUrl) {
4773
+ const entry = process.argv[1];
4774
+ if (!entry) return false;
4775
+ try {
4776
+ if (moduleUrl === pathToFileURL(entry).href) return true;
4777
+ } catch {
4778
+ return false;
4779
+ }
4780
+ try {
4781
+ return isSameRealPath(fileURLToPath2(moduleUrl), entry);
4782
+ } catch {
4783
+ return false;
4784
+ }
4785
+ }
4786
+
4787
+ // src/hooks/pre-tool-use-gate.ts
4788
+ var HOOK_EVENT = "PreToolUse";
4789
+ async function main() {
4790
+ await runPreToolUseDecision("pre-tool-use-gate", HOOK_EVENT);
4791
+ }
4778
4792
  if (isDirectInvocation(import.meta.url)) {
4779
- main3();
4793
+ main();
4780
4794
  }