@massu/core 2.5.0 → 2.5.2

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.
@@ -1167,6 +1167,13 @@ var init_config = __esm({
1167
1167
  });
1168
1168
  LanguageFrameworkEntrySchema = z2.object({
1169
1169
  framework: z2.string().optional(),
1170
+ // `source_dirs` is emitted by `init` (commands/init.ts:417) and existence-checked
1171
+ // by config validation (init.ts:868-876), but until 2026-08-14 it survived only via
1172
+ // `.passthrough()` — declared in the file, untyped in the schema. `lib/source-layout.ts`
1173
+ // derives the index builders' candidate set from it, so it is typed here rather than
1174
+ // read as an unknown: a consumer that has to shape-check its own SoT is one branch away
1175
+ // from silently contributing `undefined` to a path predicate.
1176
+ source_dirs: z2.array(z2.string()).optional(),
1170
1177
  test_framework: z2.string().optional(),
1171
1178
  test: z2.string().optional(),
1172
1179
  runtime: z2.string().optional(),
@@ -1339,6 +1346,16 @@ var init_memory_vector = __esm({
1339
1346
  }
1340
1347
  });
1341
1348
 
1349
+ // src/lib/timestamps.ts
1350
+ function nowIso() {
1351
+ return (/* @__PURE__ */ new Date()).toISOString();
1352
+ }
1353
+ var init_timestamps = __esm({
1354
+ "src/lib/timestamps.ts"() {
1355
+ "use strict";
1356
+ }
1357
+ });
1358
+
1342
1359
  // src/db-backup.ts
1343
1360
  import { existsSync as existsSync5, mkdirSync as mkdirSync4, readdirSync, statSync, unlinkSync, copyFileSync, rmSync as rmSync4 } from "fs";
1344
1361
  import { resolve as resolve3, join as join3, basename, dirname as dirname4 } from "path";
@@ -3599,8 +3616,8 @@ function linkSessionToTask(db, sessionId, taskId) {
3599
3616
  }
3600
3617
  function addConversationTurn(db, sessionId, turnNumber, userPrompt, assistantResponse, toolCallsJson, toolCallCount, promptTokens, responseTokens) {
3601
3618
  const result = db.prepare(`
3602
- INSERT INTO conversation_turns (session_id, turn_number, user_prompt, assistant_response, tool_calls_json, tool_call_count, prompt_tokens, response_tokens)
3603
- VALUES (?, ?, ?, ?, ?, ?, ?, ?)
3619
+ INSERT INTO conversation_turns (session_id, turn_number, user_prompt, assistant_response, tool_calls_json, tool_call_count, prompt_tokens, response_tokens, created_at)
3620
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
3604
3621
  `).run(
3605
3622
  sessionId,
3606
3623
  turnNumber,
@@ -3609,14 +3626,15 @@ function addConversationTurn(db, sessionId, turnNumber, userPrompt, assistantRes
3609
3626
  toolCallsJson,
3610
3627
  toolCallCount,
3611
3628
  promptTokens,
3612
- responseTokens
3629
+ responseTokens,
3630
+ nowIso()
3613
3631
  );
3614
3632
  return Number(result.lastInsertRowid);
3615
3633
  }
3616
3634
  function addToolCallDetail(db, sessionId, turnNumber, toolName, inputSummary, inputSize, outputSize, success, filesInvolved) {
3617
3635
  db.prepare(`
3618
- INSERT INTO tool_call_details (session_id, turn_number, tool_name, tool_input_summary, tool_input_size, tool_output_size, tool_success, files_involved)
3619
- VALUES (?, ?, ?, ?, ?, ?, ?, ?)
3636
+ INSERT INTO tool_call_details (session_id, turn_number, tool_name, tool_input_summary, tool_input_size, tool_output_size, tool_success, files_involved, created_at)
3637
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
3620
3638
  `).run(
3621
3639
  sessionId,
3622
3640
  turnNumber,
@@ -3625,7 +3643,8 @@ function addToolCallDetail(db, sessionId, turnNumber, toolName, inputSummary, in
3625
3643
  inputSize,
3626
3644
  outputSize,
3627
3645
  success ? 1 : 0,
3628
- filesInvolved ? JSON.stringify(filesInvolved) : null
3646
+ filesInvolved ? JSON.stringify(filesInvolved) : null,
3647
+ nowIso()
3629
3648
  );
3630
3649
  }
3631
3650
  function getLastProcessedLine(db, sessionId) {
@@ -3884,6 +3903,7 @@ var init_memory_db = __esm({
3884
3903
  init_db_driver();
3885
3904
  init_config();
3886
3905
  init_memory_vector();
3906
+ init_timestamps();
3887
3907
  init_db_backup();
3888
3908
  init_rule_delivery();
3889
3909
  init_rule_candidate_store();
@@ -3973,12 +3993,57 @@ var init_hook_failure_signal = __esm({
3973
3993
  });
3974
3994
 
3975
3995
  // src/hooks/lib/write-hook-message.ts
3976
- function writeHookMessage(message) {
3977
- process.stdout.write(JSON.stringify({ message }) + "\n");
3996
+ var HOOK_EVENTS = [
3997
+ "UserPromptSubmit",
3998
+ "UserPromptExpansion",
3999
+ "SessionStart",
4000
+ "PreToolUse",
4001
+ "PostToolUse",
4002
+ "Stop",
4003
+ "SubagentStop",
4004
+ "PreCompact"
4005
+ ];
4006
+ var ADDITIONAL_CONTEXT_MAX_CHARS = 1e4;
4007
+ function isHookEvent(value) {
4008
+ return typeof value === "string" && HOOK_EVENTS.includes(value);
4009
+ }
4010
+ function writeHookContext(hookEventName, message) {
4011
+ if (!isHookEvent(hookEventName)) {
4012
+ process.stderr.write(
4013
+ `[massu] writeHookContext: refusing to emit \u2014 unrecognised hook event ${JSON.stringify(hookEventName)}. Known: ${HOOK_EVENTS.join(", ")}.
4014
+ `
4015
+ );
4016
+ return;
4017
+ }
4018
+ if (typeof message !== "string" || message.length === 0) return;
4019
+ let additionalContext = message;
4020
+ if (additionalContext.length > ADDITIONAL_CONTEXT_MAX_CHARS) {
4021
+ const marker = `
4022
+ [massu] \u2026truncated at ${ADDITIONAL_CONTEXT_MAX_CHARS} chars (spec cap).`;
4023
+ additionalContext = additionalContext.slice(0, ADDITIONAL_CONTEXT_MAX_CHARS - marker.length) + marker;
4024
+ }
4025
+ process.stdout.write(
4026
+ JSON.stringify({ hookSpecificOutput: { hookEventName, additionalContext } }) + "\n"
4027
+ );
3978
4028
  }
3979
4029
 
3980
4030
  // src/hooks/security-gate.ts
3981
4031
  init_hook_failure_signal();
4032
+
4033
+ // src/hooks/lib/is-direct-invocation.ts
4034
+ import { pathToFileURL } from "url";
4035
+ function isDirectInvocation(moduleUrl) {
4036
+ const entry = process.argv[1];
4037
+ if (!entry) return false;
4038
+ try {
4039
+ return moduleUrl === pathToFileURL(entry).href;
4040
+ } catch {
4041
+ return false;
4042
+ }
4043
+ }
4044
+
4045
+ // src/hooks/security-gate.ts
4046
+ var HOOK_EVENT = "PreToolUse";
3982
4047
  var DANGEROUS_BASH_PATTERNS = [
3983
4048
  // BLOCK — destructive and irreversible, or arbitrary remote code execution.
3984
4049
  { pattern: /rm\s+-[a-z]*r[a-z]*f[a-z]*\s+\/(?:\s|$)/, label: "rm -rf /", severity: "block" },
@@ -4098,7 +4163,7 @@ async function main() {
4098
4163
  const input = await readStdin();
4099
4164
  const hookInput = JSON.parse(input);
4100
4165
  const messages = runSecurityGateChecks(hookInput);
4101
- for (const msg of messages) writeHookMessage(msg);
4166
+ for (const msg of messages) writeHookContext(HOOK_EVENT, msg);
4102
4167
  } catch (err) {
4103
4168
  recordHookFailure("security-gate", err);
4104
4169
  process.stderr.write("MASSU SECURITY GATE \u2014 BLOCKED (gate could not evaluate this call)\n");
@@ -4117,7 +4182,7 @@ function readStdin() {
4117
4182
  setTimeout(() => resolve5(data), 400);
4118
4183
  });
4119
4184
  }
4120
- if (process.argv[1]?.endsWith("security-gate.js") || process.argv[1]?.endsWith("security-gate")) {
4185
+ if (isDirectInvocation(import.meta.url)) {
4121
4186
  main();
4122
4187
  }
4123
4188
 
@@ -4219,7 +4284,57 @@ function getFeatureImpact(db, filePaths) {
4219
4284
 
4220
4285
  // src/hooks/pre-delete-check.ts
4221
4286
  init_config();
4287
+
4288
+ // src/lib/source-layout.ts
4289
+ init_config();
4290
+ function normalizeDir(raw) {
4291
+ if (typeof raw !== "string") return null;
4292
+ const posix = raw.replace(/\\/g, "/");
4293
+ if (posix.startsWith("/") || /^[A-Za-z]:\//.test(posix)) return null;
4294
+ const parts = posix.split("/").filter((p) => p !== "" && p !== ".");
4295
+ if (parts.some((p) => p === "..")) return null;
4296
+ if (parts.length === 0) return ".";
4297
+ return parts.join("/");
4298
+ }
4299
+ function declaredLanguageSourceDirs() {
4300
+ const languages = getConfig().framework.languages;
4301
+ if (!languages) return [];
4302
+ const out = [];
4303
+ for (const entry of Object.values(languages)) {
4304
+ if (Array.isArray(entry.source_dirs)) out.push(...entry.source_dirs);
4305
+ }
4306
+ return out;
4307
+ }
4308
+ function getSourceLayout() {
4309
+ const config = getConfig();
4310
+ const paths = config.paths;
4311
+ const declared = [
4312
+ paths.source,
4313
+ ...paths.monorepo_roots ?? [],
4314
+ ...declaredLanguageSourceDirs()
4315
+ ];
4316
+ const normalized = declared.map(normalizeDir).filter((d) => d !== null);
4317
+ const includesRoot = normalized.includes(".");
4318
+ const sourceDirs = includesRoot ? [] : [...new Set(normalized)].sort();
4319
+ const source = normalizeDir(paths.source) ?? ".";
4320
+ const under = (child) => source === "." ? child : `${source}/${child}`;
4321
+ return {
4322
+ sourceDirs,
4323
+ includesRoot,
4324
+ extensions: getResolvedPaths().extensions,
4325
+ pagesDir: normalizeDir(paths.pages ?? under("app")) ?? under("app"),
4326
+ componentsDir: normalizeDir(paths.components ?? under("components")) ?? under("components")
4327
+ };
4328
+ }
4329
+ function isUnderSourceDir(path, layout = getSourceLayout()) {
4330
+ if (normalizeDir(path) === null) return false;
4331
+ if (layout.includesRoot) return true;
4332
+ return layout.sourceDirs.some((d) => path.startsWith(`${d}/`));
4333
+ }
4334
+
4335
+ // src/hooks/pre-delete-check.ts
4222
4336
  init_hook_failure_signal();
4337
+ var HOOK_EVENT2 = "PreToolUse";
4223
4338
  var PROJECT_ROOT2 = getProjectRoot();
4224
4339
  var KNOWLEDGE_PROTECTED_FILES = [
4225
4340
  "knowledge-db.ts",
@@ -4269,8 +4384,8 @@ function extractDeletedFiles(input) {
4269
4384
  if (rmMatch) {
4270
4385
  const paths = rmMatch[1].split(/\s+/).filter((p) => !p.startsWith("-"));
4271
4386
  for (const p of paths) {
4272
- const relPath = p.startsWith("src/") ? p : p.replace(PROJECT_ROOT2 + "/", "");
4273
- if (relPath.startsWith("src/") || relPath.endsWith(".py")) {
4387
+ const relPath = isUnderSourceDir(p) ? p : p.replace(PROJECT_ROOT2 + "/", "");
4388
+ if (isUnderSourceDir(relPath) || relPath.endsWith(".py")) {
4274
4389
  files.push(relPath);
4275
4390
  }
4276
4391
  }
@@ -4280,7 +4395,7 @@ function extractDeletedFiles(input) {
4280
4395
  const content = input.tool_input.content || "";
4281
4396
  if (content.trim().length === 0) {
4282
4397
  const relPath = input.tool_input.file_path.replace(PROJECT_ROOT2 + "/", "");
4283
- if (relPath.startsWith("src/") || relPath.endsWith(".py")) {
4398
+ if (isUnderSourceDir(relPath) || relPath.endsWith(".py")) {
4284
4399
  files.push(relPath);
4285
4400
  }
4286
4401
  }
@@ -4363,7 +4478,7 @@ async function main2() {
4363
4478
  const input = await readStdin2();
4364
4479
  const hookInput = JSON.parse(input);
4365
4480
  const messages = runPreDeleteChecks(hookInput);
4366
- for (const msg of messages) writeHookMessage(msg);
4481
+ for (const msg of messages) writeHookContext(HOOK_EVENT2, msg);
4367
4482
  } catch (err) {
4368
4483
  recordHookFailure("pre-delete-check", err);
4369
4484
  process.stderr.write("MASSU SECURITY GATE \u2014 BLOCKED (gate could not evaluate this call)\n");
@@ -4382,7 +4497,7 @@ function readStdin2() {
4382
4497
  setTimeout(() => resolve5(data), 400);
4383
4498
  });
4384
4499
  }
4385
- if (process.argv[1]?.endsWith("pre-delete-check.js") || process.argv[1]?.endsWith("pre-delete-check")) {
4500
+ if (isDirectInvocation(import.meta.url)) {
4386
4501
  main2();
4387
4502
  }
4388
4503
 
@@ -4423,6 +4538,7 @@ function readStdinToEof(deadlineMs = 4e3) {
4423
4538
 
4424
4539
  // src/hooks/pre-tool-use-gate.ts
4425
4540
  init_hook_failure_signal();
4541
+ var HOOK_EVENT3 = "PreToolUse";
4426
4542
  var EXIT_DENY = 2;
4427
4543
  var EXIT_ALLOW = 0;
4428
4544
  function deny(reason, detail) {
@@ -4472,11 +4588,13 @@ async function main3() {
4472
4588
  }
4473
4589
  const blocking = findings.filter((f) => f.severity === "block");
4474
4590
  for (const f of findings.filter((x) => x.severity === "warn")) {
4475
- writeHookMessage(f.message);
4591
+ writeHookContext(HOOK_EVENT3, f.message);
4476
4592
  }
4477
4593
  if (blocking.length > 0) {
4478
4594
  deny(blocking.map((f) => f.message).join("\n\n"));
4479
4595
  }
4480
4596
  process.exit(EXIT_ALLOW);
4481
4597
  }
4482
- main3();
4598
+ if (isDirectInvocation(import.meta.url)) {
4599
+ main3();
4600
+ }
@@ -1167,6 +1167,13 @@ var init_config = __esm({
1167
1167
  });
1168
1168
  LanguageFrameworkEntrySchema = z2.object({
1169
1169
  framework: z2.string().optional(),
1170
+ // `source_dirs` is emitted by `init` (commands/init.ts:417) and existence-checked
1171
+ // by config validation (init.ts:868-876), but until 2026-08-14 it survived only via
1172
+ // `.passthrough()` — declared in the file, untyped in the schema. `lib/source-layout.ts`
1173
+ // derives the index builders' candidate set from it, so it is typed here rather than
1174
+ // read as an unknown: a consumer that has to shape-check its own SoT is one branch away
1175
+ // from silently contributing `undefined` to a path predicate.
1176
+ source_dirs: z2.array(z2.string()).optional(),
1170
1177
  test_framework: z2.string().optional(),
1171
1178
  test: z2.string().optional(),
1172
1179
  runtime: z2.string().optional(),
@@ -1339,6 +1346,16 @@ var init_memory_vector = __esm({
1339
1346
  }
1340
1347
  });
1341
1348
 
1349
+ // src/lib/timestamps.ts
1350
+ function nowIso() {
1351
+ return (/* @__PURE__ */ new Date()).toISOString();
1352
+ }
1353
+ var init_timestamps = __esm({
1354
+ "src/lib/timestamps.ts"() {
1355
+ "use strict";
1356
+ }
1357
+ });
1358
+
1342
1359
  // src/db-backup.ts
1343
1360
  import { existsSync as existsSync5, mkdirSync as mkdirSync4, readdirSync, statSync, unlinkSync, copyFileSync, rmSync as rmSync4 } from "fs";
1344
1361
  import { resolve as resolve3, join as join3, basename, dirname as dirname4 } from "path";
@@ -3670,8 +3687,8 @@ function linkSessionToTask(db, sessionId, taskId) {
3670
3687
  }
3671
3688
  function addConversationTurn(db, sessionId, turnNumber, userPrompt, assistantResponse, toolCallsJson, toolCallCount, promptTokens, responseTokens) {
3672
3689
  const result = db.prepare(`
3673
- INSERT INTO conversation_turns (session_id, turn_number, user_prompt, assistant_response, tool_calls_json, tool_call_count, prompt_tokens, response_tokens)
3674
- VALUES (?, ?, ?, ?, ?, ?, ?, ?)
3690
+ INSERT INTO conversation_turns (session_id, turn_number, user_prompt, assistant_response, tool_calls_json, tool_call_count, prompt_tokens, response_tokens, created_at)
3691
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
3675
3692
  `).run(
3676
3693
  sessionId,
3677
3694
  turnNumber,
@@ -3680,14 +3697,15 @@ function addConversationTurn(db, sessionId, turnNumber, userPrompt, assistantRes
3680
3697
  toolCallsJson,
3681
3698
  toolCallCount,
3682
3699
  promptTokens,
3683
- responseTokens
3700
+ responseTokens,
3701
+ nowIso()
3684
3702
  );
3685
3703
  return Number(result.lastInsertRowid);
3686
3704
  }
3687
3705
  function addToolCallDetail(db, sessionId, turnNumber, toolName, inputSummary, inputSize, outputSize, success, filesInvolved) {
3688
3706
  db.prepare(`
3689
- INSERT INTO tool_call_details (session_id, turn_number, tool_name, tool_input_summary, tool_input_size, tool_output_size, tool_success, files_involved)
3690
- VALUES (?, ?, ?, ?, ?, ?, ?, ?)
3707
+ INSERT INTO tool_call_details (session_id, turn_number, tool_name, tool_input_summary, tool_input_size, tool_output_size, tool_success, files_involved, created_at)
3708
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
3691
3709
  `).run(
3692
3710
  sessionId,
3693
3711
  turnNumber,
@@ -3696,7 +3714,8 @@ function addToolCallDetail(db, sessionId, turnNumber, toolName, inputSummary, in
3696
3714
  inputSize,
3697
3715
  outputSize,
3698
3716
  success ? 1 : 0,
3699
- filesInvolved ? JSON.stringify(filesInvolved) : null
3717
+ filesInvolved ? JSON.stringify(filesInvolved) : null,
3718
+ nowIso()
3700
3719
  );
3701
3720
  }
3702
3721
  function getLastProcessedLine(db, sessionId) {
@@ -3955,6 +3974,7 @@ var init_memory_db = __esm({
3955
3974
  init_db_driver();
3956
3975
  init_config();
3957
3976
  init_memory_vector();
3977
+ init_timestamps();
3958
3978
  init_db_backup();
3959
3979
  init_rule_delivery();
3960
3980
  init_rule_candidate_store();
@@ -4072,6 +4092,21 @@ function toolResponseText(raw) {
4072
4092
 
4073
4093
  // src/hooks/quality-event.ts
4074
4094
  init_hook_failure_signal();
4095
+ init_timestamps();
4096
+
4097
+ // src/hooks/lib/is-direct-invocation.ts
4098
+ import { pathToFileURL } from "url";
4099
+ function isDirectInvocation(moduleUrl) {
4100
+ const entry = process.argv[1];
4101
+ if (!entry) return false;
4102
+ try {
4103
+ return moduleUrl === pathToFileURL(entry).href;
4104
+ } catch {
4105
+ return false;
4106
+ }
4107
+ }
4108
+
4109
+ // src/hooks/quality-event.ts
4075
4110
  var TEST_FAILURE_PATTERNS = [
4076
4111
  /\bFAIL\b/,
4077
4112
  /✗/,
@@ -4135,11 +4170,11 @@ async function main() {
4135
4170
  const db = getMemoryDb();
4136
4171
  try {
4137
4172
  const stmt = db.prepare(`
4138
- INSERT INTO quality_events (session_id, event_type, tool_name, details)
4139
- VALUES (?, ?, ?, ?)
4173
+ INSERT INTO quality_events (session_id, event_type, tool_name, details, created_at)
4174
+ VALUES (?, ?, ?, ?, ?)
4140
4175
  `);
4141
4176
  for (const signal of signals) {
4142
- stmt.run(session_id, signal.event_type, tool_name, signal.details);
4177
+ stmt.run(session_id, signal.event_type, tool_name, signal.details, nowIso());
4143
4178
  }
4144
4179
  } finally {
4145
4180
  db.close();
@@ -4160,4 +4195,6 @@ function readStdin() {
4160
4195
  setTimeout(() => resolve5(data), 3e3);
4161
4196
  });
4162
4197
  }
4163
- main();
4198
+ if (isDirectInvocation(import.meta.url)) {
4199
+ main();
4200
+ }
@@ -697,6 +697,13 @@ var init_config = __esm({
697
697
  });
698
698
  LanguageFrameworkEntrySchema = z2.object({
699
699
  framework: z2.string().optional(),
700
+ // `source_dirs` is emitted by `init` (commands/init.ts:417) and existence-checked
701
+ // by config validation (init.ts:868-876), but until 2026-08-14 it survived only via
702
+ // `.passthrough()` — declared in the file, untyped in the schema. `lib/source-layout.ts`
703
+ // derives the index builders' candidate set from it, so it is typed here rather than
704
+ // read as an unknown: a consumer that has to shape-check its own SoT is one branch away
705
+ // from silently contributing `undefined` to a path predicate.
706
+ source_dirs: z2.array(z2.string()).optional(),
700
707
  test_framework: z2.string().optional(),
701
708
  test: z2.string().optional(),
702
709
  runtime: z2.string().optional(),
@@ -1339,6 +1346,16 @@ var init_memory_vector = __esm({
1339
1346
  }
1340
1347
  });
1341
1348
 
1349
+ // src/lib/timestamps.ts
1350
+ function nowIso() {
1351
+ return (/* @__PURE__ */ new Date()).toISOString();
1352
+ }
1353
+ var init_timestamps = __esm({
1354
+ "src/lib/timestamps.ts"() {
1355
+ "use strict";
1356
+ }
1357
+ });
1358
+
1342
1359
  // src/db-backup.ts
1343
1360
  import { existsSync as existsSync5, mkdirSync as mkdirSync4, readdirSync, statSync, unlinkSync, copyFileSync, rmSync as rmSync4 } from "fs";
1344
1361
  import { resolve as resolve3, join as join3, basename, dirname as dirname4 } from "path";
@@ -3599,8 +3616,8 @@ function linkSessionToTask(db, sessionId, taskId) {
3599
3616
  }
3600
3617
  function addConversationTurn(db, sessionId, turnNumber, userPrompt, assistantResponse, toolCallsJson, toolCallCount, promptTokens, responseTokens) {
3601
3618
  const result = db.prepare(`
3602
- INSERT INTO conversation_turns (session_id, turn_number, user_prompt, assistant_response, tool_calls_json, tool_call_count, prompt_tokens, response_tokens)
3603
- VALUES (?, ?, ?, ?, ?, ?, ?, ?)
3619
+ INSERT INTO conversation_turns (session_id, turn_number, user_prompt, assistant_response, tool_calls_json, tool_call_count, prompt_tokens, response_tokens, created_at)
3620
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
3604
3621
  `).run(
3605
3622
  sessionId,
3606
3623
  turnNumber,
@@ -3609,14 +3626,15 @@ function addConversationTurn(db, sessionId, turnNumber, userPrompt, assistantRes
3609
3626
  toolCallsJson,
3610
3627
  toolCallCount,
3611
3628
  promptTokens,
3612
- responseTokens
3629
+ responseTokens,
3630
+ nowIso()
3613
3631
  );
3614
3632
  return Number(result.lastInsertRowid);
3615
3633
  }
3616
3634
  function addToolCallDetail(db, sessionId, turnNumber, toolName, inputSummary, inputSize, outputSize, success, filesInvolved) {
3617
3635
  db.prepare(`
3618
- INSERT INTO tool_call_details (session_id, turn_number, tool_name, tool_input_summary, tool_input_size, tool_output_size, tool_success, files_involved)
3619
- VALUES (?, ?, ?, ?, ?, ?, ?, ?)
3636
+ INSERT INTO tool_call_details (session_id, turn_number, tool_name, tool_input_summary, tool_input_size, tool_output_size, tool_success, files_involved, created_at)
3637
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
3620
3638
  `).run(
3621
3639
  sessionId,
3622
3640
  turnNumber,
@@ -3625,7 +3643,8 @@ function addToolCallDetail(db, sessionId, turnNumber, toolName, inputSummary, in
3625
3643
  inputSize,
3626
3644
  outputSize,
3627
3645
  success ? 1 : 0,
3628
- filesInvolved ? JSON.stringify(filesInvolved) : null
3646
+ filesInvolved ? JSON.stringify(filesInvolved) : null,
3647
+ nowIso()
3629
3648
  );
3630
3649
  }
3631
3650
  function getLastProcessedLine(db, sessionId) {
@@ -3884,6 +3903,7 @@ var init_memory_db = __esm({
3884
3903
  init_db_driver();
3885
3904
  init_config();
3886
3905
  init_memory_vector();
3906
+ init_timestamps();
3887
3907
  init_db_backup();
3888
3908
  init_rule_delivery();
3889
3909
  init_rule_candidate_store();
@@ -3978,12 +3998,57 @@ import { existsSync as existsSync9, readFileSync as readFileSync6, readdirSync a
3978
3998
  import { basename as basename3, resolve as resolve5 } from "path";
3979
3999
 
3980
4000
  // src/hooks/lib/write-hook-message.ts
3981
- function writeHookMessage(message) {
3982
- process.stdout.write(JSON.stringify({ message }) + "\n");
4001
+ var HOOK_EVENTS = [
4002
+ "UserPromptSubmit",
4003
+ "UserPromptExpansion",
4004
+ "SessionStart",
4005
+ "PreToolUse",
4006
+ "PostToolUse",
4007
+ "Stop",
4008
+ "SubagentStop",
4009
+ "PreCompact"
4010
+ ];
4011
+ var ADDITIONAL_CONTEXT_MAX_CHARS = 1e4;
4012
+ function isHookEvent(value) {
4013
+ return typeof value === "string" && HOOK_EVENTS.includes(value);
4014
+ }
4015
+ function writeHookContext(hookEventName, message) {
4016
+ if (!isHookEvent(hookEventName)) {
4017
+ process.stderr.write(
4018
+ `[massu] writeHookContext: refusing to emit \u2014 unrecognised hook event ${JSON.stringify(hookEventName)}. Known: ${HOOK_EVENTS.join(", ")}.
4019
+ `
4020
+ );
4021
+ return;
4022
+ }
4023
+ if (typeof message !== "string" || message.length === 0) return;
4024
+ let additionalContext = message;
4025
+ if (additionalContext.length > ADDITIONAL_CONTEXT_MAX_CHARS) {
4026
+ const marker = `
4027
+ [massu] \u2026truncated at ${ADDITIONAL_CONTEXT_MAX_CHARS} chars (spec cap).`;
4028
+ additionalContext = additionalContext.slice(0, ADDITIONAL_CONTEXT_MAX_CHARS - marker.length) + marker;
4029
+ }
4030
+ process.stdout.write(
4031
+ JSON.stringify({ hookSpecificOutput: { hookEventName, additionalContext } }) + "\n"
4032
+ );
3983
4033
  }
3984
4034
 
3985
4035
  // src/hooks/rule-enforcement-pipeline.ts
3986
4036
  init_hook_failure_signal();
4037
+
4038
+ // src/hooks/lib/is-direct-invocation.ts
4039
+ import { pathToFileURL } from "url";
4040
+ function isDirectInvocation(moduleUrl) {
4041
+ const entry = process.argv[1];
4042
+ if (!entry) return false;
4043
+ try {
4044
+ return moduleUrl === pathToFileURL(entry).href;
4045
+ } catch {
4046
+ return false;
4047
+ }
4048
+ }
4049
+
4050
+ // src/hooks/rule-enforcement-pipeline.ts
4051
+ var HOOK_EVENT = "PostToolUse";
3987
4052
  async function main() {
3988
4053
  try {
3989
4054
  const input = await readStdin();
@@ -4081,7 +4146,7 @@ async function main() {
4081
4146
  lines.push(" This step is MANDATORY per the auto-learning pipeline.");
4082
4147
  lines.push("============================================================================");
4083
4148
  lines.push("");
4084
- writeHookMessage(lines.join("\n"));
4149
+ writeHookContext(HOOK_EVENT, lines.join("\n"));
4085
4150
  } catch (err) {
4086
4151
  recordHookFailure("rule-enforcement-pipeline", err);
4087
4152
  }
@@ -4098,4 +4163,6 @@ function readStdin() {
4098
4163
  setTimeout(() => resolve6(data), 3e3);
4099
4164
  });
4100
4165
  }
4101
- main();
4166
+ if (isDirectInvocation(import.meta.url)) {
4167
+ main();
4168
+ }