@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/CHANGELOG.md +164 -0
- package/README.md +81 -4
- package/docs/working-on-branches.md +63 -0
- package/package.json +1 -1
- package/src/commands/_resolveProject.js +59 -3
- package/src/commands/cleanup.js +689 -0
- package/src/commands/delete.js +16 -1
- package/src/commands/info.js +51 -2
- package/src/commands/release.js +13 -60
- package/src/commands/remove.js +13 -0
- package/src/commands/takeover.js +52 -2
- package/src/commands/watch.js +93 -25
- package/src/index.js +23 -3
- package/src/lib/folderSync.js +7 -0
- package/src/lib/impactLog.js +251 -0
- package/src/lib/periodicClaim.js +100 -0
- package/src/lib/printError.js +46 -0
- package/src/lib/receiptStore.js +1 -1
- package/src/lib/takeoverRestore.js +156 -0
- package/src/mcp/server.js +30 -10
- package/src/mcp/setup.js +441 -23
- package/src/mcp/teardown.js +7 -2
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
|
-
|
|
57
|
-
|
|
58
|
-
|
|
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
|
-
|
|
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
|
-
|
|
597
|
-
|
|
598
|
-
console.log(
|
|
599
|
-
}
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
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 =
|
|
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
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
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(
|
|
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
|
-
|
|
755
|
-
|
|
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,
|