@niroai/niro 0.10.1 → 0.12.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/src/mcp/setup.js CHANGED
@@ -50,19 +50,30 @@ const NIRO_MD_MARKER = "<!-- niro-mcp -->";
50
50
  * read once per session, which is why it now states its own non-expiry explicitly. Wording is imperative
51
51
  * and names concrete tools so the agent knows what to call instead of native search.
52
52
  */
53
- const NIRO_EXPLORE_FIRST_SENTENCE = "MUST FOLLOW: before you run grep, find, glob, or open a file to hunt for something, first check whether a Niro tool answers it (code_grep for content search, resolve_fqn and get_source_code for a known symbol, find_handler and find_api_endpoints for entry points). Use the connection tools (get_call_chain, get_reverse_call_chain, get_blast_radius, get_impact_analysis, get_service_topology) to understand how code connects BEFORE deciding which file to read or search next, rather than reading files to rebuild that picture by hand. Native search is the fallback for what Niro does not cover (config files, docs, logs, generated output, non-code text) or when the tools are unavailable. This rule does not expire: it holds on the first turn and on the fiftieth, mid-task as much as at the start of a session, and a long conversation is not a reason to drift back to native search.";
53
+ const NIRO_EXPLORE_FIRST_SENTENCE = "MUST FOLLOW: before you run grep, find, glob, or open a file to hunt for something, first check whether a Niro tool answers it (code_grep for content search, resolve_fqn and get_source_code for a known symbol, find_handler and find_api_endpoints for entry points). Use the connection tools (get_call_chain, get_reverse_call_chain, get_blast_radius, get_impact_analysis, get_service_topology, get_exception_flow for which exceptions can escape a method) to understand how code connects BEFORE deciding which file to read or search next, rather than reading files to rebuild that picture by hand. Native search is the fallback for what Niro does not cover (config files, docs, logs, generated output, non-code text) or when the tools are unavailable. This rule does not expire: it holds on the first turn and on the fiftieth, mid-task as much as at the start of a session, and a long conversation is not a reason to drift back to native search.";
54
54
 
55
55
  const NIRO_MULTI_REPO_SENTENCE = "The temporary project answer is per (repo, BRANCH): after you create/switch a branch in a repo — and before your first edit on that branch — call should_create_temp_project with the repo's git url, its CURRENT branch, and its `.niro-project` id if present, then run `niro new-temp-project` in its folder if needs_temp_project is true. Re-check after every branch change (a `false` from another branch does NOT carry over); this applies to every repo you touch, including when the session started in a parent folder that is not itself a git repo.";
56
- const NIRO_CLAUDE_MD_SECTION = `\n${NIRO_MD_MARKER}\n## Niro MCP\nNiro MCP tools are available for code intelligence. Use them (call chains, blast radius, impact analysis, source lookup) before falling back to native grep/read for code questions.\nBefore adding/writing any new code, you MUST use the find_reusable_code tool from niro to figure out if there is anything reusable.\n${NIRO_EXPLORE_FIRST_SENTENCE}\n${NIRO_MULTI_REPO_SENTENCE}\n`;
57
- const NIRO_CODEX_MD_SECTION = `\n${NIRO_MD_MARKER}\n## Niro MCP\nNiro MCP code-intelligence tools and the \`niro-skill\` are available (call chains, blast radius, impact analysis, source lookup). Prefer them over grep/read for questions about this codebase; the niro-skill has the full tool-routing playbook. Before adding or writing any new code, use find_reusable_code to check for reusable code. If the niro tools are not present or the repo is not indexed, use native tools.\n${NIRO_EXPLORE_FIRST_SENTENCE}\n${NIRO_MULTI_REPO_SENTENCE}\n`;
58
- const NIRO_WINDSURF_MD_SECTION = `\n${NIRO_MD_MARKER}\n## Niro MCP\nNiro MCP code-intelligence tools and the niro-skill are available. Prefer them over grep/read for questions about this codebase (structure, call chains, blast radius, impact, data flows, reuse); the niro-skill has the full routing playbook. Before writing any new code, use find_reusable_code. If the niro tools are not present or the repo is not indexed, use native tools.\n${NIRO_EXPLORE_FIRST_SENTENCE}\n${NIRO_MULTI_REPO_SENTENCE}\n`;
56
+ /**
57
+ * Impact-both-ways rule, shared by every client's markdown section. The explore-first sentence above
58
+ * governs READING code; this one governs CHANGING it. The failure it exists to stop is an agent that
59
+ * reads the one file it was pointed at, proposes an edit, and never learns which callers that edit
60
+ * breaks or which dependency constrains it. get_blast_radius is named first because it answers BOTH
61
+ * sides in a single call, so the rule costs one tool call and skipping it costs a broken caller nobody
62
+ * looked for. It is worded "before you plan or propose", not "before you edit", because once the plan
63
+ * is agreed the wrong basis is already baked in; and it says the size of the diff does not excuse it,
64
+ * because a one-line change to shared code reaches further than a rewrite of a standalone tool.
65
+ */
66
+ const NIRO_IMPACT_BOTH_WAYS_SENTENCE = "MUST FOLLOW: before you plan, propose, or start any change to existing code, establish BOTH sides of it. Upstream: what depends on the code you are about to touch, including callers in other services over HTTP or events, and the tests and config that name it. Downstream: what that code itself calls, and the data and events it relies on. Use get_blast_radius, which returns callers, callees, cross-service callers and impacted files in one call, or get_impact_analysis for the lighter upstream-only view. Put the result in the plan you show the user: name the callers you found, say which ones you will update, and say which ones you are deliberately leaving alone. This applies to a one-line change too, because how far a change reaches is decided by what depends on it, not by the size of the diff. Keep that list: after the edit, verify those flows still hold (run their tests, trace them, read the callers) and say what you verified. The graph updates only after your edit is synced and rebuilt, so do not re-query it to verify; use the list you captured before. A change that cannot alter behaviour at all (a log line, a comment, formatting) needs no map: say so in one sentence.";
67
+ const NIRO_CLAUDE_MD_SECTION = `\n${NIRO_MD_MARKER}\n## Niro MCP\nNiro MCP tools are available for code intelligence. Use them (call chains, blast radius, impact analysis, source lookup) before falling back to native grep/read for code questions.\nBefore adding/writing any new code, you MUST use the find_reusable_code tool from niro to figure out if there is anything reusable.\n${NIRO_EXPLORE_FIRST_SENTENCE}\n${NIRO_IMPACT_BOTH_WAYS_SENTENCE}\n${NIRO_MULTI_REPO_SENTENCE}\n`;
68
+ const NIRO_CODEX_MD_SECTION = `\n${NIRO_MD_MARKER}\n## Niro MCP\nNiro MCP code-intelligence tools and the \`niro-skill\` are available (call chains, blast radius, impact analysis, source lookup). Prefer them over grep/read for questions about this codebase; the niro-skill has the full tool-routing playbook. Before adding or writing any new code, use find_reusable_code to check for reusable code. If the niro tools are not present or the repo is not indexed, use native tools.\n${NIRO_EXPLORE_FIRST_SENTENCE}\n${NIRO_IMPACT_BOTH_WAYS_SENTENCE}\n${NIRO_MULTI_REPO_SENTENCE}\n`;
69
+ const NIRO_WINDSURF_MD_SECTION = `\n${NIRO_MD_MARKER}\n## Niro MCP\nNiro MCP code-intelligence tools and the niro-skill are available. Prefer them over grep/read for questions about this codebase (structure, call chains, blast radius, impact, data flows, reuse); the niro-skill has the full routing playbook. Before writing any new code, use find_reusable_code. If the niro tools are not present or the repo is not indexed, use native tools.\n${NIRO_EXPLORE_FIRST_SENTENCE}\n${NIRO_IMPACT_BOTH_WAYS_SENTENCE}\n${NIRO_MULTI_REPO_SENTENCE}\n`;
59
70
  /**
60
71
  * Shared generic section for clients whose "always-on instructions" file has no companion
61
72
  * skill mechanism we've verified (Gemini CLI's GEMINI.md, GitHub Copilot CLI's
62
73
  * copilot-instructions.md, Factory's AGENTS.md, OpenCode's AGENTS.md) — one wording, reused
63
74
  * across those destination files (teardown matches it per-file, so reuse is safe).
64
75
  */
65
- const NIRO_GENERIC_MD_SECTION = `\n${NIRO_MD_MARKER}\n## Niro MCP\nNiro MCP code-intelligence tools are available (call chains, blast radius, impact analysis, source lookup). Prefer them over grep/read for questions about this codebase. Before adding or writing any new code, use find_reusable_code to check for reusable code. If the niro tools are not present or the repo is not indexed, use native tools.\n${NIRO_EXPLORE_FIRST_SENTENCE}\n${NIRO_MULTI_REPO_SENTENCE}\n`;
76
+ const NIRO_GENERIC_MD_SECTION = `\n${NIRO_MD_MARKER}\n## Niro MCP\nNiro MCP code-intelligence tools are available (call chains, blast radius, impact analysis, source lookup). Prefer them over grep/read for questions about this codebase. Before adding or writing any new code, use find_reusable_code to check for reusable code. If the niro tools are not present or the repo is not indexed, use native tools.\n${NIRO_EXPLORE_FIRST_SENTENCE}\n${NIRO_IMPACT_BOTH_WAYS_SENTENCE}\n${NIRO_MULTI_REPO_SENTENCE}\n`;
66
77
 
67
78
  /** All niro markdown sections — the uninstaller tries each for an exact removal. */
68
79
  const NIRO_MD_SECTIONS = [NIRO_CLAUDE_MD_SECTION, NIRO_CODEX_MD_SECTION, NIRO_WINDSURF_MD_SECTION, NIRO_GENERIC_MD_SECTION];
@@ -150,6 +161,19 @@ function isNiroExploreHook(h) {
150
161
  return cmds.some((c) => c.includes("niro-explore-"));
151
162
  }
152
163
 
164
+ /**
165
+ * True if `h` is niro's PreToolUse impact-gate entry (matcher Edit|Write). Keyed on the unique
166
+ * `niro-impact-` identity token so a foreign PreToolUse hook on the same matcher, and niro's own
167
+ * temp-project hook on the same matcher, are never matched or removed by mistake. Shared by installer
168
+ * (upgrade-in-place) and uninstaller (removal).
169
+ */
170
+ function isNiroImpactHook(h) {
171
+ const cmds = [];
172
+ if (h && Array.isArray(h.hooks)) h.hooks.forEach((i) => i && typeof i.command === "string" && cmds.push(i.command));
173
+ if (h && typeof h.command === "string") cmds.push(h.command);
174
+ return cmds.some((c) => c.includes("niro-impact-"));
175
+ }
176
+
153
177
  /**
154
178
  * Writes a niro-managed skill/rule file, refreshing it when the generated content has
155
179
  * changed. These files are GENERATED (not hand-edited), so refresh-on-change is correct:
@@ -578,8 +602,49 @@ function printClients({ all = false } = {}) {
578
602
  console.log("");
579
603
  }
580
604
 
581
- async function setupClaudeCode(apiKey, mcpServerUrl) {
605
+ /** The URL an existing Claude Code registration points at, read off `claude mcp get niro`; null if none. */
606
+ function registeredServerUrl(getOutput) {
607
+ const m = /NIRO_MCP_SERVER_URL=(\S+)/.exec(String(getOutput || ""));
608
+ return m ? m[1] : null;
609
+ }
610
+
611
+ /**
612
+ * What to do about the Claude Code registration before the hooks are written. Pure, so it is tested
613
+ * with the real `claude mcp get` output as a fixture.
614
+ * "add" nothing is registered (`claude mcp get niro` failed)
615
+ * "keep" registered and already pointing at the wanted URL
616
+ * "replace" registered, but at another URL or in a shape this cannot read: remove, then add
617
+ */
618
+ function planClaudeRegistration({ existingOutput, wantedUrl }) {
619
+ if (existingOutput == null) return "add";
620
+ const url = registeredServerUrl(existingOutput);
621
+ return url && url === wantedUrl ? "keep" : "replace";
622
+ }
623
+
624
+ /**
625
+ * Register the niro server with Claude Code, or keep the registration already there, then write the
626
+ * hooks, the memory section and the skills.
627
+ *
628
+ * `claude mcp add` refuses when a server of that name exists ("MCP server niro already exists in
629
+ * user config", exit 1). This function treated every failure of that command as "Claude Code is not
630
+ * installed" and returned before writing a single hook, so on any machine where niro had been set up
631
+ * once, `niro mcp install` refreshed nothing: not the hooks, not the CLAUDE.md section. That is the
632
+ * exact step the out-of-date notice tells users to run. Found 2026-09-05 when the new Edit|Write
633
+ * hook could not be installed on the machine it was written on.
634
+ *
635
+ * `deps` is for tests only: `get` returns the `claude mcp get niro` output or null, `run` runs a
636
+ * client command; both default to the real thing.
637
+ */
638
+ async function setupClaudeCode(apiKey, mcpServerUrl, deps = {}) {
582
639
  console.log("\n Configuring Claude Code...\n");
640
+ const run = deps.run || runClientCommand;
641
+ const get = deps.get || (() => {
642
+ try {
643
+ return execSync("claude mcp get niro", { stdio: ["ignore", "pipe", "pipe"], encoding: "utf8" });
644
+ } catch (_) {
645
+ return null; // not registered, or no `claude` on the PATH: `add` below tells the two apart
646
+ }
647
+ });
583
648
 
584
649
  const cmd = [
585
650
  "claude", "mcp", "add",
@@ -593,20 +658,34 @@ async function setupClaudeCode(apiKey, mcpServerUrl) {
593
658
  cmd.push("-e", `NIRO_MCP_SERVER_URL=${mcpServerUrl}`);
594
659
  cmd.push("--", "niro", "mcp", "serve");
595
660
 
596
- try {
597
- runClientCommand(cmd.join(" "));
598
- console.log(" MCP server registered globally in Claude Code.\n");
599
- } catch (err) {
600
- console.error(" Failed to register MCP server. Is Claude Code installed?");
601
- console.error(" Install: https://claude.ai/code\n");
602
- console.error(` Manual command:\n ${cmd.join(" ")}\n`);
603
- return false;
661
+ const plan = planClaudeRegistration({ existingOutput: get(), wantedUrl: mcpServerUrl });
662
+ if (plan === "keep") {
663
+ console.log(` MCP server already registered in Claude Code at ${mcpServerUrl}. Keeping it.\n`);
664
+ } else {
665
+ if (plan === "replace") {
666
+ try {
667
+ run("claude mcp remove --scope user niro");
668
+ } catch (_) {
669
+ /* the add below reports what is wrong */
670
+ }
671
+ }
672
+ try {
673
+ run(cmd.join(" "));
674
+ console.log(" MCP server registered globally in Claude Code.\n");
675
+ } catch (err) {
676
+ const detail = String((err && (err.stderr || err.message)) || "").trim().split("\n")[0];
677
+ console.error(` Failed to register MCP server${detail ? `: ${detail}` : ""}.`);
678
+ console.error(" If Claude Code is not installed: https://claude.ai/code\n");
679
+ console.error(` Manual command:\n ${cmd.join(" ")}\n`);
680
+ return false;
681
+ }
604
682
  }
605
683
 
606
684
  addClaudeCodePostCompactHook();
607
685
  addClaudeCodeUserPromptHook();
608
686
  addClaudeCodePreToolUseHook();
609
687
  addClaudeCodeExploreHook();
688
+ addClaudeCodeImpactHook();
610
689
  removeStaleNiroStopHook();
611
690
  addClaudeCodeMemory();
612
691
  addClaudeCodeSkill();
@@ -620,9 +699,59 @@ async function setupClaudeCode(apiKey, mcpServerUrl) {
620
699
  * about Niro tools after context compaction (the point where tool awareness
621
700
  * most commonly degrades in long sessions).
622
701
  */
702
+ /**
703
+ * The opening of every PostCompact command niro has ever generated. Verified against all four commits
704
+ * that ever touched the string (ffeb4e0, ce017c7, d3ff3d8, 84e2e7b): the generated text has always been
705
+ * byte-identical and has always started here.
706
+ */
707
+ const NIRO_POST_COMPACT_PREFIX = "echo 'Niro MCP tools are available for code intelligence";
708
+
709
+ /**
710
+ * Narrow identity for the DESTRUCTIVE half of the upgrade path: did NIRO ITSELF generate this entry?
711
+ *
712
+ * isNiroPostCompactHook matches the bare substring "Niro MCP", which is the right question for "is a
713
+ * niro hook already here" and the WRONG one for "may I delete this entry". A hook the USER wrote that
714
+ * merely mentions Niro MCP (a reminder about a Niro MCP dashboard, say) matches that substring too, and
715
+ * the filter below would delete their hook and put ours in its place. Matching the generated prefix
716
+ * instead recognises every real niro install while leaving anything a human wrote alone.
717
+ *
718
+ * Deliberately narrower than isNiroPostCompactHook, and the asymmetry is the point: a false negative
719
+ * here appends a second niro entry (noisy, recoverable), a false positive destroys a user's hook.
720
+ * Handles both entry shapes, the nested `hooks` array and the legacy flat `command`.
721
+ */
722
+ function isNiroGeneratedPostCompactHook(h) {
723
+ if (!h || typeof h !== "object") return false;
724
+ const commands = Array.isArray(h.hooks) ? h.hooks.map((i) => i && i.command) : [h.command];
725
+ return commands.some((c) => typeof c === "string" && c.startsWith(NIRO_POST_COMPACT_PREFIX));
726
+ }
727
+
728
+ /**
729
+ * The PostCompact reminder. Compaction is the moment tool awareness degrades in a long session, so this
730
+ * is the one reminder that fires precisely when the agent has just lost the plan it made earlier — which
731
+ * is exactly when it is most likely to resume editing without re-checking who calls the code. It carries
732
+ * the impact rule for that reason. Kept as a named constant (like the other hook bodies) so the
733
+ * regression test can assert on the real string the installer writes.
734
+ *
735
+ * MUST keep the literal "Niro MCP": isNiroPostCompactHook identifies niro entries by that substring, and
736
+ * the uninstaller shares the predicate. Dropping it would strand the hook in a user's settings.json.
737
+ */
738
+ const NIRO_POST_COMPACT_COMMAND =
739
+ "echo 'Niro MCP tools are available for code intelligence (call chains, blast radius, impact analysis, source lookup). "
740
+ + "Use niro tools before native grep/read for code questions. Before you plan or propose a change to existing code, call "
741
+ + "get_blast_radius (or get_impact_analysis) to see who calls it and what it calls, and put that impact in the plan. "
742
+ + "Keep that list and verify those flows after the edit; the graph lags your edit, so do not re-query it to verify. "
743
+ + "A log line or comment needs no map.'";
744
+
623
745
  function addClaudeCodePostCompactHook() {
746
+ // Guard the invariant that keeps upgrade-in-place working: if the reminder is ever reworded so it
747
+ // no longer starts with the generated prefix, every install would append a fresh entry instead of
748
+ // replacing the old one. Fail loudly at the boundary rather than quietly stacking hooks.
749
+ if (!NIRO_POST_COMPACT_COMMAND.startsWith(NIRO_POST_COMPACT_PREFIX)) {
750
+ throw new Error("NIRO_POST_COMPACT_COMMAND must start with NIRO_POST_COMPACT_PREFIX, or upgrade-in-place breaks");
751
+ }
752
+
624
753
  const settingsPath = path.join(process.env.HOME || "", ".claude", "settings.json");
625
- const hookCommand = "echo 'Niro MCP tools are available for code intelligence (call chains, blast radius, impact analysis, source lookup). Use niro tools before native grep/read for code questions.'";
754
+ const hookCommand = NIRO_POST_COMPACT_COMMAND;
626
755
 
627
756
  let settings = {};
628
757
  try {
@@ -636,13 +765,21 @@ function addClaudeCodePostCompactHook() {
636
765
  if (!settings.hooks) settings.hooks = {};
637
766
  if (!settings.hooks.PostCompact) settings.hooks.PostCompact = [];
638
767
 
639
- const alreadyPresent = settings.hooks.PostCompact.some(isNiroPostCompactHook);
640
-
641
- if (alreadyPresent) {
642
- console.log(" PostCompact hook already configured. Skipping.\n");
768
+ // Exact-match the CURRENT command first: only then is there nothing to do. Matching the loose niro
769
+ // signature instead (the old behaviour) meant an existing user with the previous reminder text was
770
+ // skipped forever and never received an updated one.
771
+ const alreadyCurrent = settings.hooks.PostCompact.some(
772
+ (h) => Array.isArray(h.hooks) && h.hooks.some((i) => i && i.command === hookCommand),
773
+ );
774
+ if (alreadyCurrent) {
775
+ console.log(" PostCompact hook already up to date. Skipping.\n");
643
776
  return;
644
777
  }
645
778
 
779
+ // Narrow predicate on purpose: this REMOVES entries from the user's settings.json, so it must only
780
+ // ever match hooks niro generated, never a user hook that happens to mention Niro MCP.
781
+ const hadStale = settings.hooks.PostCompact.some(isNiroGeneratedPostCompactHook);
782
+ settings.hooks.PostCompact = settings.hooks.PostCompact.filter((h) => !isNiroGeneratedPostCompactHook(h));
646
783
  settings.hooks.PostCompact.push({
647
784
  matcher: "",
648
785
  hooks: [{ type: "command", command: hookCommand }],
@@ -652,7 +789,7 @@ function addClaudeCodePostCompactHook() {
652
789
  try {
653
790
  fs.mkdirSync(path.dirname(settingsPath), { recursive: true });
654
791
  fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + "\n");
655
- console.log(" PostCompact hook added to ~/.claude/settings.json\n");
792
+ console.log(` PostCompact hook ${hadStale ? "upgraded" : "added"} in ~/.claude/settings.json\n`);
656
793
  } catch (err) {
657
794
  console.warn(` Could not write ~/.claude/settings.json: ${err.message}`);
658
795
  console.warn(" Add manually to the PostCompact array in ~/.claude/settings.json:\n");
@@ -714,7 +851,7 @@ function addClaudeCodeMemory() {
714
851
  */
715
852
  const NIRO_USER_PROMPT_COMMAND =
716
853
  "IN=\"$(cat)\"; d=\"${CLAUDE_PROJECT_DIR:-$PWD}\"; while [ -n \"$d\" ] && [ \"$d\" != \"/\" ]; do " +
717
- "if [ -f \"$d/.niro-project\" ]; then echo 'NIRO ACTIVE. Standing rule for this whole session, at the start and mid-task alike: before Grep/Glob/Read or any native file hunt, use niro (code_grep, get_source_code, resolve_fqn, get_call_chain, get_blast_radius, get_impact_analysis). Before writing new code, call find_reusable_code. Native tools only for config, docs, logs, generated or non-code text. If the niro tools are not in your toolset at all, use native tools and do not mention Niro.'; " +
854
+ "if [ -f \"$d/.niro-project\" ]; then echo 'NIRO ACTIVE. Standing rule for this whole session, at the start and mid-task alike: before Grep/Glob/Read or any native file hunt, use niro (code_grep, get_source_code, resolve_fqn, get_call_chain, get_blast_radius, get_impact_analysis). Before writing new code, call find_reusable_code. Before you plan or propose a change to existing code, call get_blast_radius (or get_impact_analysis) to see who calls it and what it calls, and put that impact in the plan. Keep that list and verify those flows after the edit; the graph lags your edit, so do not re-query it to verify. A log line or comment needs no map. Native tools only for config, docs, logs, generated or non-code text. If the niro tools are not in your toolset at all, use native tools and do not mention Niro.'; " +
718
855
  "printf '%s' \"$IN\" | grep -qiE 'new branch|create[^.]{0,24}branch|switch[^.]{0,24}branch|checkout -b|change[^.]{0,24}branch|different branch|another branch' && " +
719
856
  "echo 'You appear to be about to create or switch a branch. The temporary project check is per (repo, branch), so do it AFTER you are on the new branch (not before) and pass that CURRENT branch: call should_create_temp_project(git_url, branch, project_id from its .niro-project) and run niro new-temp-project if needs_temp_project is true. A check you ran on the old branch does not count.'; break; fi; " +
720
857
  "d=\"$(dirname \"$d\")\"; done";
@@ -725,7 +862,51 @@ const NIRO_USER_PROMPT_COMMAND =
725
862
  * always-on pointer that fixes "Claude forgets Niro mid-session"; the long-form
726
863
  * guidance lives in the skill, so this is deliberately terse.
727
864
  */
865
+ /**
866
+ * Openings of every main reminder the UserPromptSubmit hook has ever written, current first. Verified
867
+ * across every commit that touched the hook (`git log --all -S"UserPromptSubmit" -- src/mcp/setup.js`,
868
+ * 9 commits): the payload has had exactly these two forms. Long, machine-written sentences on purpose —
869
+ * they are what makes "niro wrote this" a safe question to answer.
870
+ */
871
+ const NIRO_USER_PROMPT_GENERATED_MARKERS = [
872
+ "NIRO ACTIVE. Standing rule for this whole session",
873
+ "This repo is indexed by Niro. IF the niro MCP tools are present in your current toolset",
874
+ ];
875
+
876
+ /**
877
+ * Narrow identity for the DESTRUCTIVE half of the UserPromptSubmit upgrade path: did NIRO ITSELF write
878
+ * this entry?
879
+ *
880
+ * isNiroUserPromptHook answers "is a niro hook already here" by looking for `.niro-project`,
881
+ * `find_reusable_code` or "`niro` skill" anywhere in the command. That is the right question for
882
+ * detection and the wrong one for deletion: a hook the USER wrote that mentions find_reusable_code (an
883
+ * ordinary thing to put in your own prompt reminder) matches it, and the filter below would delete it.
884
+ * This matters now in a way it did not before: the installer only reaches the filter when the reminder
885
+ * text has CHANGED, so every reword of the reminder runs this delete across every existing user at once.
886
+ *
887
+ * Matching a generated payload marker instead recognises both historical niro versions while leaving
888
+ * anything a human wrote alone. An unknown older variant would be a false negative: a duplicate entry,
889
+ * noisy and recoverable, and still removed by the broad predicate at uninstall. That asymmetry is the
890
+ * whole point — a false positive here destroys a user's hook.
891
+ *
892
+ * Same failure this repo already reverted a feature over (84e2e7b: "the niro matchers are substring
893
+ * tests on prose, so a user hook merely mentioning find_reusable_code was captured and overwritten").
894
+ */
895
+ function isNiroGeneratedUserPromptHook(h) {
896
+ if (!h || typeof h !== "object") return false;
897
+ const commands = Array.isArray(h.hooks) ? h.hooks.map((i) => i && i.command) : [h.command];
898
+ return commands.some(
899
+ (c) => typeof c === "string" && NIRO_USER_PROMPT_GENERATED_MARKERS.some((marker) => c.includes(marker)),
900
+ );
901
+ }
902
+
728
903
  function addClaudeCodeUserPromptHook() {
904
+ // Guard the invariant that keeps upgrade-in-place working: if the reminder is reworded so it no longer
905
+ // carries a known generated marker, every install would append a fresh entry instead of replacing the
906
+ // old one. Fail loudly at the boundary rather than quietly stacking hooks in the user's settings.
907
+ if (!NIRO_USER_PROMPT_GENERATED_MARKERS.some((m) => NIRO_USER_PROMPT_COMMAND.includes(m))) {
908
+ throw new Error("NIRO_USER_PROMPT_COMMAND must contain a known generated marker, or upgrade-in-place breaks");
909
+ }
729
910
  const settingsPath = path.join(process.env.HOME || "", ".claude", "settings.json");
730
911
  const hookCommand = NIRO_USER_PROMPT_COMMAND;
731
912
 
@@ -751,8 +932,10 @@ function addClaudeCodeUserPromptHook() {
751
932
  return;
752
933
  }
753
934
 
754
- const hadStale = settings.hooks.UserPromptSubmit.some(isNiroUserPromptHook);
755
- settings.hooks.UserPromptSubmit = settings.hooks.UserPromptSubmit.filter((h) => !isNiroUserPromptHook(h));
935
+ // Narrow predicate on purpose: this REMOVES entries from the user's settings.json, so it must only
936
+ // ever match hooks niro generated, never a user hook that happens to mention find_reusable_code.
937
+ const hadStale = settings.hooks.UserPromptSubmit.some(isNiroGeneratedUserPromptHook);
938
+ settings.hooks.UserPromptSubmit = settings.hooks.UserPromptSubmit.filter((h) => !isNiroGeneratedUserPromptHook(h));
756
939
  // UserPromptSubmit hooks have no tool matcher — just the hooks array.
757
940
  settings.hooks.UserPromptSubmit.push({
758
941
  hooks: [{ type: "command", command: hookCommand }],
@@ -989,6 +1172,165 @@ function addClaudeCodeExploreHook() {
989
1172
  }
990
1173
  }
991
1174
 
1175
+ /**
1176
+ * The three answers the impact hook can give. Built with JSON.stringify so the text is valid JSON by
1177
+ * construction. All three are wrapped in single quotes inside the shell command, so no message may
1178
+ * contain an apostrophe or a backtick; the test suite executes the command, which would break loudly
1179
+ * if one crept in. None carries a permissionDecision: the hook advises, it never approves or refuses.
1180
+ *
1181
+ * Why advisory and not a refusal, in Niraj's words (2026-09-05): "The reason I said before editing
1182
+ * any method, the impact of editing it should be measured is because I want the coding agent to verify
1183
+ * that after the change as well, the flows connected with the current method are not breaking. [...]
1184
+ * graph updating takes time after an edit is performed. The change does not take effect instantly.
1185
+ * [...] it is not to enforce use of niro [...] now if someone is just adding a log message, that's not
1186
+ * gonna affect anything so if the verification should happen or not is also subjective."
1187
+ */
1188
+ const NIRO_IMPACT_UNMAPPED_JSON = JSON.stringify({
1189
+ hookSpecificOutput: {
1190
+ hookEventName: "PreToolUse",
1191
+ additionalContext:
1192
+ "Niro impact check, before you edit this file: decide whether this change can alter behaviour. " +
1193
+ "A log line, a comment, formatting or a message string cannot, so say so in one sentence and go " +
1194
+ "ahead. Anything else can, so map who depends on the method NOW with get_blast_radius or " +
1195
+ "get_impact_analysis (a partial name such as ClassName.method resolves when it is unique; " +
1196
+ "resolve_fqn on one that does not) and keep the list of callers and flows: it is your checklist " +
1197
+ "for AFTER the edit. The graph updates only after your edit is synced and rebuilt, so do not " +
1198
+ "re-query it to verify; verify the listed flows by running their tests, tracing them, or reading " +
1199
+ "the callers, and say what you verified. Shown once per file per session.",
1200
+ },
1201
+ });
1202
+ const NIRO_IMPACT_MAPPED_JSON = JSON.stringify({
1203
+ hookSpecificOutput: {
1204
+ hookEventName: "PreToolUse",
1205
+ additionalContext:
1206
+ "Niro impact check: an impact map for this file was recorded in the last hour. After this edit, " +
1207
+ "go back to the callers and flows it listed and verify they still hold: run their tests, trace " +
1208
+ "them, or read the callers, and say what you verified. The graph updates only after your edit is " +
1209
+ "synced and rebuilt, so a fresh query would still show the old code; use the list you already " +
1210
+ "have. Shown once per file per session.",
1211
+ },
1212
+ });
1213
+ const NIRO_IMPACT_NO_BRIDGE_JSON = JSON.stringify({
1214
+ hookSpecificOutput: {
1215
+ hookEventName: "PreToolUse",
1216
+ additionalContext:
1217
+ "Niro impact check: you are editing indexed source with no impact map recorded for this file, " +
1218
+ "and the Niro bridge has not answered in the last 30 minutes. If this change can alter behaviour " +
1219
+ "(anything beyond a log line, a comment, formatting or a message string), map who depends on it " +
1220
+ "as soon as Niro answers and verify those flows after the edit; the graph will lag your edit, so " +
1221
+ "use the list you capture, not a fresh query. Shown once per file per session.",
1222
+ },
1223
+ });
1224
+
1225
+ /**
1226
+ * The niro PreToolUse hook command (matcher Edit|Write): before an edit to indexed source, make the
1227
+ * agent decide out loud whether the change can alter behaviour and, if so, map who depends on the
1228
+ * method while the graph is still current, then carry that list across the edit as the checklist for
1229
+ * afterwards. The prose rule (NIRO_IMPACT_BOTH_WAYS_SENTENCE) is on every instruction surface and
1230
+ * still gets read past; a reminder at the moment of the edit is what survives a long session.
1231
+ * Full design, the reasoning in Niraj's words, and the scenario table: docs/usecases/impact-gate.md.
1232
+ *
1233
+ * Advisory, never a refusal. The first cut denied the edit until an impact call named the file. That
1234
+ * enforced a tool call, which was never the goal; it could not tell a log line from a signature change;
1235
+ * and review found two ways it failed closed. Now every answer is additionalContext, and it never emits
1236
+ * permissionDecision at all ("allow" would auto-approve every edit and bypass the permission prompt).
1237
+ *
1238
+ * The evidence comes from the bridge: src/lib/impactLog.js writes one JSON line per answered
1239
+ * get_blast_radius / get_impact_analysis / get_reverse_call_chain call to ~/.niro/impact/<key>.jsonl,
1240
+ * and touches <key>.alive whenever the server answers. <key> is the session folder with every
1241
+ * non-alphanumeric byte replaced by "-": here `cd -P` + `tr -c`, there fs.realpathSync + a byte loop,
1242
+ * and test/impactGateHook.test.js proves the two agree.
1243
+ *
1244
+ * Decision, top to bottom, first match wins: no file_path, the off switch, a file that does not exist
1245
+ * yet, a non-source extension, a test path, or no `.niro-project` above the file -> silent. A log line
1246
+ * from the last hour that names the file (a files entry ending "/Foo.java", or a name containing
1247
+ * ".Foo.", "\"Foo.", ".Foo\"", "\"Foo\"," or "\"Foo\"]": always a VALUE, so a file called ts.js or
1248
+ * tool.js is not matched by the record's own "ts"/"tool" keys) -> the MAPPED reminder: verify the
1249
+ * listed flows after the edit, do not re-query the lagging graph. Otherwise, alive file touched in the
1250
+ * last 30 minutes -> the UNMAPPED reminder: decide, and map now if the change can alter behaviour; no
1251
+ * live bridge -> the short NO_BRIDGE form. Each reminder is shown once per (session, file), keyed like
1252
+ * the temp-project hook's marker, so ten edits to one file do not produce ten copies.
1253
+ *
1254
+ * Dependency-free like its siblings (grep -oE, sed -E, tr, find -mmin, cksum: GNU + BSD).
1255
+ * `niro-impact-` is the identity token isNiroImpactHook and teardown match on.
1256
+ */
1257
+ const NIRO_IMPACT_COMMAND = [
1258
+ 'IN="$(cat)"',
1259
+ `f="$(printf '%s' "$IN" | grep -oE '"file_path"[[:space:]]*:[[:space:]]*"[^"]*"' | head -1 | sed -E 's/.*:[[:space:]]*"([^"]*)"/\\1/')"`,
1260
+ '[ -z "$f" ] && exit 0',
1261
+ `c="$(printf '%s' "$IN" | grep -oE '"cwd"[[:space:]]*:[[:space:]]*"[^"]*"' | head -1 | sed -E 's/.*:[[:space:]]*"([^"]*)"/\\1/')"`,
1262
+ '[ -z "$c" ] && c="${CLAUDE_PROJECT_DIR:-$PWD}"',
1263
+ 'case "$f" in /*) ;; *) f="$c/$f" ;; esac',
1264
+ 'H="${HOME:-/tmp}"',
1265
+ '[ -f "$H/.niro/impact-gate-off" ] && exit 0',
1266
+ // A file that does not exist yet has no callers; a Write creating it is never gated.
1267
+ '[ -f "$f" ] || exit 0',
1268
+ 'case "$f" in *.java|*.js|*.mjs|*.cjs|*.ts|*.tsx|*.jsx|*.py|*.go|*.rb|*.kt|*.kts|*.cs|*.php|*.rs|*.scala|*.swift|*.c|*.cc|*.cpp|*.h|*.hpp) ;; *) exit 0 ;; esac',
1269
+ 'case "$f" in */test/*|*/tests/*|*/__tests__/*|*/spec/*|*.test.*|*.spec.*|*Test.*|*Tests.*|*IT.java|*_test.*|*/test_*|*_spec.*|*/conftest.py) exit 0 ;; esac',
1270
+ // Symlink-resolved walk-up for the .niro-project marker, same as the explore hook.
1271
+ `d0="$(dirname "$f")"; d0="$(cd -P "$d0" 2>/dev/null && pwd -P || printf '%s' "$d0")"`,
1272
+ 'd="$d0"; ok=0; while [ -n "$d" ] && [ "$d" != "/" ] && [ "$d" != "." ]; do if [ -f "$d/.niro-project" ]; then ok=1; break; fi; p="$(dirname "$d")"; [ "$p" = "$d" ] && break; d="$p"; done',
1273
+ '[ "$ok" = 0 ] && exit 0',
1274
+ // The log key, byte for byte what impactLog.keyForCwd computes from the bridge's process.cwd().
1275
+ `cr="$(cd -P "$c" 2>/dev/null && pwd -P || printf '%s' "$c")"`,
1276
+ `k="$(printf '%s' "$cr" | tr -c 'A-Za-z0-9' '-')"`,
1277
+ 'L="$H/.niro/impact/$k.jsonl"; A="$H/.niro/impact/$k.alive"',
1278
+ 'b="$(basename "$f")"; n="${b%.*}"',
1279
+ // The session + file key for the once-per-file markers, hashed the way the temp-project hook does.
1280
+ `s="$(printf '%s' "$IN" | grep -oE '"session_id"[[:space:]]*:[[:space:]]*"[^"]*"' | head -1 | sed -E 's/.*:[[:space:]]*"([^"]*)"/\\1/')"`,
1281
+ `j="$(printf '%s\\n%s' "$s" "$f" | cksum | cut -d' ' -f1)"`,
1282
+ 'NIRO_HOOK_ID=niro-impact-gate',
1283
+ // Mapped: the log was written in the last hour (the bridge prunes on write, so an older file holds
1284
+ // only stale lines) and a line names this file or its class -> remind once to verify AFTER the edit.
1285
+ `if [ -f "$L" ] && [ -n "$(find "$L" -mmin -60 2>/dev/null)" ] && grep -qF -e "/$b\\"" -e ".$n." -e "\\"$n." -e ".$n\\"" -e "\\"$n\\"," -e "\\"$n\\"]" "$L" 2>/dev/null; then m="\${TMPDIR:-/tmp}/niro-impact-after-$j"; [ -f "$m" ] && exit 0; touch "$m"; printf '%s' '${NIRO_IMPACT_MAPPED_JSON}'; exit 0; fi`,
1286
+ // Unmapped: ask once per (session, file) for the decision; with no live bridge, the short form.
1287
+ 'm="${TMPDIR:-/tmp}/niro-impact-before-$j"; [ -f "$m" ] && exit 0; touch "$m"',
1288
+ `if [ -f "$A" ] && [ -n "$(find "$A" -mmin -30 2>/dev/null)" ]; then printf '%s' '${NIRO_IMPACT_UNMAPPED_JSON}'; else printf '%s' '${NIRO_IMPACT_NO_BRIDGE_JSON}'; fi`,
1289
+ ].join('; ');
1290
+
1291
+ /**
1292
+ * Adds the PreToolUse (Edit|Write) impact-gate hook to ~/.claude/settings.json. Idempotent: upgrades a
1293
+ * stale niro entry in place, never touches a foreign PreToolUse hook on the same matcher nor niro's own
1294
+ * temp-project hook (matched by the niro-impact- token via isNiroImpactHook).
1295
+ */
1296
+ function addClaudeCodeImpactHook() {
1297
+ const settingsPath = path.join(process.env.HOME || "", ".claude", "settings.json");
1298
+ let settings = {};
1299
+ try {
1300
+ if (fs.existsSync(settingsPath)) settings = JSON.parse(fs.readFileSync(settingsPath, "utf-8"));
1301
+ } catch {
1302
+ // Corrupt or missing — start fresh
1303
+ }
1304
+
1305
+ if (!settings.hooks) settings.hooks = {};
1306
+ if (!settings.hooks.PreToolUse) settings.hooks.PreToolUse = [];
1307
+
1308
+ const entry = { matcher: "Edit|Write", hooks: [{ type: "command", command: NIRO_IMPACT_COMMAND }] };
1309
+ const alreadyCurrent = settings.hooks.PreToolUse.some(
1310
+ (h) => Array.isArray(h.hooks) && h.hooks.some((i) => i && i.command === NIRO_IMPACT_COMMAND),
1311
+ );
1312
+ if (alreadyCurrent) {
1313
+ console.log(" PreToolUse impact-gate hook already up to date. Skipping.\n");
1314
+ return;
1315
+ }
1316
+
1317
+ const hadStale = settings.hooks.PreToolUse.some(isNiroImpactHook);
1318
+ settings.hooks.PreToolUse = settings.hooks.PreToolUse.filter((h) => !isNiroImpactHook(h));
1319
+ settings.hooks.PreToolUse.push(entry);
1320
+
1321
+ try {
1322
+ fs.mkdirSync(path.dirname(settingsPath), { recursive: true });
1323
+ fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + "\n");
1324
+ console.log(` PreToolUse impact-gate hook ${hadStale ? "upgraded" : "added"} in ~/.claude/settings.json`);
1325
+ console.log(" Before an edit to indexed source, Claude is asked to map who depends on the method and to");
1326
+ console.log(" verify those flows after the edit. Advisory only. Off switch: touch ~/.niro/impact-gate-off\n");
1327
+ } catch (err) {
1328
+ console.warn(` Could not write ~/.claude/settings.json: ${err.message}`);
1329
+ console.warn(" Add manually to the PreToolUse array in ~/.claude/settings.json:\n");
1330
+ console.warn(JSON.stringify(entry, null, 2) + "\n");
1331
+ }
1332
+ }
1333
+
992
1334
  /** Body of the `niro` skill (long-form tool-routing playbook). */
993
1335
  const NIRO_SKILL = `---
994
1336
  name: niro-skill
@@ -1016,6 +1358,31 @@ stays the fallback for what Niro does not cover, and when the tools are unavaila
1016
1358
  \`project_id\` auto-resolves from a \`.niro-project\` file at the repo root — omit it. If there
1017
1359
  is no such file, call \`list_projects\` and pass the chosen \`project_id\`.
1018
1360
 
1361
+ ## Check both directions before you change anything (MUST FOLLOW)
1362
+
1363
+ Reading the file you are about to edit tells you what it does, not what it holds up. Before you
1364
+ plan, propose, or start any change to existing code, establish both sides:
1365
+
1366
+ - **Upstream: what depends on this?** Callers in this service, callers in other services over HTTP
1367
+ or events, and the tests and config that name it. This is what your change can break.
1368
+ - **Downstream: what does this depend on?** What it calls, the collections and tables it touches,
1369
+ the events it publishes. This is what constrains the change.
1370
+
1371
+ \`get_blast_radius\` answers both sides in ONE call (callers, callees, cross-service callers and
1372
+ impacted files). \`get_impact_analysis\` is the lighter upstream-only view. Pass
1373
+ \`include_source_code=true\` when you need to read what you found.
1374
+
1375
+ Keep the list. After the edit, go back to the callers and flows it named and verify they still
1376
+ hold: run their tests, trace them, read the callers, and say what you verified. The graph updates
1377
+ only after your edit is synced and rebuilt, so do not re-query it to verify; use the list you
1378
+ captured before. A change that cannot alter behaviour at all (a log line, a comment, formatting)
1379
+ needs no map: say so in one sentence.
1380
+
1381
+ Put the answer in the plan you show the user, not just in your head: name the callers you found,
1382
+ say which ones you will update, and say which ones you are deliberately leaving alone. A one-line
1383
+ change is not exempt, because reach is decided by what depends on the code, not by the size of the
1384
+ diff.
1385
+
1019
1386
  ## Availability self-check — read FIRST
1020
1387
 
1021
1388
  Niro is not always applicable. Check these before relying on it, and fall back to native
@@ -1167,6 +1534,10 @@ Also available (catalog / utility, use when the above don't fit): \`list_project
1167
1534
  ## Hard rules
1168
1535
 
1169
1536
  - **Before adding or writing ANY new code, call \`find_reusable_code\` first.** Mandatory.
1537
+ - **Before you plan or propose a change to existing code, call \`get_blast_radius\` (or
1538
+ \`get_impact_analysis\`) and put the upstream and downstream impact in the plan.** Mandatory,
1539
+ whatever the size of the change. Keep the list and verify those flows after the edit; the graph
1540
+ lags your edit, so do not re-query it to verify. A log line or comment needs no map.
1170
1541
  - For comprehension / debugging, set \`purpose\` to COMPREHEND or DEBUG and, on
1171
1542
  \`get_blast_radius\` / \`get_impact_analysis\`, pass \`include_source_code=true\`.
1172
1543
  - If a result contains **⚠️ ACTION REQUIRED**, the graph is stale — read the listed files
@@ -1419,6 +1790,17 @@ get_reverse_call_chain, get_blast_radius, get_impact_analysis, get_service_topol
1419
1790
  how the code connects BEFORE deciding which file to read or search next. Native search is the
1420
1791
  fallback for what niro does not cover, and when the tools are unavailable.
1421
1792
 
1793
+ MUST FOLLOW (check both directions before changing anything): before you plan, propose or start
1794
+ any change to existing code, establish who depends on it (upstream callers, including other
1795
+ services over HTTP or events) and what it depends on (downstream callees, data, events). Use
1796
+ get_blast_radius, which returns both sides plus impacted files in one call, or
1797
+ get_impact_analysis for the upstream-only view. Name the callers you found in the plan and say
1798
+ which ones you will update. A one-line change is not exempt. Keep that list: after the edit, verify
1799
+ those flows still hold (run their tests, trace them, read the callers) and say what you verified.
1800
+ The graph updates only after your edit is synced and rebuilt, so do not re-query it to verify. A
1801
+ change that cannot alter behaviour at all (a log line, a comment, formatting) needs no map: say so
1802
+ in one sentence.
1803
+
1422
1804
  Availability self-check FIRST — if the niro tools are not present in this session, or the
1423
1805
  repo is not indexed (no project resolves / a tool reports "not indexed"), fall back to
1424
1806
  native tools silently and do not mention Niro.
@@ -1882,6 +2264,17 @@ something, check whether a niro tool answers it (\`code_grep\`, \`resolve_fqn\`
1882
2264
  \`get_service_topology\`) to see how the code connects BEFORE deciding which file to read or
1883
2265
  search next. Native search is the fallback for what niro does not cover.
1884
2266
 
2267
+ MUST FOLLOW (check both directions before changing anything): before you plan, propose or start
2268
+ any change to existing code, establish who depends on it (upstream callers, including callers in
2269
+ other services over HTTP or events) and what it depends on (downstream callees, data, events). Use
2270
+ \`get_blast_radius\`, which returns both sides plus impacted files in one call, or
2271
+ \`get_impact_analysis\` for the upstream-only view. Name the callers you found in the plan and say
2272
+ which ones you will update. A one-line change is not exempt. Keep that list: after the edit, verify
2273
+ those flows still hold (run their tests, trace them, read the callers) and say what you verified.
2274
+ The graph updates only after your edit is synced and rebuilt, so do not re-query it to verify. A
2275
+ change that cannot alter behaviour at all (a log line, a comment, formatting) needs no map: say so
2276
+ in one sentence.
2277
+
1885
2278
  If the niro tools are not present in this session, or the repo is not indexed (no project
1886
2279
  resolves), fall back to native tools silently — do not mention Niro's absence.
1887
2280
  `;
@@ -1927,6 +2320,17 @@ something, check whether a niro tool answers it (\`code_grep\`, \`resolve_fqn\`
1927
2320
  \`get_service_topology\`) to see how the code connects BEFORE deciding which file to read or
1928
2321
  search next. Native search is the fallback for what niro does not cover.
1929
2322
 
2323
+ MUST FOLLOW (check both directions before changing anything): before you plan, propose or start
2324
+ any change to existing code, establish who depends on it (upstream callers, including callers in
2325
+ other services over HTTP or events) and what it depends on (downstream callees, data, events). Use
2326
+ \`get_blast_radius\`, which returns both sides plus impacted files in one call, or
2327
+ \`get_impact_analysis\` for the upstream-only view. Name the callers you found in the plan and say
2328
+ which ones you will update. A one-line change is not exempt. Keep that list: after the edit, verify
2329
+ those flows still hold (run their tests, trace them, read the callers) and say what you verified.
2330
+ The graph updates only after your edit is synced and rebuilt, so do not re-query it to verify. A
2331
+ change that cannot alter behaviour at all (a log line, a comment, formatting) needs no map: say so
2332
+ in one sentence.
2333
+
1930
2334
  If the niro tools are not present in this session, or the repo is not indexed (no project
1931
2335
  resolves), fall back to native tools silently — do not mention Niro's absence.
1932
2336
  `;
@@ -2385,6 +2789,11 @@ module.exports = {
2385
2789
  isNiroUserPromptHook,
2386
2790
  isNiroPreToolUseHook,
2387
2791
  isNiroExploreHook,
2792
+ isNiroImpactHook,
2793
+ NIRO_IMPACT_COMMAND,
2794
+ setupClaudeCode,
2795
+ planClaudeRegistration,
2796
+ registeredServerUrl,
2388
2797
  isNiroStopHook,
2389
2798
  clearCodexPerToolApprovals,
2390
2799
  hardenCodexServerConfig,
@@ -2394,11 +2803,20 @@ module.exports = {
2394
2803
  NIRO_EXPLORE_COMMAND,
2395
2804
  NIRO_PRETOOL_COMMAND,
2396
2805
  NIRO_USER_PROMPT_COMMAND,
2806
+ NIRO_POST_COMPACT_COMMAND,
2807
+ // Exposed for tests: the stale-hook upgrade path is only observable by running the real installer.
2808
+ addClaudeCodePostCompactHook,
2809
+ isNiroGeneratedPostCompactHook,
2810
+ isNiroGeneratedUserPromptHook,
2811
+ NIRO_USER_PROMPT_GENERATED_MARKERS,
2812
+ addClaudeCodeUserPromptHook,
2813
+ NIRO_POST_COMPACT_PREFIX,
2397
2814
  // Exposed for content regression tests (e.g. the cross-project routing guidance).
2398
2815
  NIRO_SKILL,
2399
2816
  NIRO_CLI_SKILL,
2400
2817
  // Exposed for tests: the explore-first rule must reach EVERY client's always-on instruction surface.
2401
2818
  NIRO_EXPLORE_FIRST_SENTENCE,
2819
+ NIRO_IMPACT_BOTH_WAYS_SENTENCE,
2402
2820
  NIRO_CURSOR_RULE,
2403
2821
  NIRO_CLINE_RULE,
2404
2822
  NIRO_KIRO_STEERING,