@bartolli/kmd 0.9.0 → 0.11.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/dist/kmd.mjs CHANGED
@@ -248,6 +248,12 @@ var init_vault_config = __esm({
248
248
  }).optional(),
249
249
  "handoff-gate": z.strictObject({
250
250
  reason: z.string().min(1).optional().describe("Stop-block preamble; the engine appends the error lines.")
251
+ }).optional(),
252
+ orient: z.strictObject({
253
+ text: z.string().min(1).optional().describe("Session-start prime instruction; the engine prepends the resolved scope.")
254
+ }).optional(),
255
+ reorient: z.strictObject({
256
+ text: z.string().min(1).optional().describe("Post-compaction re-orientation; the engine prepends the resolved scope.")
251
257
  }).optional()
252
258
  });
253
259
  VaultConfigSchema = z.strictObject({
@@ -269,7 +275,7 @@ var init_vault_config = __esm({
269
275
  'Full-replace of the trigger base per scope \u2014 escape hatch. "_all" is reserved for triggers_extra.'
270
276
  ),
271
277
  builtin_hooks: BuiltinHooksSchema.optional().describe(
272
- "Message overrides for the fixed-function hooks (resync, handoff-gate) by public id."
278
+ "Message overrides for the fixed-function hooks (resync, handoff-gate, orient, reorient) by public id."
273
279
  ),
274
280
  triggers_extra: TriggersSchema.optional().describe(
275
281
  'Appended per scope after the engine defaults; the reserved "_all" key fires in every session.'
@@ -809,15 +815,11 @@ async function syncVault(vaultRoot2) {
809
815
  }
810
816
  indexedPaths.push(path);
811
817
  }
812
- let pagesDeleted = 0;
813
- let linksDeleted = 0;
814
- if (indexedPaths.length > 0) {
815
- const placeholders = indexedPaths.map(() => "?").join(", ");
816
- const pageResult = db.prepare(`DELETE FROM pages WHERE path NOT IN (${placeholders})`).run(...indexedPaths);
817
- pagesDeleted = Number(pageResult.changes);
818
- const linkResult = db.prepare("DELETE FROM links WHERE source_path NOT IN (SELECT path FROM pages)").run();
819
- linksDeleted = Number(linkResult.changes);
820
- }
818
+ const placeholders = indexedPaths.map(() => "?").join(", ");
819
+ const pageResult = indexedPaths.length > 0 ? db.prepare(`DELETE FROM pages WHERE path NOT IN (${placeholders})`).run(...indexedPaths) : db.prepare("DELETE FROM pages").run();
820
+ const pagesDeleted = Number(pageResult.changes);
821
+ const linkResult = db.prepare("DELETE FROM links WHERE source_path NOT IN (SELECT path FROM pages)").run();
822
+ const linksDeleted = Number(linkResult.changes);
821
823
  db.exec("INSERT INTO pages_fts(pages_fts) VALUES('rebuild')");
822
824
  setMeta(db, "vault_root", canonicalVaultRoot(vaultRoot2));
823
825
  setMeta(db, "last_synced", (/* @__PURE__ */ new Date()).toISOString());
@@ -841,7 +843,7 @@ async function runSync() {
841
843
  console.log(`sync: ${env.WIKI_VAULT} \u2192 ${resolveIndexPath(env.WIKI_VAULT)}`);
842
844
  const stats = await syncVault(env.WIKI_VAULT);
843
845
  if (stats.noPages) {
844
- console.warn("no indexable pages found; skipping orphan deletion (safety)");
846
+ console.warn("no indexable pages found; index swept empty");
845
847
  }
846
848
  console.log(
847
849
  `done: ${stats.changed} changed, ${stats.unchanged} unchanged, ${stats.skipped} skipped, ${stats.pagesDeleted} pages deleted, ${stats.linksDeleted} link orphans cleared`
@@ -2361,6 +2363,8 @@ __export(hook_exports, {
2361
2363
  dedupePretoolMatches: () => dedupePretoolMatches,
2362
2364
  effectiveTriggers: () => effectiveTriggers,
2363
2365
  evaluateMatches: () => evaluateMatches,
2366
+ explainPretool: () => explainPretool,
2367
+ explainPrompt: () => explainPrompt,
2364
2368
  hookStateDir: () => hookStateDir,
2365
2369
  kiroIdePromptEvent: () => kiroIdePromptEvent,
2366
2370
  loadTriggerFile: () => loadTriggerFile,
@@ -2368,14 +2372,18 @@ __export(hook_exports, {
2368
2372
  matchPromptTriggers: () => matchPromptTriggers,
2369
2373
  parsePretoolEvent: () => parsePretoolEvent,
2370
2374
  parsePromptEvent: () => parsePromptEvent,
2375
+ parseSessionStartEvent: () => parseSessionStartEvent,
2371
2376
  parseStopEvent: () => parseStopEvent,
2372
2377
  renderPosttool: () => renderPosttool,
2373
2378
  renderPretool: () => renderPretool,
2379
+ renderPrompt: () => renderPrompt,
2380
+ renderSessionStart: () => renderSessionStart,
2374
2381
  renderStop: () => renderStop,
2375
2382
  resolveScope: () => resolveScope,
2376
2383
  runHookPosttool: () => runHookPosttool,
2377
2384
  runHookPretool: () => runHookPretool,
2378
2385
  runHookPrompt: () => runHookPrompt,
2386
+ runHookSessionStart: () => runHookSessionStart,
2379
2387
  runHookStop: () => runHookStop,
2380
2388
  vaultPathTouched: () => vaultPathTouched
2381
2389
  });
@@ -2464,37 +2472,50 @@ function openPromptIndex(prompt) {
2464
2472
  db.prepare("INSERT INTO prompt_doc (text) VALUES (?)").run(prompt);
2465
2473
  return db;
2466
2474
  }
2475
+ function promptProbe(trigger, prompt, getDb) {
2476
+ let keywords = "unset";
2477
+ if (trigger.keywords !== void 0 && trigger.keywords.length > 0) {
2478
+ const row = getDb().prepare("SELECT count(*) AS n FROM prompt_doc WHERE prompt_doc MATCH ?").get(keywordQuery(trigger.keywords));
2479
+ keywords = row.n > 0 ? "hit" : "miss";
2480
+ }
2481
+ let intent = "unset";
2482
+ if (trigger.intent !== void 0) {
2483
+ intent = trigger.intent.some((pattern) => new RegExp(pattern, "i").test(prompt)) ? "hit" : "miss";
2484
+ }
2485
+ return { keywords, intent };
2486
+ }
2487
+ function promptHit(probe) {
2488
+ return probe.keywords === "hit" || probe.intent === "hit";
2489
+ }
2490
+ function isInjectTrigger(trigger) {
2491
+ return trigger.on === "prompt" && trigger.enforce === "inject" && trigger.text !== void 0;
2492
+ }
2467
2493
  function matchPromptTriggers(prompt, triggers) {
2468
- const candidates = triggers.filter(
2469
- (trigger) => trigger.on === "prompt" && trigger.enforce === "inject" && trigger.text !== void 0
2470
- );
2494
+ const candidates = triggers.filter(isInjectTrigger);
2471
2495
  if (candidates.length === 0) return [];
2472
2496
  const matches = [];
2473
- let db = null;
2497
+ const state = { db: null };
2498
+ const getDb = () => state.db ??= openPromptIndex(prompt);
2474
2499
  try {
2475
2500
  for (const trigger of candidates) {
2476
- let hit = false;
2477
- if (trigger.keywords !== void 0 && trigger.keywords.length > 0) {
2478
- db ??= openPromptIndex(prompt);
2479
- const row = db.prepare("SELECT count(*) AS n FROM prompt_doc WHERE prompt_doc MATCH ?").get(keywordQuery(trigger.keywords));
2480
- hit = row.n > 0;
2481
- }
2482
- if (!hit && trigger.intent !== void 0) {
2483
- hit = trigger.intent.some((pattern) => new RegExp(pattern, "i").test(prompt));
2484
- }
2485
- if (hit) {
2486
- matches.push({
2487
- id: trigger.id,
2488
- text: trigger.text,
2489
- ...trigger.dedup !== void 0 && { dedup: trigger.dedup }
2490
- });
2491
- }
2501
+ if (!promptHit(promptProbe(trigger, prompt, getDb))) continue;
2502
+ matches.push({
2503
+ id: trigger.id,
2504
+ text: trigger.text,
2505
+ ...trigger.dedup !== void 0 && { dedup: trigger.dedup }
2506
+ });
2492
2507
  }
2493
2508
  } finally {
2494
- db?.close();
2509
+ state.db?.close();
2495
2510
  }
2496
2511
  return matches;
2497
2512
  }
2513
+ function uniqueTexts(texts) {
2514
+ return [...new Set(texts)];
2515
+ }
2516
+ function renderPrompt(matches) {
2517
+ return uniqueTexts(matches.map((match) => match.text));
2518
+ }
2498
2519
  function parsePretoolEvent(raw) {
2499
2520
  const fields = eventFields(raw);
2500
2521
  if (fields === null) return null;
@@ -2543,32 +2564,41 @@ function pathCandidates(toolInput, cwd) {
2543
2564
  }
2544
2565
  return candidates;
2545
2566
  }
2567
+ function pretoolStage(trigger, toolName, toolInput, cwd) {
2568
+ if (trigger.tool !== void 0 && trigger.tool !== toolName) return "tool";
2569
+ if (trigger.args_match !== void 0) {
2570
+ const serialized = JSON.stringify(toolInput ?? {});
2571
+ if (!new RegExp(trigger.args_match).test(serialized)) return "args";
2572
+ }
2573
+ if (trigger.files !== void 0 && trigger.files.length > 0) {
2574
+ const candidates = [...pathCandidates(toolInput, cwd), ...patchPaths(toolInput)];
2575
+ const hit = trigger.files.some((glob) => {
2576
+ const regex = globToRegExp(glob);
2577
+ return candidates.some((candidate) => regex.test(candidate));
2578
+ });
2579
+ if (!hit) return "files";
2580
+ }
2581
+ if (pretoolText(trigger) === void 0) return "payload";
2582
+ return "hit";
2583
+ }
2584
+ function pretoolText(trigger) {
2585
+ return trigger.enforce === "block" ? trigger.reason : trigger.text;
2586
+ }
2587
+ function pretoolMatch(trigger) {
2588
+ return {
2589
+ id: trigger.id,
2590
+ enforce: trigger.enforce,
2591
+ text: pretoolText(trigger),
2592
+ ...trigger.when !== void 0 && { when: trigger.when },
2593
+ ...trigger.dedup !== void 0 && { dedup: trigger.dedup }
2594
+ };
2595
+ }
2546
2596
  function matchPretoolTriggers(toolName, toolInput, triggers, cwd) {
2547
2597
  const matches = [];
2548
2598
  for (const trigger of triggers) {
2549
2599
  if (trigger.on !== "pretool") continue;
2550
- if (trigger.tool !== void 0 && trigger.tool !== toolName) continue;
2551
- if (trigger.args_match !== void 0) {
2552
- const serialized = JSON.stringify(toolInput ?? {});
2553
- if (!new RegExp(trigger.args_match).test(serialized)) continue;
2554
- }
2555
- if (trigger.files !== void 0 && trigger.files.length > 0) {
2556
- const candidates = [...pathCandidates(toolInput, cwd), ...patchPaths(toolInput)];
2557
- const hit = trigger.files.some((glob) => {
2558
- const regex = globToRegExp(glob);
2559
- return candidates.some((candidate) => regex.test(candidate));
2560
- });
2561
- if (!hit) continue;
2562
- }
2563
- const text = trigger.enforce === "block" ? trigger.reason : trigger.text;
2564
- if (text === void 0) continue;
2565
- matches.push({
2566
- id: trigger.id,
2567
- enforce: trigger.enforce,
2568
- text,
2569
- ...trigger.when !== void 0 && { when: trigger.when },
2570
- ...trigger.dedup !== void 0 && { dedup: trigger.dedup }
2571
- });
2600
+ if (pretoolStage(trigger, toolName, toolInput, cwd) !== "hit") continue;
2601
+ matches.push(pretoolMatch(trigger));
2572
2602
  }
2573
2603
  return matches;
2574
2604
  }
@@ -2586,18 +2616,23 @@ function evaluateMatches(matches, vaultRoot2) {
2586
2616
  }
2587
2617
  return { fired, skipped };
2588
2618
  }
2589
- function evaluateWhen(when, vaultRoot2) {
2590
- if (typeof when === "string") return null;
2619
+ function evaluateWhenVerdict(when, vaultRoot2) {
2620
+ if (typeof when === "string") return "unknown";
2591
2621
  try {
2592
2622
  const than = newestUpdated(vaultRoot2, when.than);
2593
- if (than === null) return true;
2623
+ if (than === null) return "vacuous";
2594
2624
  const fresh = newestUpdated(vaultRoot2, when.fresh);
2595
- if (fresh === null) return false;
2596
- return fresh >= than;
2625
+ if (fresh === null) return "unmet";
2626
+ return fresh >= than ? "satisfied" : "unmet";
2597
2627
  } catch {
2598
- return null;
2628
+ return "unknown";
2599
2629
  }
2600
2630
  }
2631
+ function evaluateWhen(when, vaultRoot2) {
2632
+ const verdict = evaluateWhenVerdict(when, vaultRoot2);
2633
+ if (verdict === "unknown") return null;
2634
+ return verdict !== "unmet";
2635
+ }
2601
2636
  function newestUpdated(vaultRoot2, globs) {
2602
2637
  const regexes = globs.map(globToRegExp);
2603
2638
  let newest = null;
@@ -2624,26 +2659,27 @@ function readUpdated(path) {
2624
2659
  return null;
2625
2660
  }
2626
2661
  function renderPretool(matches, format) {
2627
- const block = matches.find((match) => match.enforce === "block");
2628
- const context = matches.filter((match) => match.enforce === "inject").map((match) => match.text);
2629
- const warnings = matches.filter((match) => match.enforce === "warn").map((match) => match.text);
2662
+ const byClass = (enforce) => uniqueTexts(matches.filter((match) => match.enforce === enforce).map((match) => match.text));
2663
+ const reasons = byClass("block");
2664
+ const context = byClass("inject");
2665
+ const warnings = byClass("warn");
2630
2666
  if (format === "claude") {
2631
2667
  const hookSpecificOutput = { hookEventName: "PreToolUse" };
2632
- if (block !== void 0) {
2668
+ if (reasons.length > 0) {
2633
2669
  hookSpecificOutput.permissionDecision = "deny";
2634
- hookSpecificOutput.permissionDecisionReason = block.text;
2670
+ hookSpecificOutput.permissionDecisionReason = reasons.join("\n");
2635
2671
  }
2636
2672
  if (context.length > 0) {
2637
2673
  hookSpecificOutput.additionalContext = context.join("\n");
2638
2674
  }
2639
- const decided = block !== void 0 || context.length > 0;
2675
+ const decided = reasons.length > 0 || context.length > 0;
2640
2676
  return { stdout: decided ? JSON.stringify({ hookSpecificOutput }) : null, stderr: warnings };
2641
2677
  }
2642
2678
  if (matches.length === 0) return { stdout: null, stderr: [] };
2643
2679
  return {
2644
2680
  stdout: JSON.stringify({
2645
- decision: block !== void 0 ? "deny" : "none",
2646
- ...block !== void 0 && { reason: block.text },
2681
+ decision: reasons.length > 0 ? "deny" : "none",
2682
+ ...reasons.length > 0 && { reason: reasons.join("\n") },
2647
2683
  context,
2648
2684
  warnings
2649
2685
  }),
@@ -2663,9 +2699,27 @@ function patchPaths(toolInput) {
2663
2699
  }
2664
2700
  return paths;
2665
2701
  }
2702
+ function commandPaths(toolInput) {
2703
+ if (typeof toolInput !== "object" || toolInput === null) return [];
2704
+ const command2 = toolInput.command;
2705
+ if (typeof command2 !== "string") return [];
2706
+ const paths = [];
2707
+ for (const match of command2.matchAll(COMMAND_TOKEN_RE)) {
2708
+ const token = match[1] ?? match[2] ?? match[3];
2709
+ for (const piece of token.split(/[;|&<>()]+/)) {
2710
+ if (piece === "") continue;
2711
+ if (piece === "." || piece === ".." || PATHISH_RE.test(piece)) paths.push(piece);
2712
+ }
2713
+ }
2714
+ return paths;
2715
+ }
2666
2716
  function vaultPathTouched(toolInput, vaultRoot2, cwd) {
2667
2717
  const root = resolve3(vaultRoot2);
2668
- const candidates = [...pathCandidates(toolInput, cwd), ...patchPaths(toolInput)];
2718
+ const candidates = [
2719
+ ...pathCandidates(toolInput, cwd),
2720
+ ...patchPaths(toolInput),
2721
+ ...commandPaths(toolInput)
2722
+ ];
2669
2723
  return candidates.some((candidate) => {
2670
2724
  const absolute = resolve3(cwd ?? ".", candidate);
2671
2725
  return absolute === root || absolute.startsWith(`${root}/`);
@@ -2711,50 +2765,141 @@ function renderStop(findings, reason) {
2711
2765
  ${lines.join("\n")}`
2712
2766
  });
2713
2767
  }
2714
- function dedupePretoolMatches(stateDir, sessionId, matches) {
2768
+ function dedupePretoolMatches(stateDir, sessionId, matches, persist = true) {
2715
2769
  const blocks = matches.filter((match) => match.enforce === "block");
2716
2770
  const rest = matches.filter((match) => match.enforce !== "block");
2717
- const fresh = dedupeMatches(stateDir, sessionId, rest);
2771
+ const fresh = dedupeMatches(stateDir, sessionId, rest, Date.now(), persist);
2718
2772
  return matches.filter((match) => blocks.includes(match) || fresh.includes(match));
2719
2773
  }
2720
2774
  function hookStateDir() {
2721
2775
  return join10(kmdHome(), "state", "hook");
2722
2776
  }
2723
- function dedupeMatches(stateDir, sessionId, matches, now = Date.now()) {
2777
+ function explainPrompt(options) {
2778
+ const now = options.now ?? Date.now();
2779
+ const fired = readFired(join10(options.stateDir, safeName(options.sessionId)));
2780
+ const entries = [];
2781
+ const rendered = [];
2782
+ const state = { db: null };
2783
+ const getDb = () => state.db ??= openPromptIndex(options.prompt);
2784
+ try {
2785
+ for (const trigger of options.triggers) {
2786
+ if (!isInjectTrigger(trigger)) {
2787
+ entries.push({ id: trigger.id, considered: false });
2788
+ continue;
2789
+ }
2790
+ const probe = promptProbe(trigger, options.prompt, getDb);
2791
+ const matched = promptHit(probe);
2792
+ const entry = {
2793
+ id: trigger.id,
2794
+ considered: true,
2795
+ keywords: probe.keywords,
2796
+ intent: probe.intent,
2797
+ matched
2798
+ };
2799
+ entries.push(entry);
2800
+ if (!matched) {
2801
+ entry.fired = false;
2802
+ continue;
2803
+ }
2804
+ const key = dedupKey(trigger, now);
2805
+ entry.dedup = key === null ? "never" : fired.has(key) ? "suppressed" : "fresh";
2806
+ entry.fired = entry.dedup !== "suppressed";
2807
+ if (entry.fired) rendered.push({ id: trigger.id, text: trigger.text });
2808
+ }
2809
+ } finally {
2810
+ state.db?.close();
2811
+ }
2812
+ return { triggers: entries, output: renderPrompt(rendered) };
2813
+ }
2814
+ function explainPretool(options) {
2815
+ const now = options.now ?? Date.now();
2816
+ const fired = readFired(join10(options.stateDir, safeName(options.sessionId)));
2817
+ const entries = [];
2818
+ const rendered = [];
2819
+ for (const trigger of options.triggers) {
2820
+ if (trigger.on !== "pretool") {
2821
+ entries.push({ id: trigger.id, considered: false });
2822
+ continue;
2823
+ }
2824
+ const stage = pretoolStage(trigger, options.toolName, options.toolInput, options.cwd);
2825
+ const entry = {
2826
+ id: trigger.id,
2827
+ enforce: trigger.enforce,
2828
+ considered: true,
2829
+ matcher: stage === "hit" ? "hit" : `${stage}-miss`
2830
+ };
2831
+ entries.push(entry);
2832
+ if (stage !== "hit") {
2833
+ entry.fired = false;
2834
+ continue;
2835
+ }
2836
+ if (trigger.when !== void 0) {
2837
+ entry.when = evaluateWhenVerdict(trigger.when, options.vaultRoot);
2838
+ if (entry.when !== "unmet") {
2839
+ entry.fired = false;
2840
+ continue;
2841
+ }
2842
+ }
2843
+ const match = pretoolMatch(trigger);
2844
+ if (match.enforce === "block") {
2845
+ entry.dedup = "exempt";
2846
+ } else {
2847
+ const key = dedupKey(match, now);
2848
+ entry.dedup = key === null ? "never" : fired.has(key) ? "suppressed" : "fresh";
2849
+ }
2850
+ entry.fired = entry.dedup !== "suppressed";
2851
+ if (entry.fired) rendered.push(match);
2852
+ }
2853
+ return { triggers: entries, outcome: renderPretool(rendered, options.format ?? "neutral") };
2854
+ }
2855
+ function dedupeMatches(stateDir, sessionId, matches, now = Date.now(), persist = true) {
2724
2856
  if (matches.length === 0) return [];
2725
- const file = join10(stateDir, `${sessionId.replace(/[^A-Za-z0-9._-]/g, "_")}.json`);
2726
- const fired = readFired(file);
2857
+ const dir = join10(stateDir, safeName(sessionId));
2858
+ const fired = readFired(dir);
2727
2859
  const fresh = [];
2728
2860
  const record = [];
2729
2861
  for (const match of matches) {
2730
- if (match.dedup === "never") {
2862
+ const key = dedupKey(match, now);
2863
+ if (key === null) {
2731
2864
  fresh.push(match);
2732
2865
  continue;
2733
2866
  }
2734
- const key = typeof match.dedup === "object" ? `${match.id}@${Math.floor(now / (match.dedup.minutes * 6e4))}` : match.id;
2735
2867
  if (fired.has(key)) continue;
2736
2868
  fresh.push(match);
2737
2869
  record.push(key);
2738
2870
  }
2739
- if (record.length > 0) {
2740
- mkdirSync4(stateDir, { recursive: true });
2741
- for (const key of record) {
2742
- fired.add(key);
2871
+ if (persist && record.length > 0) {
2872
+ try {
2873
+ mkdirSync4(dir, { recursive: true });
2874
+ for (const key of record) {
2875
+ try {
2876
+ writeFileSync(join10(dir, key), "", { flag: "wx" });
2877
+ } catch (err) {
2878
+ if (err.code !== "EEXIST") throw err;
2879
+ }
2880
+ }
2881
+ pruneStale(stateDir, dir);
2882
+ } catch (err) {
2883
+ diag2(`dedup state not persisted: ${err instanceof Error ? err.message : String(err)}`);
2743
2884
  }
2744
- writeFileSync(file, JSON.stringify([...fired]));
2745
- pruneStale(stateDir, file);
2746
2885
  }
2747
2886
  return fresh;
2748
2887
  }
2749
- function readFired(file) {
2888
+ function safeName(part) {
2889
+ return part.replace(/[^A-Za-z0-9._@-]/g, "_");
2890
+ }
2891
+ function dedupKey(match, now) {
2892
+ if (match.dedup === "never") return null;
2893
+ return safeName(
2894
+ typeof match.dedup === "object" ? `${match.id}@${Math.floor(now / (match.dedup.minutes * 6e4))}` : match.id
2895
+ );
2896
+ }
2897
+ function readFired(dir) {
2750
2898
  try {
2751
- const data = JSON.parse(readFileSync(file, "utf8"));
2752
- if (Array.isArray(data)) {
2753
- return new Set(data.filter((entry) => typeof entry === "string"));
2754
- }
2899
+ return new Set(readdirSync2(dir));
2755
2900
  } catch {
2901
+ return /* @__PURE__ */ new Set();
2756
2902
  }
2757
- return /* @__PURE__ */ new Set();
2758
2903
  }
2759
2904
  function pruneStale(stateDir, keep) {
2760
2905
  try {
@@ -2762,7 +2907,7 @@ function pruneStale(stateDir, keep) {
2762
2907
  for (const entry of readdirSync2(stateDir)) {
2763
2908
  const path = join10(stateDir, entry);
2764
2909
  if (path !== keep && statSync(path).mtimeMs < cutoff) {
2765
- rmSync2(path, { force: true });
2910
+ rmSync2(path, { recursive: true, force: true });
2766
2911
  }
2767
2912
  }
2768
2913
  } catch {
@@ -2776,7 +2921,9 @@ function hookInvocation() {
2776
2921
  options: {
2777
2922
  scope: { type: "string" },
2778
2923
  harness: { type: "string" },
2779
- triggers: { type: "string" }
2924
+ triggers: { type: "string" },
2925
+ "dry-run": { type: "boolean" },
2926
+ explain: { type: "boolean" }
2780
2927
  }
2781
2928
  });
2782
2929
  const vaultRoot2 = positionals2[2] ?? process.env.WIKI_VAULT;
@@ -2788,7 +2935,9 @@ function hookInvocation() {
2788
2935
  vaultRoot: vaultRoot2,
2789
2936
  scope: typeof values2.scope === "string" ? values2.scope : process.env.WIKI_SCOPE,
2790
2937
  harness: values2.harness,
2791
- triggersFile: values2.triggers
2938
+ triggersFile: values2.triggers,
2939
+ dryRun: values2["dry-run"] === true || values2.explain === true,
2940
+ explain: values2.explain === true
2792
2941
  };
2793
2942
  }
2794
2943
  function resolveFileTriggers(invocation) {
@@ -2826,9 +2975,26 @@ async function runHookPrompt() {
2826
2975
  for (const id of duplicates) {
2827
2976
  diag2(`duplicate trigger id "${id}" \u2014 later occurrence ignored`);
2828
2977
  }
2978
+ if (invocation.explain) {
2979
+ const trace = explainPrompt({
2980
+ prompt: event.prompt,
2981
+ triggers,
2982
+ stateDir: hookStateDir(),
2983
+ sessionId: event.session_id
2984
+ });
2985
+ console.log(JSON.stringify({ event: "prompt", scope: scope ?? null, duplicates, ...trace }));
2986
+ return;
2987
+ }
2829
2988
  const matches = matchPromptTriggers(event.prompt, triggers);
2830
- for (const match of dedupeMatches(hookStateDir(), event.session_id, matches)) {
2831
- console.log(match.text);
2989
+ const fresh = dedupeMatches(
2990
+ hookStateDir(),
2991
+ event.session_id,
2992
+ matches,
2993
+ Date.now(),
2994
+ !invocation.dryRun
2995
+ );
2996
+ for (const line of renderPrompt(fresh)) {
2997
+ console.log(line);
2832
2998
  }
2833
2999
  } catch (err) {
2834
3000
  diag2(err instanceof Error ? err.message : String(err));
@@ -2860,13 +3026,27 @@ async function runHookPretool() {
2860
3026
  for (const id of duplicates) {
2861
3027
  diag2(`duplicate trigger id "${id}" \u2014 later occurrence ignored`);
2862
3028
  }
3029
+ if (invocation.explain) {
3030
+ const trace = explainPretool({
3031
+ toolName: event.tool_name,
3032
+ toolInput: event.tool_input,
3033
+ triggers,
3034
+ vaultRoot: vaultRoot2,
3035
+ stateDir: hookStateDir(),
3036
+ sessionId: event.session_id,
3037
+ ...event.cwd !== void 0 && { cwd: event.cwd },
3038
+ format
3039
+ });
3040
+ console.log(JSON.stringify({ event: "pretool", scope: scope ?? null, duplicates, ...trace }));
3041
+ return;
3042
+ }
2863
3043
  const matches = matchPretoolTriggers(event.tool_name, event.tool_input, triggers, event.cwd);
2864
3044
  const { fired, skipped } = evaluateMatches(matches, vaultRoot2);
2865
3045
  for (const id of skipped) {
2866
3046
  diag2(`trigger "${id}": unknown or unevaluable predicate \u2014 skipped`);
2867
3047
  }
2868
3048
  const rendered = renderPretool(
2869
- dedupePretoolMatches(hookStateDir(), event.session_id, fired),
3049
+ dedupePretoolMatches(hookStateDir(), event.session_id, fired, !invocation.dryRun),
2870
3050
  format
2871
3051
  );
2872
3052
  for (const line of rendered.stderr) {
@@ -2883,6 +3063,10 @@ async function runHookPosttool() {
2883
3063
  try {
2884
3064
  const invocation = hookInvocation();
2885
3065
  if (invocation === null) return;
3066
+ if (invocation.dryRun) {
3067
+ diag2("--dry-run/--explain support prompt and pretool events only");
3068
+ return;
3069
+ }
2886
3070
  let format = "neutral";
2887
3071
  if (invocation.harness === "claude") {
2888
3072
  format = "claude";
@@ -2918,6 +3102,10 @@ async function runHookStop() {
2918
3102
  try {
2919
3103
  const invocation = hookInvocation();
2920
3104
  if (invocation === null) return;
3105
+ if (invocation.dryRun) {
3106
+ diag2("--dry-run/--explain support prompt and pretool events only");
3107
+ return;
3108
+ }
2921
3109
  const event = parseStopEvent(await readStdin());
2922
3110
  if (event === null) {
2923
3111
  diag2("stdin is not a stop event ({session_id})");
@@ -2939,6 +3127,42 @@ async function runHookStop() {
2939
3127
  diag2(err instanceof Error ? err.message : String(err));
2940
3128
  }
2941
3129
  }
3130
+ function parseSessionStartEvent(raw) {
3131
+ const fields = eventFields(raw);
3132
+ if (fields === null) return null;
3133
+ const { session_id, cwd, source } = fields;
3134
+ if (typeof session_id !== "string") return null;
3135
+ return {
3136
+ session_id,
3137
+ ...typeof cwd === "string" && { cwd },
3138
+ ...typeof source === "string" && { source }
3139
+ };
3140
+ }
3141
+ function renderSessionStart(scope, source, messages = {}) {
3142
+ const text = source === "compact" ? messages.reorient?.text ?? REORIENT_TEXT : messages.orient?.text ?? ORIENT_TEXT;
3143
+ return `Wiki scope "${scope}": ${text}`;
3144
+ }
3145
+ async function runHookSessionStart() {
3146
+ try {
3147
+ const invocation = hookInvocation();
3148
+ if (invocation === null) return;
3149
+ if (invocation.dryRun) {
3150
+ diag2("--dry-run/--explain support prompt and pretool events only");
3151
+ return;
3152
+ }
3153
+ const event = parseSessionStartEvent(await readStdin());
3154
+ if (event === null) {
3155
+ diag2("stdin is not a session-start event ({session_id})");
3156
+ return;
3157
+ }
3158
+ const config = await loadVaultConfig(invocation.vaultRoot);
3159
+ const scope = invocation.scope ?? resolveScope(config, event.cwd);
3160
+ if (scope === void 0) return;
3161
+ console.log(renderSessionStart(scope, event.source, config.builtin_hooks ?? {}));
3162
+ } catch (err) {
3163
+ diag2(err instanceof Error ? err.message : String(err));
3164
+ }
3165
+ }
2942
3166
  async function readStdin() {
2943
3167
  process.stdin.setEncoding("utf8");
2944
3168
  let input = "";
@@ -2950,7 +3174,7 @@ async function readStdin() {
2950
3174
  function diag2(message) {
2951
3175
  console.error(`kmd hook: ${message}`);
2952
3176
  }
2953
- var DEFAULT_TRIGGERS, SESSION_STATE_MAX_AGE_MS, KIRO_IDE_BUCKET_MS, ALL_SCOPES_KEY, PATCH_FILE_RE, RESYNC_REASON, RESYNC_TEXT, HANDOFF_GATE_REASON;
3177
+ var DEFAULT_TRIGGERS, SESSION_STATE_MAX_AGE_MS, KIRO_IDE_BUCKET_MS, ALL_SCOPES_KEY, PATCH_FILE_RE, COMMAND_TOKEN_RE, PATHISH_RE, RESYNC_REASON, RESYNC_TEXT, HANDOFF_GATE_REASON, ORIENT_TEXT, REORIENT_TEXT;
2954
3178
  var init_hook = __esm({
2955
3179
  "../cli/src/hook.ts"() {
2956
3180
  "use strict";
@@ -2964,9 +3188,13 @@ var init_hook = __esm({
2964
3188
  KIRO_IDE_BUCKET_MS = 30 * 60 * 1e3;
2965
3189
  ALL_SCOPES_KEY = "_all";
2966
3190
  PATCH_FILE_RE = /^\*\*\* (?:Add|Update|Delete) File: (.+)$/gm;
3191
+ COMMAND_TOKEN_RE = /"([^"]*)"|'([^']*)'|(\S+)/g;
3192
+ PATHISH_RE = /[/*$]|\.(?:md|base|canvas|ya?ml)$/i;
2967
3193
  RESYNC_REASON = "Edit landed; the index sync is held until these validate errors are fixed";
2968
3194
  RESYNC_TEXT = "kmd sync failed \u2014 index not updated; see hook stderr";
2969
3195
  HANDOFF_GATE_REASON = "Validate errors are outstanding and the index sync is held \u2014 fix them, let the resync run, then finish";
3196
+ ORIENT_TEXT = "prime via the wiki MCP prime tool before substantive work \u2014 the primer carries current focus, book of work, and invariants.";
3197
+ REORIENT_TEXT = "context was compacted and transcript detail is lost \u2014 re-read the primer via the wiki MCP prime tool and route uncaptured findings into the wiki before continuing.";
2970
3198
  }
2971
3199
  });
2972
3200
 
@@ -2987,7 +3215,7 @@ commands:
2987
3215
  mcp [<vault-root>] start the stdio MCP server (default: $WIKI_VAULT)
2988
3216
  config [<vault-root>] print vault + index resolution; with no vault, list known vaults
2989
3217
  db reset [<vault-root>] delete the vault's index (default: $WIKI_VAULT)
2990
- hook <prompt|pretool|posttool|stop> [<vault-root>] [--scope <s>] [--harness <claude|kiro-ide>] [--triggers <file>]
3218
+ hook <prompt|pretool|posttool|stop|session-start> [<vault-root>] [--scope <s>] [--harness <claude|kiro-ide>] [--triggers <file>]
2991
3219
  harness gate engine: JSON event on stdin, decision/context on stdout;
2992
3220
  posttool auto-runs validate + sync after a vault write;
2993
3221
  stop blocks the handoff once while validate errors hold the sync
@@ -3068,11 +3296,14 @@ async function run() {
3068
3296
  } else if (sub === "stop") {
3069
3297
  const { runHookStop: runHookStop2 } = await Promise.resolve().then(() => (init_hook(), hook_exports));
3070
3298
  await runHookStop2();
3299
+ } else if (sub === "session-start") {
3300
+ const { runHookSessionStart: runHookSessionStart2 } = await Promise.resolve().then(() => (init_hook(), hook_exports));
3301
+ await runHookSessionStart2();
3071
3302
  } else if (sub) {
3072
3303
  console.error(`kmd hook: unknown event: ${sub}`);
3073
3304
  } else {
3074
3305
  console.error(
3075
- "usage: kmd hook <prompt|pretool|posttool|stop> [<vault-root>] [--scope <scope>] [--harness <claude|kiro-ide>]"
3306
+ "usage: kmd hook <prompt|pretool|posttool|stop|session-start> [<vault-root>] [--scope <scope>] [--harness <claude|kiro-ide>]"
3076
3307
  );
3077
3308
  process.exit(2);
3078
3309
  }