From 1b4e3f0a81a71de5addc91adc9c08b1e7832965c Mon Sep 17 00:00:00 2001 From: Parafee41 Date: Wed, 14 Oct 2026 15:53:15 +0800 Subject: [PATCH] fix: use merge-base in regression review guidance (#3514) --- AGENTS.md | 104 ++++++------- CLAUDE.md | 55 +++---- gitnexus/src/cli/ai-context.ts | 69 +++++++-- gitnexus/src/cli/analyze.ts | 2 +- gitnexus/test/unit/ai-context.test.ts | 202 +++++++++++++++++++++++--- 5 files changed, 320 insertions(+), 112 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 42a9340b7..023198e54 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -7,12 +7,12 @@ Last reviewed: 2026-09-24 ## Scope -| Boundary | Rule | -|----------|------| -| **Reads** | `gitnexus/`, `gitnexus-web/`, `eval/`, plugin packages, `.github/`, `.gitnexus/`, docs. | -| **Writes** | Only paths required for the change; keep diffs minimal. Update lockfiles when deps change. | -| **Executes** | `npm`, `npx`, `node` under `gitnexus/` and `gitnexus-web/`; `uv run` for Python under `eval/`; documented CI/dev workflows. | -| **Off-limits** | Real `.env` / secrets, production credentials, unrelated repos, destructive git ops without confirmation. | +| Boundary | Rule | +| -------------- | --------------------------------------------------------------------------------------------------------------------------- | +| **Reads** | `gitnexus/`, `gitnexus-web/`, `eval/`, plugin packages, `.github/`, `.gitnexus/`, docs. | +| **Writes** | Only paths required for the change; keep diffs minimal. Update lockfiles when deps change. | +| **Executes** | `npm`, `npx`, `node` under `gitnexus/` and `gitnexus-web/`; `uv run` for Python under `eval/`; documented CI/dev workflows. | +| **Off-limits** | Real `.env` / secrets, production credentials, unrelated repos, destructive git ops without confirmation. | ## Model Configuration @@ -22,11 +22,12 @@ Last reviewed: 2026-09-24 ## Execution Sequence (complex tasks) For multi-step work, state up front: + 1. Which rules in this file and **[GUARDRAILS.md](GUARDRAILS.md)** apply (and any relevant Signs). 2. Current **Scope** boundaries. 3. Which **validation commands** you will run (`cd gitnexus && npm test`, `npx tsc --noEmit`). -On long threads, *"Remember: apply all AGENTS.md rules"* re-weights these instructions against context dilution. +On long threads, _"Remember: apply all AGENTS.md rules"_ re-weights these instructions against context dilution. ## Claude Code hooks @@ -89,30 +90,31 @@ mirror. `gitnexus/test/unit/shipped-skills-sync.test.ts` guards the copies. Toke ## Changelog -| Date | Version | Change | -|------|---------|--------| -| 2026-09-24 | 1.17.0 | Clones with the same `origin` URL now share a store automatically; `--no-share` records a lasting opt-out (#3352). | -| 2026-09-24 | 1.16.0 | Documented the shared worktree index store (`/stores/`, `analyze --share-with`, `GITNEXUS_SHARED_STORE=off`) in the storage notes (#3352). | -| 2026-09-07 | 1.15.0 | Added the Objective-C provider guide as the required reference before changing Objective-C parsing or resolution. | -| 2026-07-20 | 1.14.0 | `gitnexus-review` gains a coordinated swarm: six `ci-personas/` lanes the CI review agent dispatches as subagents (via the `Agent` tool), with a bounded critic gate and sidechain-excluded evidence. | -| 2026-07-16 | 1.13.0 | `gitnexus-plan` asks plan depth up front (quick/standard/deep) in interactive runs; `gitnexus-lfg` gate slimmed to proceed/stop (Deepen stays as the route-back mechanism). | -| 2026-07-16 | 1.12.0 | Renamed `gitnexus-pr-review` to `gitnexus-review`; added PR URL/number, branch/range, and local-change targets plus install migration (setup warns on a legacy `gitnexus-pr-review` dir and leaves it in place; uninstall removes it). | -| 2026-07-11 | 1.11.0 | Skill family shipped via npm skills/ + plugin (sync-guarded); added eval/workflow_bench token-savings benchmark. | -| 2026-07-11 | 1.10.0 | Added `gitnexus-work` (plan executor) and `gitnexus-lfg` (plan → deepen/work gate → review pipeline) skills; section renamed to Engineering planning & execution. | -| 2026-07-11 | 1.9.0 | Added Engineering planning (`/gitnexus-plan`) section; registered the `gitnexus-plan` skill (`.claude/skills/gitnexus-plan/`). | -| 2026-05-22 | 1.8.0 | Kotlin added to `MIGRATED_LANGUAGES` (registry-primary call resolution by default). Closes #1756 (companion-vs-instance dispatch) and #1757 (lambda scopes); refs #1746. RFC §6.4 corpus criterion waived (corpus-mode wiring is #927-scope); fixture criterion met. | -| 2026-04-23 | 1.7.0 | TypeScript added to `MIGRATED_LANGUAGES` (registry-primary call resolution by default). | -| 2026-04-20 | 1.6.0 | Added scope-resolution pipeline pointer (RFC #909 Ring 3); Python migrated to registry-primary. | -| 2026-04-19 | 1.5.0 | Cross-repo impact (#794): `impact`/`query`/`context` accept `repo: "@"` + `service`. Removed `group_query`/`group_contracts`/`group_status` MCP tools; added `gitnexus://group/{name}/contracts` and `gitnexus://group/{name}/status` resources. | -| 2026-04-16 | 1.4.0 | Fixed: web UI description, pre-commit behavior, MCP tools (7->16), added gitnexus-shared, removed stale vite-plugin-wasm gotcha. | -| 2026-04-13 | 1.3.0 | Updated GitNexus index stats after DAG refactor. | -| 2026-03-24 | 1.2.0 | Fixed gitnexus:start block duplication. | -| 2026-03-23 | 1.1.0 | Updated agent instructions, references, Cursor layout. | -| 2026-03-22 | 1.0.0 | Initial structured header and changelog. | +| Date | Version | Change | +| ---------- | ------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| 2026-09-24 | 1.17.0 | Clones with the same `origin` URL now share a store automatically; `--no-share` records a lasting opt-out (#3352). | +| 2026-09-24 | 1.16.0 | Documented the shared worktree index store (`/stores/`, `analyze --share-with`, `GITNEXUS_SHARED_STORE=off`) in the storage notes (#3352). | +| 2026-09-07 | 1.15.0 | Added the Objective-C provider guide as the required reference before changing Objective-C parsing or resolution. | +| 2026-07-20 | 1.14.0 | `gitnexus-review` gains a coordinated swarm: six `ci-personas/` lanes the CI review agent dispatches as subagents (via the `Agent` tool), with a bounded critic gate and sidechain-excluded evidence. | +| 2026-07-16 | 1.13.0 | `gitnexus-plan` asks plan depth up front (quick/standard/deep) in interactive runs; `gitnexus-lfg` gate slimmed to proceed/stop (Deepen stays as the route-back mechanism). | +| 2026-07-16 | 1.12.0 | Renamed `gitnexus-pr-review` to `gitnexus-review`; added PR URL/number, branch/range, and local-change targets plus install migration (setup warns on a legacy `gitnexus-pr-review` dir and leaves it in place; uninstall removes it). | +| 2026-07-11 | 1.11.0 | Skill family shipped via npm skills/ + plugin (sync-guarded); added eval/workflow_bench token-savings benchmark. | +| 2026-07-11 | 1.10.0 | Added `gitnexus-work` (plan executor) and `gitnexus-lfg` (plan → deepen/work gate → review pipeline) skills; section renamed to Engineering planning & execution. | +| 2026-07-11 | 1.9.0 | Added Engineering planning (`/gitnexus-plan`) section; registered the `gitnexus-plan` skill (`.claude/skills/gitnexus-plan/`). | +| 2026-05-22 | 1.8.0 | Kotlin added to `MIGRATED_LANGUAGES` (registry-primary call resolution by default). Closes #1756 (companion-vs-instance dispatch) and #1757 (lambda scopes); refs #1746. RFC §6.4 corpus criterion waived (corpus-mode wiring is #927-scope); fixture criterion met. | +| 2026-04-23 | 1.7.0 | TypeScript added to `MIGRATED_LANGUAGES` (registry-primary call resolution by default). | +| 2026-04-20 | 1.6.0 | Added scope-resolution pipeline pointer (RFC #909 Ring 3); Python migrated to registry-primary. | +| 2026-04-19 | 1.5.0 | Cross-repo impact (#794): `impact`/`query`/`context` accept `repo: "@"` + `service`. Removed `group_query`/`group_contracts`/`group_status` MCP tools; added `gitnexus://group/{name}/contracts` and `gitnexus://group/{name}/status` resources. | +| 2026-04-16 | 1.4.0 | Fixed: web UI description, pre-commit behavior, MCP tools (7->16), added gitnexus-shared, removed stale vite-plugin-wasm gotcha. | +| 2026-04-13 | 1.3.0 | Updated GitNexus index stats after DAG refactor. | +| 2026-03-24 | 1.2.0 | Fixed gitnexus:start block duplication. | +| 2026-03-23 | 1.1.0 | Updated agent instructions, references, Cursor layout. | +| 2026-03-22 | 1.0.0 | Initial structured header and changelog. | --- + # GitNexus — Code Intelligence This project is indexed by GitNexus as **GitNexus** (248612 symbols, 565510 relationships, 918 execution flows). @@ -123,7 +125,7 @@ This project is indexed by GitNexus as **GitNexus** (248612 symbols, 565510 rela ## Always Do - **MUST run impact analysis before editing.** Use `impact({target: "symbolName", direction: "upstream"})` (MCP) or `node .gitnexus/run.cjs impact "symbolName" --direction upstream --repo .` (CLI fallback); report callers, processes, and risk. Never substitute grep for graph analysis. For unified PDG impact, add `mode: "pdg"` with optional `line: ` — it returns statement-level `affectedStatements` over CDG + REACHING_DEF and inter-procedural symbols in `interproceduralByDepth`/`byDepth`; no-layer/degraded PDG results are UNKNOWN-risk notes (`--pdg` layer). CLI equivalent: `node .gitnexus/run.cjs impact "symbolName" --direction upstream --mode pdg --line --repo .`. -- **MUST analyze graph changes before committing.** Use `detect_changes({scope: "all"})` (MCP) or `node .gitnexus/run.cjs detect-changes --scope all --repo .` (CLI fallback). `partial: true` or `truncated: true` is not a clean check — a zero means unseen, not unaffected; re-run it. For regression review: `detect_changes({scope: "compare", base_ref: "main"})` or `node .gitnexus/run.cjs detect-changes --scope compare --base-ref "main" --repo .`. +- **MUST analyze changes before commit.** Run `detect_changes({scope: "all"})` or `node .gitnexus/run.cjs detect-changes --scope all --repo .`. `partial: true` or `truncated: true` is not a clean check — a zero means unseen, not unaffected; re-run it. For regression review, fetch target branch "main" from its remote; compute its merge base with the reviewed HEAD and pass that SHA to `detect_changes({scope: "compare", base_ref: ""})` or `node .gitnexus/run.cjs detect-changes --scope compare --base-ref "" --repo .`. - MUST warn on HIGH/CRITICAL `risk` pre-edit; never use `riskSharedAxes` to waive a HIGH/CRITICAL `risk` warning. Compare File/symbol: MCP File omits axes; Graph-RAG expands File. - **MUST treat `risk: UNKNOWN` as unresolved, not as low.** An empty caller set is not evidence the symbol is unused — it can also mean the callers are not resolvable by the index (plain-object property access, dynamic dispatch, cross-language calls). `impact` pairs `UNKNOWN` with a `riskNote` saying so. Confirm with a text search before treating the symbol as safe to change or delete; do not proceed on the strength of a zero. - **MUST use `query({search_query: "concept"})` for concepts/flows, `context({name: "symbolName"})` for a named symbol, or `impact` for blast radius, on read-only callers, dependencies, imports, or execution flow.** Graph first; text search only for empty/`UNKNOWN`/literals. @@ -139,23 +141,23 @@ This project is indexed by GitNexus as **GitNexus** (248612 symbols, 565510 rela ## Resources -| Resource | Use for | -| --- | --- | -| `gitnexus://repo/GitNexus/context` | Codebase overview, check index freshness | -| `gitnexus://repo/GitNexus/clusters` | All functional areas | -| `gitnexus://repo/GitNexus/processes` | All execution flows | -| `gitnexus://repo/GitNexus/process/{name}` | Step-by-step execution trace | +| Resource | Use for | +| ----------------------------------------- | ---------------------------------------- | +| `gitnexus://repo/GitNexus/context` | Codebase overview, check index freshness | +| `gitnexus://repo/GitNexus/clusters` | All functional areas | +| `gitnexus://repo/GitNexus/processes` | All execution flows | +| `gitnexus://repo/GitNexus/process/{name}` | Step-by-step execution trace | ## CLI -| Task | Read this skill file | -| --- | --- | -| Understand architecture / "How does X work?" | `.claude/skills/gitnexus-exploring/SKILL.md` | -| Blast radius / "What breaks if I change X?" | `.claude/skills/gitnexus-impact-analysis/SKILL.md` | -| Trace bugs / "Why is X failing?" | `.claude/skills/gitnexus-debugging/SKILL.md` | -| Rename / extract / split / refactor | `.claude/skills/gitnexus-refactoring/SKILL.md` | -| Tools, resources, schema reference | `.claude/skills/gitnexus-guide/SKILL.md` | -| Index, status, clean, wiki CLI commands | `.claude/skills/gitnexus-cli/SKILL.md` | +| Task | Read this skill file | +| -------------------------------------------- | -------------------------------------------------- | +| Understand architecture / "How does X work?" | `.claude/skills/gitnexus-exploring/SKILL.md` | +| Blast radius / "What breaks if I change X?" | `.claude/skills/gitnexus-impact-analysis/SKILL.md` | +| Trace bugs / "Why is X failing?" | `.claude/skills/gitnexus-debugging/SKILL.md` | +| Rename / extract / split / refactor | `.claude/skills/gitnexus-refactoring/SKILL.md` | +| Tools, resources, schema reference | `.claude/skills/gitnexus-guide/SKILL.md` | +| Index, status, clean, wiki CLI commands | `.claude/skills/gitnexus-cli/SKILL.md` | @@ -163,14 +165,14 @@ This project is indexed by GitNexus as **GitNexus** (248612 symbols, 565510 rela ### Packages -| Package | Path | Purpose | -|---------|------|---------| -| **CLI/Core** | `gitnexus/` | TypeScript CLI, indexing pipeline, MCP server. Published to npm. | -| **Web UI** | `gitnexus-web/` | React/Vite thin client. All queries via `gitnexus serve` HTTP API. | -| **Shared** | `gitnexus-shared/` | Shared TypeScript types and constants. | -| Claude Plugin | `gitnexus-claude-plugin/` | Static config for Claude marketplace. | -| Cursor Integration | `gitnexus-cursor-integration/` | Static config for Cursor editor. | -| Eval | `eval/` | Python evaluation harness (Docker + LLM API keys). | +| Package | Path | Purpose | +| ------------------ | ------------------------------ | ------------------------------------------------------------------ | +| **CLI/Core** | `gitnexus/` | TypeScript CLI, indexing pipeline, MCP server. Published to npm. | +| **Web UI** | `gitnexus-web/` | React/Vite thin client. All queries via `gitnexus serve` HTTP API. | +| **Shared** | `gitnexus-shared/` | Shared TypeScript types and constants. | +| Claude Plugin | `gitnexus-claude-plugin/` | Static config for Claude marketplace. | +| Cursor Integration | `gitnexus-cursor-integration/` | Static config for Cursor editor. | +| Eval | `eval/` | Python evaluation harness (Docker + LLM API keys). | ### Running services @@ -183,12 +185,14 @@ npx gitnexus serve # HTTP API on port 4747 (from any ind ### Testing **CLI / Core (`gitnexus/`)** + - `npm test` — full vitest suite (~2000 tests) - `npm run test:unit` — unit tests only - `npm run test:integration` — integration (~1850 tests). LadybugDB file-locking tests may fail in containers (known env issue). - `npx tsc --noEmit` — typecheck **Web UI (`gitnexus-web/`)** + - `npm test` — vitest (~200 tests) - `npm run test:e2e` — Playwright (7 spec files; requires `gitnexus serve` + `npm run dev`) - `npx tsc -b --noEmit` — typecheck diff --git a/CLAUDE.md b/CLAUDE.md index da45384e4..28bee0152 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -30,7 +30,7 @@ Prefer **PreToolUse** hooks for hard gates (e.g. tests before `git_commit`). Ada ## Context budget -If always-on instructions grow, load deep conventions via conditional reads (e.g. *“When writing new code, read STANDARDS.md”*) instead of pasting long blocks here. In Cursor, prefer `.cursor/index.mdc` plus optional `.cursor/rules/*.mdc` globs (see [AGENTS.md](AGENTS.md) § Context budget). +If always-on instructions grow, load deep conventions via conditional reads (e.g. _“When writing new code, read STANDARDS.md”_) instead of pasting long blocks here. In Cursor, prefer `.cursor/index.mdc` plus optional `.cursor/rules/*.mdc` globs (see [AGENTS.md](AGENTS.md) § Context budget). ## Reference Documentation @@ -41,17 +41,17 @@ If always-on instructions grow, load deep conventions via conditional reads (e.g ## Changelog -| Date | Version | Change | -|------|---------|--------| -| 2026-07-20 | 1.8.0 | The CI review agent runs `gitnexus-review` as a coordinated swarm — six `ci-personas/` lanes dispatched via the `Agent` tool with a bounded critic gate. | -| 2026-07-16 | 1.7.0 | `/gitnexus-plan` asks depth up front in interactive runs; `/gitnexus-lfg` gate slimmed to proceed/stop. | -| 2026-07-16 | 1.6.0 | Renamed `/gitnexus-pr-review` to `/gitnexus-review` and added PR, branch/range, and local-change targets. | -| 2026-07-11 | 1.5.0 | Added `/gitnexus-work` and `/gitnexus-lfg` to the engineering plans & execution pointer. | -| 2026-07-11 | 1.4.0 | Added `/gitnexus-plan` pointer to Reference Documentation. | -| 2026-04-13 | 1.3.0 | Updated GitNexus index stats after DAG refactor. | -| 2026-03-24 | 1.2.0 | Removed duplicated gitnexus:start block and scope table; replaced with pointers to AGENTS.md. | -| 2026-03-23 | 1.1.0 | Updated agent instructions to match AGENTS.md. | -| 2026-03-22 | 1.0.0 | Added structured header and changelog. | +| Date | Version | Change | +| ---------- | ------- | -------------------------------------------------------------------------------------------------------------------------------------------------------- | +| 2026-07-20 | 1.8.0 | The CI review agent runs `gitnexus-review` as a coordinated swarm — six `ci-personas/` lanes dispatched via the `Agent` tool with a bounded critic gate. | +| 2026-07-16 | 1.7.0 | `/gitnexus-plan` asks depth up front in interactive runs; `/gitnexus-lfg` gate slimmed to proceed/stop. | +| 2026-07-16 | 1.6.0 | Renamed `/gitnexus-pr-review` to `/gitnexus-review` and added PR, branch/range, and local-change targets. | +| 2026-07-11 | 1.5.0 | Added `/gitnexus-work` and `/gitnexus-lfg` to the engineering plans & execution pointer. | +| 2026-07-11 | 1.4.0 | Added `/gitnexus-plan` pointer to Reference Documentation. | +| 2026-04-13 | 1.3.0 | Updated GitNexus index stats after DAG refactor. | +| 2026-03-24 | 1.2.0 | Removed duplicated gitnexus:start block and scope table; replaced with pointers to AGENTS.md. | +| 2026-03-23 | 1.1.0 | Updated agent instructions to match AGENTS.md. | +| 2026-03-22 | 1.0.0 | Added structured header and changelog. | --- @@ -60,6 +60,7 @@ If always-on instructions grow, load deep conventions via conditional reads (e.g See the ` … ` block in **[AGENTS.md](AGENTS.md)** for the canonical MCP tools, impact analysis rules, and index instructions. + # GitNexus — Code Intelligence This project is indexed by GitNexus as **GitNexus** (248612 symbols, 565510 relationships, 918 execution flows). @@ -70,7 +71,7 @@ This project is indexed by GitNexus as **GitNexus** (248612 symbols, 565510 rela ## Always Do - **MUST run impact analysis before editing.** Use `impact({target: "symbolName", direction: "upstream"})` (MCP) or `node .gitnexus/run.cjs impact "symbolName" --direction upstream --repo .` (CLI fallback); report callers, processes, and risk. Never substitute grep for graph analysis. For unified PDG impact, add `mode: "pdg"` with optional `line: ` — it returns statement-level `affectedStatements` over CDG + REACHING_DEF and inter-procedural symbols in `interproceduralByDepth`/`byDepth`; no-layer/degraded PDG results are UNKNOWN-risk notes (`--pdg` layer). CLI equivalent: `node .gitnexus/run.cjs impact "symbolName" --direction upstream --mode pdg --line --repo .`. -- **MUST analyze graph changes before committing.** Use `detect_changes({scope: "all"})` (MCP) or `node .gitnexus/run.cjs detect-changes --scope all --repo .` (CLI fallback). `partial: true` or `truncated: true` is not a clean check — a zero means unseen, not unaffected; re-run it. For regression review: `detect_changes({scope: "compare", base_ref: "main"})` or `node .gitnexus/run.cjs detect-changes --scope compare --base-ref "main" --repo .`. +- **MUST analyze changes before commit.** Run `detect_changes({scope: "all"})` or `node .gitnexus/run.cjs detect-changes --scope all --repo .`. `partial: true` or `truncated: true` is not a clean check — a zero means unseen, not unaffected; re-run it. For regression review, fetch target branch "main" from its remote; compute its merge base with the reviewed HEAD and pass that SHA to `detect_changes({scope: "compare", base_ref: ""})` or `node .gitnexus/run.cjs detect-changes --scope compare --base-ref "" --repo .`. - MUST warn on HIGH/CRITICAL `risk` pre-edit; never use `riskSharedAxes` to waive a HIGH/CRITICAL `risk` warning. Compare File/symbol: MCP File omits axes; Graph-RAG expands File. - **MUST treat `risk: UNKNOWN` as unresolved, not as low.** An empty caller set is not evidence the symbol is unused — it can also mean the callers are not resolvable by the index (plain-object property access, dynamic dispatch, cross-language calls). `impact` pairs `UNKNOWN` with a `riskNote` saying so. Confirm with a text search before treating the symbol as safe to change or delete; do not proceed on the strength of a zero. - **MUST use `query({search_query: "concept"})` for concepts/flows, `context({name: "symbolName"})` for a named symbol, or `impact` for blast radius, on read-only callers, dependencies, imports, or execution flow.** Graph first; text search only for empty/`UNKNOWN`/literals. @@ -86,22 +87,22 @@ This project is indexed by GitNexus as **GitNexus** (248612 symbols, 565510 rela ## Resources -| Resource | Use for | -| --- | --- | -| `gitnexus://repo/GitNexus/context` | Codebase overview, check index freshness | -| `gitnexus://repo/GitNexus/clusters` | All functional areas | -| `gitnexus://repo/GitNexus/processes` | All execution flows | -| `gitnexus://repo/GitNexus/process/{name}` | Step-by-step execution trace | +| Resource | Use for | +| ----------------------------------------- | ---------------------------------------- | +| `gitnexus://repo/GitNexus/context` | Codebase overview, check index freshness | +| `gitnexus://repo/GitNexus/clusters` | All functional areas | +| `gitnexus://repo/GitNexus/processes` | All execution flows | +| `gitnexus://repo/GitNexus/process/{name}` | Step-by-step execution trace | ## CLI -| Task | Read this skill file | -| --- | --- | -| Understand architecture / "How does X work?" | `.claude/skills/gitnexus-exploring/SKILL.md` | -| Blast radius / "What breaks if I change X?" | `.claude/skills/gitnexus-impact-analysis/SKILL.md` | -| Trace bugs / "Why is X failing?" | `.claude/skills/gitnexus-debugging/SKILL.md` | -| Rename / extract / split / refactor | `.claude/skills/gitnexus-refactoring/SKILL.md` | -| Tools, resources, schema reference | `.claude/skills/gitnexus-guide/SKILL.md` | -| Index, status, clean, wiki CLI commands | `.claude/skills/gitnexus-cli/SKILL.md` | +| Task | Read this skill file | +| -------------------------------------------- | -------------------------------------------------- | +| Understand architecture / "How does X work?" | `.claude/skills/gitnexus-exploring/SKILL.md` | +| Blast radius / "What breaks if I change X?" | `.claude/skills/gitnexus-impact-analysis/SKILL.md` | +| Trace bugs / "Why is X failing?" | `.claude/skills/gitnexus-debugging/SKILL.md` | +| Rename / extract / split / refactor | `.claude/skills/gitnexus-refactoring/SKILL.md` | +| Tools, resources, schema reference | `.claude/skills/gitnexus-guide/SKILL.md` | +| Index, status, clean, wiki CLI commands | `.claude/skills/gitnexus-cli/SKILL.md` | diff --git a/gitnexus/src/cli/ai-context.ts b/gitnexus/src/cli/ai-context.ts index f6f8cedb4..c16cdce7c 100644 --- a/gitnexus/src/cli/ai-context.ts +++ b/gitnexus/src/cli/ai-context.ts @@ -117,6 +117,15 @@ export function markdownSafeBranch(branch: string): string { return branch.replace(/`/g, ''); } +function regressionReviewGuidance(defaultBranch: string, runner?: string): string { + const safeBranch = markdownSafeBranch(defaultBranch); + const target = JSON.stringify(safeBranch); + const cliFallback = runner + ? ` or \`${runner} detect-changes --scope compare --base-ref "" --repo .\`` + : ''; + return `For regression review, fetch target branch ${target} from its remote; compute its merge base with the reviewed HEAD and pass that SHA to \`detect_changes({scope: "compare", base_ref: ""})\`${cliFallback}.`; +} + /** Options for {@link generateGitNexusContent} (collapsed from positional * params, #2188 review — six `undefined`s to reach `hasPdg` was the smell). */ export interface GitNexusContentOptions { @@ -129,9 +138,9 @@ export interface GitNexusContentOptions { * the available runner (global `gitnexus` → `pnpm dlx` → `bunx` → `npx`) at * call time. */ runnerPath?: string; - /** Default branch for the regression-compare example (#243). Configurable so - * projects on `develop`/`master`/etc. don't get `base_ref: "main"` rewritten - * back over their fix on every analyze. The value is embedded inside a + /** Default target branch for the regression-review guidance (#243, #3509). + * This is a target hint only: compare scope must receive the exact merge-base + * SHA, never the moving branch tip. The value is embedded inside a * Markdown inline-code span: validateBranchName rejects backticks upstream, * and `markdownSafeBranch` strips any remaining backtick here as defense in * depth, so JSON.stringify's quote/escape handling is sufficient and the @@ -203,8 +212,8 @@ ${tableBody}` // bun-only machine has no npm, npx or pnpm at all, and the npx-only note left // it with a bootstrap command it could not run. const bootstrapNote = - `No \`${runnerPath}\` yet? Bootstrap with \`npx\`, \`bunx\`, or \`pnpm dlx\` — ` + - 'e.g. `bunx gitnexus@latest analyze` (npm 11 npx crash; #1939).'; + `No \`${runnerPath}\`? Bootstrap with \`npx\`, \`bunx\`, or \`pnpm dlx\`; ` + + 'e.g. `bunx gitnexus@latest analyze` (#1939).'; // This block is injected into every user's repo and its total size is capped // by test (ai-context.test.ts, #856) — a new bullet or clause has to be paid @@ -222,17 +231,17 @@ ${tableBody}` This project is indexed by GitNexus as **${projectName}**${noStats ? '' : ` (${stats.nodes || 0} symbols, ${stats.edges || 0} relationships, ${stats.processes || 0} execution flows)`}. -> Index stale? Run \`${runner} analyze --index-only\` from the project root — it auto-selects an available runner. ${bootstrapNote} +> Index stale? Run \`${runner} analyze --index-only\`. ${bootstrapNote} > On query/context/impact/cypher object results, read staleness.status and branch/lastCommit. Re-analyze only for behind or diverged — current is clone HEAD, not main. ## Always Do -- **MUST run impact before editing.** Use \`impact({target: "symbolName", direction: "upstream"})\` or \`${runner} impact "symbolName" --direction upstream --repo .\`; report callers, processes, and risk. Never substitute grep for graph analysis.${ +- **MUST run impact before editing.** Use \`impact({target: "symbolName", direction: "upstream"})\` or \`${runner} impact "symbolName" --direction upstream --repo .\`; report callers/processes/risk. Never substitute grep for graph analysis.${ hasPdg ? ` For unified PDG impact, add \`mode: "pdg"\` with optional \`line: \` — it returns statement-level \`affectedStatements\` over CDG + REACHING_DEF and inter-procedural symbols in \`interproceduralByDepth\`/\`byDepth\`; no-layer/degraded PDG results are UNKNOWN-risk notes (\`--pdg\` layer). CLI equivalent: \`${runner} impact "symbolName" --direction upstream --mode pdg --line --repo .\`.` : '' } -- **MUST analyze graph changes before committing.** Use \`detect_changes({scope: "all"})\` (MCP) or \`${runner} detect-changes --scope all --repo .\` (CLI fallback). \`partial: true\` or \`truncated: true\` is not a clean check — a zero means unseen, not unaffected; re-run it. For regression review: \`detect_changes({scope: "compare", base_ref: ${JSON.stringify(markdownSafeBranch(defaultBranch))}})\` or \`${runner} detect-changes --scope compare --base-ref ${JSON.stringify(markdownSafeBranch(defaultBranch))} --repo .\`. +- **MUST analyze changes before commit.** Run \`detect_changes({scope: "all"})\` or \`${runner} detect-changes --scope all --repo .\`. \`partial: true\` or \`truncated: true\` is not a clean check — a zero means unseen, not unaffected; re-run it. ${regressionReviewGuidance(defaultBranch, runner)} - MUST warn on HIGH/CRITICAL \`risk\` pre-edit; never use \`riskSharedAxes\` to waive a HIGH/CRITICAL \`risk\` warning. Compare File/symbol: MCP File omits axes; Graph-RAG expands File. - **MUST treat \`risk: UNKNOWN\` as unresolved, not as low.** An empty caller set is not evidence the symbol is unused — it can also mean the callers are not resolvable by the index (plain-object property access, dynamic dispatch, cross-language calls). \`impact\` pairs \`UNKNOWN\` with a \`riskNote\` saying so. Confirm with a text search before treating the symbol as safe to change or delete; do not proceed on the strength of a zero. - **MUST use \`query({search_query: "concept"})\` for concepts/flows, \`context({name: "symbolName"})\` for a named symbol, or \`impact\` for blast radius, on read-only callers, dependencies, imports, or execution flow.** Graph first; text search only for empty/\`UNKNOWN\`/literals.${ @@ -726,8 +735,8 @@ export async function generateAIContextFiles( } /** - * Refresh only the `base_ref: "..."` value inside the GitNexus block of an - * already-generated AGENTS.md / CLAUDE.md, in place (#1996 tri-review P2). + * Refresh the regression-review target inside the GitNexus block of an + * already-generated AGENTS.md / CLAUDE.md, in place (#1996 P2, #3509). * * The `alreadyUpToDate` analyze fast path returns before the normal * {@link generateAIContextFiles} call, so a changed `.gitnexusrc` defaultBranch @@ -736,8 +745,10 @@ export async function generateAIContextFiles( * block — including community-skill rows written by a prior `--skills` run — * rather than regenerating (which would drop those rows on a no-`--skills` run). * - * Best-effort: missing files, a missing/blank block, or a block with no - * `base_ref` line (e.g. a user-trimmed keep block) are silently skipped. Writes + * Legacy blocks that passed the branch tip as `base_ref` are migrated to the + * merge-base guidance at the same time. Best-effort: missing files, a + * missing/blank block, or a block with no generated regression sentence are + * silently skipped. Writes * only when the value actually changes, so a routine up-to-date run is a no-op. */ export async function refreshBaseRefLine( @@ -746,7 +757,6 @@ export async function refreshBaseRefLine( options?: { skipAgentsMd?: boolean }, ): Promise<{ files: string[] }> { if (options?.skipAgentsMd) return { files: [] }; - const replacement = `base_ref: ${JSON.stringify(markdownSafeBranch(defaultBranch))}`; const updated: string[] = []; for (const name of ['AGENTS.md', 'CLAUDE.md']) { const filePath = path.join(repoPath, name); @@ -763,9 +773,36 @@ export async function refreshBaseRefLine( if (endIdx === -1 || endIdx <= startIdx) continue; const blockEnd = endIdx + GITNEXUS_END_MARKER.length; const block = content.substring(startIdx, blockEnd); - // Only the generated regression example carries a base_ref line, and only - // one per block; replace its quoted value while leaving the rest untouched. - const newBlock = block.replace(/base_ref: "(?:[^"\\]|\\.)*"/, replacement); + // Preserve the runner emitted with the block. Its location follows the + // configured storage path, so assuming `.gitnexus/run.cjs` would break the + // CLI fallback when migrating guidance generated for an external store. + const legacyRunner = block.match( + /or `([^`\n]+?) detect-changes --scope compare --base-ref\b/, + )?.[1]; + const mergeBaseGuidance = regressionReviewGuidance(defaultBranch, legacyRunner); + let newBlock = block; + if (!block.includes(mergeBaseGuidance) && block.includes('base_ref: ""')) { + newBlock = block.replace( + /(?:(?:For (?:regression review of )?)?target branch "(?:[^"\\]|\\.)*": `git fetch origin -- [^`\n]+`; pass `git merge-base [^`\n]+ HEAD`'s SHA|Regression base: (?:PR GitHub `baseRefOid`\/`headRefOid`|exact PR base\/head commits from host metadata); target branch "(?:[^"\\]|\\.)*" via tracking remote\. Fetch both; pass merge-base SHA|For regression review, fetch target branch "(?:[^"\\]|\\.)*" from its remote; compute its merge base with the reviewed HEAD and pass that SHA) to `detect_changes\(\{scope: "compare", base_ref: ""\}\)`(?: or `[^`\n]+? detect-changes --scope compare --base-ref "" --repo \.\`)?\./, + mergeBaseGuidance, + ); + } + if (newBlock === block) { + // Migrate the legacy generated sentence. + newBlock = block.replace( + /For regression review: `detect_changes\(\{scope: "compare", base_ref: "(?:[^"\\]|\\.)*"\}\)`(?: or `[^`\n]+? detect-changes --scope compare --base-ref "(?:[^"\\]|\\.)*" --repo \.\`)?\./, + mergeBaseGuidance, + ); + } + if (newBlock === block) { + // Older compact blocks used this exact generated bullet rather than the + // prose sentence. Keep the anchor narrow so user-authored compare calls + // elsewhere in the managed block remain untouched. + newBlock = block.replace( + /- run detect_changes\(\{scope: "compare", base_ref: "(?:[^"\\]|\\.)*"\}\)(?=\s*(?:\n|$))/, + `- ${mergeBaseGuidance}`, + ); + } if (newBlock === block) continue; // no base_ref line present, or already current const newContent = content.substring(0, startIdx) + newBlock + content.substring(blockEnd); try { diff --git a/gitnexus/src/cli/analyze.ts b/gitnexus/src/cli/analyze.ts index 00e9a9d62..483b889de 100644 --- a/gitnexus/src/cli/analyze.ts +++ b/gitnexus/src/cli/analyze.ts @@ -1663,7 +1663,7 @@ const analyzeCommandImpl = async ( } if (baseRefRefreshed.length > 0) { console.log( - ` Updated base_ref to "${resolvedDefaultBranch}" in ${baseRefRefreshed.join(', ')}\n`, + ` Updated regression-review target to "${resolvedDefaultBranch}" in ${baseRefRefreshed.join(', ')}\n`, ); } // #2639: opt-in self-commit of any AGENTS.md/CLAUDE.md churn from this diff --git a/gitnexus/test/unit/ai-context.test.ts b/gitnexus/test/unit/ai-context.test.ts index b57c556ed..b74e599a3 100644 --- a/gitnexus/test/unit/ai-context.test.ts +++ b/gitnexus/test/unit/ai-context.test.ts @@ -191,8 +191,13 @@ describe('generateAIContextFiles', () => { ); expect(content).toContain('detect_changes({scope: "all"})'); expect(content).toContain('node .gitnexus/run.cjs detect-changes --scope all --repo .'); - expect(content).toContain('detect_changes({scope: "compare", base_ref: "develop"})'); - expect(content).toContain('--scope compare --base-ref "develop" --repo .'); + expect(content).toContain('target branch "develop"'); + expect(content).toContain('target branch "develop" from its remote'); + expect(content).toContain('compute its merge base with the reviewed HEAD'); + expect(content).toContain( + 'detect_changes({scope: "compare", base_ref: ""})', + ); + expect(content).toContain('--scope compare --base-ref "" --repo .'); expect(content).toContain('Never substitute grep for graph analysis'); expect(content).not.toContain('gitnexus impact --target'); } @@ -1474,19 +1479,30 @@ Indexed as **placeholder** (1 symbols, 1 relationships, 1 execution flows). Cust }); // ────────────────────────────────────────────────────────────────── - // Configurable default branch in the regression example (#243) + // Configurable review target and exact merge-base guidance (#243, #3509) // ────────────────────────────────────────────────────────────────── - it('generated regression-compare example uses the configured default branch (#243)', () => { + it('uses the configured branch only as a review target, never as base_ref (#243, #3509)', () => { const stats = { nodes: 50, edges: 100, processes: 5 }; const develop = generateGitNexusContent('P', stats, { defaultBranch: 'develop' }); - expect(develop).toContain('base_ref: "develop"'); - expect(develop).not.toContain('base_ref: "main"'); + expect(develop).toContain('target branch "develop" from its remote'); + expect(develop).toContain('compute its merge base with the reviewed HEAD'); + expect(develop).not.toContain('GitHub'); + expect(develop).not.toContain('PR base/head'); + expect(develop).not.toContain('git fetch origin'); + expect(develop).toContain('base_ref: ""'); + expect(develop).not.toContain('base_ref: "develop"'); }); - it('defaults the regression-compare example to "main" when no branch is configured (#243)', () => { + it('defaults the review target to main without comparing to the main tip (#243, #3509)', () => { const content = generateGitNexusContent('P', { nodes: 50, edges: 100, processes: 5 }); - expect(content).toContain('base_ref: "main"'); + expect(content).toContain('target branch "main" from its remote'); + expect(content).toContain('compute its merge base with the reviewed HEAD'); + expect(content).not.toContain('GitHub'); + expect(content).not.toContain('PR base/head'); + expect(content).not.toContain('git fetch origin'); + expect(content).toContain('base_ref: ""'); + expect(content).not.toContain('base_ref: "main"'); }); it('references MCP tools by their registered (unprefixed) names (#2059)', () => { @@ -1508,23 +1524,24 @@ Indexed as **placeholder** (1 symbols, 1 relationships, 1 execution flows). Cust // A branch name with a double-quote must be JSON-escaped, not concatenated // raw, so it stays inside the inline code span. const content = generateGitNexusContent('P', { nodes: 1 }, { defaultBranch: 'we"ird' }); - expect(content).toContain('base_ref: "we\\"ird"'); - expect(content).toContain('--base-ref "we\\"ird" --repo .'); + expect(content).toContain('target branch "we\\"ird"'); + expect(content).toContain('--base-ref "" --repo .'); }); it('a backtick branch cannot break the generated Markdown code span (#1996 P1)', () => { // The branch is embedded inside a backtick inline-code span; a stray // backtick would close it early. markdownSafeBranch strips it at the sink. const content = generateGitNexusContent('P', { nodes: 1 }, { defaultBranch: 'main`evil' }); - const line = content.split('\n').find((l) => l.includes('base_ref'))!; + const line = content.split('\n').find((l) => l.includes('target branch'))!; // Even backtick count ⇒ every span is balanced (the regression line opens // and closes exactly one). expect((line.match(/`/g) || []).length % 2).toBe(0); expect(line).not.toContain('main`evil'); + expect(line).toContain('target branch "mainevil" from its remote'); expect(markdownSafeBranch('a`b`c')).toBe('abc'); }); - it('refreshBaseRefLine updates base_ref in place, preserving the rest of the block (#1996 P2)', async () => { + it('refreshBaseRefLine migrates legacy guidance and preserves the rest of the block (#1996 P2, #3509)', async () => { const dir = await fs.mkdtemp(path.join(os.tmpdir(), 'gn-baseref-')); try { // Seed a realistic block: a configured base_ref "main" plus a community @@ -1534,7 +1551,7 @@ Indexed as **placeholder** (1 symbols, 1 relationships, 1 execution flows). Cust # GitNexus — Code Intelligence -- run \`detect_changes({scope: "compare", base_ref: "main"})\`. +For regression review: \`detect_changes({scope: "compare", base_ref: "main"})\` or \`.gitnexus/run.cjs detect-changes --scope compare --base-ref "main" --repo .\`. | Task | Read this skill file | |------|---------------------| @@ -1550,7 +1567,11 @@ Indexed as **placeholder** (1 symbols, 1 relationships, 1 execution flows). Cust for (const f of ['AGENTS.md', 'CLAUDE.md']) { const after = await fs.readFile(path.join(dir, f), 'utf-8'); - expect(after).toContain('base_ref: "develop"'); + expect(after).toContain('target branch "develop"'); + expect(after).toContain('compute its merge base with the reviewed HEAD'); + expect(after).toContain('target branch "develop" from its remote'); + expect(after).toContain('base_ref: ""'); + expect(after).not.toContain('base_ref: "develop"'); expect(after).not.toContain('base_ref: "main"'); // The community-skill row (and everything else) is preserved. expect(after).toContain('.claude/skills/gitnexus-area-auth/SKILL.md'); @@ -1564,14 +1585,14 @@ Indexed as **placeholder** (1 symbols, 1 relationships, 1 execution flows). Cust const skipped = await refreshBaseRefLine(dir, 'master', { skipAgentsMd: true }); expect(skipped.files).toEqual([]); expect(await fs.readFile(path.join(dir, 'AGENTS.md'), 'utf-8')).toContain( - 'base_ref: "develop"', + 'target branch "develop"', ); } finally { await fs.rm(dir, { recursive: true, force: true }); } }); - it('refreshBaseRefLine is a no-op when there is no base_ref line or no file (#1996 P2)', async () => { + it('refreshBaseRefLine is a no-op when there is no generated review line or no file (#1996 P2)', async () => { const dir = await fs.mkdtemp(path.join(os.tmpdir(), 'gn-baseref-noop-')); try { // No AGENTS.md/CLAUDE.md at all → no files updated, no throw. @@ -1585,6 +1606,150 @@ Indexed as **P**. Custom. await fs.writeFile(path.join(dir, 'CLAUDE.md'), seed, 'utf-8'); expect((await refreshBaseRefLine(dir, 'develop')).files).toEqual([]); expect(await fs.readFile(path.join(dir, 'CLAUDE.md'), 'utf-8')).toBe(seed); + + const custom = ` +Custom target branch "main" and base_ref: "main" notes. + +`; + await fs.writeFile(path.join(dir, 'AGENTS.md'), custom, 'utf-8'); + expect((await refreshBaseRefLine(dir, 'develop')).files).toEqual([]); + expect(await fs.readFile(path.join(dir, 'AGENTS.md'), 'utf-8')).toBe(custom); + + // A user-authored compare instruction is not generated migration state, + // even though it contains the same tool call and base_ref shape. + const compareInstruction = ` +Before release, run \`detect_changes({scope: "compare", base_ref: "main"})\` and inspect every returned flow. + +`; + await fs.writeFile(path.join(dir, 'CLAUDE.md'), compareInstruction, 'utf-8'); + expect((await refreshBaseRefLine(dir, 'develop')).files).toEqual([]); + expect(await fs.readFile(path.join(dir, 'CLAUDE.md'), 'utf-8')).toBe(compareInstruction); + } finally { + await fs.rm(dir, { recursive: true, force: true }); + } + }); + + it('refreshBaseRefLine updates current merge-base guidance idempotently (#3509)', async () => { + const dir = await fs.mkdtemp(path.join(os.tmpdir(), 'gn-review-target-')); + try { + const seed = generateGitNexusContent('P', { nodes: 1 }, { defaultBranch: 'main' }); + await fs.writeFile(path.join(dir, 'CLAUDE.md'), seed, 'utf-8'); + + expect((await refreshBaseRefLine(dir, 'develop')).files).toEqual(['CLAUDE.md']); + const after = await fs.readFile(path.join(dir, 'CLAUDE.md'), 'utf-8'); + expect(after).toContain('target branch "develop"'); + expect(after).toContain('base_ref: ""'); + expect(after).not.toContain('base_ref: "develop"'); + expect((await refreshBaseRefLine(dir, 'develop')).files).toEqual([]); + } finally { + await fs.rm(dir, { recursive: true, force: true }); + } + }); + + it('preserves a custom runner path while migrating legacy guidance (#3509)', async () => { + const dir = await fs.mkdtemp(path.join(os.tmpdir(), 'gn-review-runner-')); + try { + const seed = ` +For regression review: \`detect_changes({scope: "compare", base_ref: "main"})\` or \`.external-index/run.cjs detect-changes --scope compare --base-ref "main" --repo .\`. + +`; + await fs.writeFile(path.join(dir, 'CLAUDE.md'), seed, 'utf-8'); + + expect((await refreshBaseRefLine(dir, 'develop')).files).toEqual(['CLAUDE.md']); + const after = await fs.readFile(path.join(dir, 'CLAUDE.md'), 'utf-8'); + expect(after).toContain('`.external-index/run.cjs detect-changes'); + expect(after).not.toContain('`.gitnexus/run.cjs detect-changes'); + expect(after).toContain('base_ref: ""'); + } finally { + await fs.rm(dir, { recursive: true, force: true }); + } + }); + + it('preserves same-line custom text after legacy guidance (#3509)', async () => { + const dir = await fs.mkdtemp(path.join(os.tmpdir(), 'gn-review-suffix-')); + try { + const seed = ` +For regression review: \`detect_changes({scope: "compare", base_ref: "main"})\`. Always inspect src/security.ts before release. + +`; + await fs.writeFile(path.join(dir, 'AGENTS.md'), seed, 'utf-8'); + + expect((await refreshBaseRefLine(dir, 'develop')).files).toEqual(['AGENTS.md']); + const after = await fs.readFile(path.join(dir, 'AGENTS.md'), 'utf-8'); + expect(after).toContain('target branch "develop"'); + expect(after).toContain('Always inspect src/security.ts before release.'); + } finally { + await fs.rm(dir, { recursive: true, force: true }); + } + }); + + it.each(['PR GitHub `baseRefOid`/`headRefOid`', 'exact PR base/head commits from host metadata'])( + 'migrates provider-specific guidance (%s) without losing custom text', + async (source) => { + const dir = await fs.mkdtemp(path.join(os.tmpdir(), 'gn-review-host-neutral-')); + try { + const seed = + '\n' + + `Regression base: ${source}; target branch "main" via tracking remote. Fetch both; pass merge-base SHA to ` + + '`detect_changes({scope: "compare", base_ref: ""})`. Keep this instruction.\n' + + '\n'; + await fs.writeFile(path.join(dir, 'AGENTS.md'), seed, 'utf-8'); + expect((await refreshBaseRefLine(dir, 'main')).files).toEqual(['AGENTS.md']); + const after = await fs.readFile(path.join(dir, 'AGENTS.md'), 'utf-8'); + expect(after).toContain('fetch target branch "main" from its remote'); + expect(after).toContain('compute its merge base with the reviewed HEAD'); + expect(after).not.toContain(source); + expect(after).toContain('Keep this instruction.'); + expect((await refreshBaseRefLine(dir, 'main')).files).toEqual([]); + } finally { + await fs.rm(dir, { recursive: true, force: true }); + } + }, + ); + + it('preserves same-line custom text when refreshing current guidance (#3509)', async () => { + const dir = await fs.mkdtemp(path.join(os.tmpdir(), 'gn-review-current-suffix-')); + try { + const current = generateGitNexusContent('P', { nodes: 1, edges: 1, processes: 0 }); + const seed = current.replace( + /(For regression review,[^\n]*\.)/, + '$1 Always inspect src/security.ts before release.', + ); + await fs.writeFile(path.join(dir, 'AGENTS.md'), seed, 'utf-8'); + + expect((await refreshBaseRefLine(dir, 'develop')).files).toEqual(['AGENTS.md']); + const after = await fs.readFile(path.join(dir, 'AGENTS.md'), 'utf-8'); + expect(after).toContain('target branch "develop"'); + expect(after).toContain('compute its merge base with the reviewed HEAD'); + expect(after).toContain('Always inspect src/security.ts before release.'); + } finally { + await fs.rm(dir, { recursive: true, force: true }); + } + }); + + it('keeps MCP-only legacy guidance MCP-only and migrates the compact shape (#3509)', async () => { + const dir = await fs.mkdtemp(path.join(os.tmpdir(), 'gn-review-mcp-only-')); + try { + const prose = ` +For regression review: \`detect_changes({scope: "compare", base_ref: "main"})\`. + +`; + await fs.writeFile(path.join(dir, 'AGENTS.md'), prose, 'utf-8'); + expect((await refreshBaseRefLine(dir, 'develop')).files).toEqual(['AGENTS.md']); + const afterProse = await fs.readFile(path.join(dir, 'AGENTS.md'), 'utf-8'); + expect(afterProse).toContain('target branch "develop"'); + expect(afterProse).not.toContain('.gitnexus/run.cjs'); + await fs.rm(path.join(dir, 'AGENTS.md')); + + const compact = ` +- run detect_changes({scope: "compare", base_ref: "main"}) + +`; + await fs.writeFile(path.join(dir, 'CLAUDE.md'), compact, 'utf-8'); + expect((await refreshBaseRefLine(dir, 'release')).files).toEqual(['CLAUDE.md']); + const afterCompact = await fs.readFile(path.join(dir, 'CLAUDE.md'), 'utf-8'); + expect(afterCompact).toContain('target branch "release"'); + expect(afterCompact).toContain('merge-base SHA'); } finally { await fs.rm(dir, { recursive: true, force: true }); } @@ -1601,8 +1766,9 @@ Indexed as **P**. Custom. }); for (const f of ['CLAUDE.md', 'AGENTS.md']) { const content = await fs.readFile(path.join(subDir, f), 'utf-8'); - expect(content).toContain('base_ref: "release/1.0"'); - expect(content).not.toContain('base_ref: "main"'); + expect(content).toContain('target branch "release/1.0"'); + expect(content).toContain('base_ref: ""'); + expect(content).not.toContain('base_ref: "release/1.0"'); } } finally { await fs.rm(subDir, { recursive: true, force: true });