fix: use merge-base in regression review guidance (#3514)

This commit is contained in:
Parafee41 2026-10-14 15:53:15 +08:00 • committed by GitHub
parent f8f97678d4
commit 1b4e3f0a81
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 320 additions and 112 deletions

104
AGENTS.md
View file

@ -7,12 +7,12 @@ Last reviewed: 2026-09-24
## Scope ## Scope
| Boundary | Rule | | Boundary | Rule |
|----------|------| | -------------- | --------------------------------------------------------------------------------------------------------------------------- |
| **Reads** | `gitnexus/`, `gitnexus-web/`, `eval/`, plugin packages, `.github/`, `.gitnexus/`, docs. | | **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. | | **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. | | **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. | | **Off-limits** | Real `.env` / secrets, production credentials, unrelated repos, destructive git ops without confirmation. |
## Model Configuration ## Model Configuration
@ -22,11 +22,12 @@ Last reviewed: 2026-09-24
## Execution Sequence (complex tasks) ## Execution Sequence (complex tasks)
For multi-step work, state up front: For multi-step work, state up front:
1. Which rules in this file and **[GUARDRAILS.md](GUARDRAILS.md)** apply (and any relevant Signs). 1. Which rules in this file and **[GUARDRAILS.md](GUARDRAILS.md)** apply (and any relevant Signs).
2. Current **Scope** boundaries. 2. Current **Scope** boundaries.
3. Which **validation commands** you will run (`cd gitnexus && npm test`, `npx tsc --noEmit`). 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 ## Claude Code hooks
@ -89,30 +90,31 @@ mirror. `gitnexus/test/unit/shipped-skills-sync.test.ts` guards the copies. Toke
## Changelog ## Changelog
| Date | Version | Change | | 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.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 (`<GITNEXUS_HOME>/stores/`, `analyze --share-with`, `GITNEXUS_SHARED_STORE=off`) in the storage notes (#3352). | | 2026-09-24 | 1.16.0 | Documented the shared worktree index store (`<GITNEXUS_HOME>/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-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-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.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-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.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.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-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-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-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-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: "@<group>"` + `service`. Removed `group_query`/`group_contracts`/`group_status` MCP tools; added `gitnexus://group/{name}/contracts` and `gitnexus://group/{name}/status` resources. | | 2026-04-19 | 1.5.0 | Cross-repo impact (#794): `impact`/`query`/`context` accept `repo: "@<group>"` + `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-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-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-24 | 1.2.0 | Fixed gitnexus:start block duplication. |
| 2026-03-23 | 1.1.0 | Updated agent instructions, references, Cursor layout. | | 2026-03-23 | 1.1.0 | Updated agent instructions, references, Cursor layout. |
| 2026-03-22 | 1.0.0 | Initial structured header and changelog. | | 2026-03-22 | 1.0.0 | Initial structured header and changelog. |
--- ---
<!-- gitnexus:start --> <!-- gitnexus:start -->
# GitNexus — Code Intelligence # GitNexus — Code Intelligence
This project is indexed by GitNexus as **GitNexus** (248612 symbols, 565510 relationships, 918 execution flows). 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 ## 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: <N>` — 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 <N> --repo .`. - **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: <N>` — 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 <N> --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: "<merge-base SHA>"})` or `node .gitnexus/run.cjs detect-changes --scope compare --base-ref "<merge-base SHA>" --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 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 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. - **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 ## Resources
| Resource | Use for | | Resource | Use for |
| --- | --- | | ----------------------------------------- | ---------------------------------------- |
| `gitnexus://repo/GitNexus/context` | Codebase overview, check index freshness | | `gitnexus://repo/GitNexus/context` | Codebase overview, check index freshness |
| `gitnexus://repo/GitNexus/clusters` | All functional areas | | `gitnexus://repo/GitNexus/clusters` | All functional areas |
| `gitnexus://repo/GitNexus/processes` | All execution flows | | `gitnexus://repo/GitNexus/processes` | All execution flows |
| `gitnexus://repo/GitNexus/process/{name}` | Step-by-step execution trace | | `gitnexus://repo/GitNexus/process/{name}` | Step-by-step execution trace |
## CLI ## CLI
| Task | Read this skill file | | Task | Read this skill file |
| --- | --- | | -------------------------------------------- | -------------------------------------------------- |
| Understand architecture / "How does X work?" | `.claude/skills/gitnexus-exploring/SKILL.md` | | 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` | | 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` | | Trace bugs / "Why is X failing?" | `.claude/skills/gitnexus-debugging/SKILL.md` |
| Rename / extract / split / refactor | `.claude/skills/gitnexus-refactoring/SKILL.md` | | Rename / extract / split / refactor | `.claude/skills/gitnexus-refactoring/SKILL.md` |
| Tools, resources, schema reference | `.claude/skills/gitnexus-guide/SKILL.md` | | Tools, resources, schema reference | `.claude/skills/gitnexus-guide/SKILL.md` |
| Index, status, clean, wiki CLI commands | `.claude/skills/gitnexus-cli/SKILL.md` | | Index, status, clean, wiki CLI commands | `.claude/skills/gitnexus-cli/SKILL.md` |
<!-- gitnexus:end --> <!-- gitnexus:end -->
@ -163,14 +165,14 @@ This project is indexed by GitNexus as **GitNexus** (248612 symbols, 565510 rela
### Packages ### Packages
| Package | Path | Purpose | | Package | Path | Purpose |
|---------|------|---------| | ------------------ | ------------------------------ | ------------------------------------------------------------------ |
| **CLI/Core** | `gitnexus/` | TypeScript CLI, indexing pipeline, MCP server. Published to npm. | | **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. | | **Web UI** | `gitnexus-web/` | React/Vite thin client. All queries via `gitnexus serve` HTTP API. |
| **Shared** | `gitnexus-shared/` | Shared TypeScript types and constants. | | **Shared** | `gitnexus-shared/` | Shared TypeScript types and constants. |
| Claude Plugin | `gitnexus-claude-plugin/` | Static config for Claude marketplace. | | Claude Plugin | `gitnexus-claude-plugin/` | Static config for Claude marketplace. |
| Cursor Integration | `gitnexus-cursor-integration/` | Static config for Cursor editor. | | Cursor Integration | `gitnexus-cursor-integration/` | Static config for Cursor editor. |
| Eval | `eval/` | Python evaluation harness (Docker + LLM API keys). | | Eval | `eval/` | Python evaluation harness (Docker + LLM API keys). |
### Running services ### Running services
@ -183,12 +185,14 @@ npx gitnexus serve # HTTP API on port 4747 (from any ind
### Testing ### Testing
**CLI / Core (`gitnexus/`)** **CLI / Core (`gitnexus/`)**
- `npm test` — full vitest suite (~2000 tests) - `npm test` — full vitest suite (~2000 tests)
- `npm run test:unit` — unit tests only - `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). - `npm run test:integration` — integration (~1850 tests). LadybugDB file-locking tests may fail in containers (known env issue).
- `npx tsc --noEmit` — typecheck - `npx tsc --noEmit` — typecheck
**Web UI (`gitnexus-web/`)** **Web UI (`gitnexus-web/`)**
- `npm test` — vitest (~200 tests) - `npm test` — vitest (~200 tests)
- `npm run test:e2e` — Playwright (7 spec files; requires `gitnexus serve` + `npm run dev`) - `npm run test:e2e` — Playwright (7 spec files; requires `gitnexus serve` + `npm run dev`)
- `npx tsc -b --noEmit` — typecheck - `npx tsc -b --noEmit` — typecheck

View file

@ -30,7 +30,7 @@ Prefer **PreToolUse** hooks for hard gates (e.g. tests before `git_commit`). Ada
## Context budget ## 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 ## Reference Documentation
@ -41,17 +41,17 @@ If always-on instructions grow, load deep conventions via conditional reads (e.g
## Changelog ## Changelog
| Date | Version | Change | | 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-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.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-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.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-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-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-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-23 | 1.1.0 | Updated agent instructions to match AGENTS.md. |
| 2026-03-22 | 1.0.0 | Added structured header and changelog. | | 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 `<!-- gitnexus:start --> … <!-- gitnexus:end -->` block in **[AGENTS.md](AGENTS.md)** for the canonical MCP tools, impact analysis rules, and index instructions. See the `<!-- gitnexus:start --> … <!-- gitnexus:end -->` block in **[AGENTS.md](AGENTS.md)** for the canonical MCP tools, impact analysis rules, and index instructions.
<!-- gitnexus:start --> <!-- gitnexus:start -->
# GitNexus — Code Intelligence # GitNexus — Code Intelligence
This project is indexed by GitNexus as **GitNexus** (248612 symbols, 565510 relationships, 918 execution flows). 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 ## 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: <N>` — 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 <N> --repo .`. - **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: <N>` — 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 <N> --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: "<merge-base SHA>"})` or `node .gitnexus/run.cjs detect-changes --scope compare --base-ref "<merge-base SHA>" --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 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 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. - **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 ## Resources
| Resource | Use for | | Resource | Use for |
| --- | --- | | ----------------------------------------- | ---------------------------------------- |
| `gitnexus://repo/GitNexus/context` | Codebase overview, check index freshness | | `gitnexus://repo/GitNexus/context` | Codebase overview, check index freshness |
| `gitnexus://repo/GitNexus/clusters` | All functional areas | | `gitnexus://repo/GitNexus/clusters` | All functional areas |
| `gitnexus://repo/GitNexus/processes` | All execution flows | | `gitnexus://repo/GitNexus/processes` | All execution flows |
| `gitnexus://repo/GitNexus/process/{name}` | Step-by-step execution trace | | `gitnexus://repo/GitNexus/process/{name}` | Step-by-step execution trace |
## CLI ## CLI
| Task | Read this skill file | | Task | Read this skill file |
| --- | --- | | -------------------------------------------- | -------------------------------------------------- |
| Understand architecture / "How does X work?" | `.claude/skills/gitnexus-exploring/SKILL.md` | | 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` | | 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` | | Trace bugs / "Why is X failing?" | `.claude/skills/gitnexus-debugging/SKILL.md` |
| Rename / extract / split / refactor | `.claude/skills/gitnexus-refactoring/SKILL.md` | | Rename / extract / split / refactor | `.claude/skills/gitnexus-refactoring/SKILL.md` |
| Tools, resources, schema reference | `.claude/skills/gitnexus-guide/SKILL.md` | | Tools, resources, schema reference | `.claude/skills/gitnexus-guide/SKILL.md` |
| Index, status, clean, wiki CLI commands | `.claude/skills/gitnexus-cli/SKILL.md` | | Index, status, clean, wiki CLI commands | `.claude/skills/gitnexus-cli/SKILL.md` |
<!-- gitnexus:end --> <!-- gitnexus:end -->

View file

@ -117,6 +117,15 @@ export function markdownSafeBranch(branch: string): string {
return branch.replace(/`/g, ''); 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 "<merge-base SHA>" --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: "<merge-base SHA>"})\`${cliFallback}.`;
}
/** Options for {@link generateGitNexusContent} (collapsed from positional /** Options for {@link generateGitNexusContent} (collapsed from positional
* params, #2188 review — six `undefined`s to reach `hasPdg` was the smell). */ * params, #2188 review — six `undefined`s to reach `hasPdg` was the smell). */
export interface GitNexusContentOptions { export interface GitNexusContentOptions {
@ -129,9 +138,9 @@ export interface GitNexusContentOptions {
* the available runner (global `gitnexus` → `pnpm dlx` → `bunx` → `npx`) at * the available runner (global `gitnexus` → `pnpm dlx` → `bunx` → `npx`) at
* call time. */ * call time. */
runnerPath?: string; runnerPath?: string;
/** Default branch for the regression-compare example (#243). Configurable so /** Default target branch for the regression-review guidance (#243, #3509).
* projects on `develop`/`master`/etc. don't get `base_ref: "main"` rewritten * This is a target hint only: compare scope must receive the exact merge-base
* back over their fix on every analyze. The value is embedded inside a * SHA, never the moving branch tip. The value is embedded inside a
* Markdown inline-code span: validateBranchName rejects backticks upstream, * Markdown inline-code span: validateBranchName rejects backticks upstream,
* and `markdownSafeBranch` strips any remaining backtick here as defense in * and `markdownSafeBranch` strips any remaining backtick here as defense in
* depth, so JSON.stringify's quote/escape handling is sufficient and the * 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 // 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. // it with a bootstrap command it could not run.
const bootstrapNote = const bootstrapNote =
`No \`${runnerPath}\` yet? Bootstrap with \`npx\`, \`bunx\`, or \`pnpm dlx\` — ` + `No \`${runnerPath}\`? Bootstrap with \`npx\`, \`bunx\`, or \`pnpm dlx\`; ` +
'e.g. `bunx gitnexus@latest analyze` (npm 11 npx crash; #1939).'; 'e.g. `bunx gitnexus@latest analyze` (#1939).';
// This block is injected into every user's repo and its total size is capped // 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 // 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)`}. 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. > 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 ## 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 hasPdg
? ` For unified PDG impact, add \`mode: "pdg"\` with optional \`line: <N>\` — 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 <N> --repo .\`.` ? ` For unified PDG impact, add \`mode: "pdg"\` with optional \`line: <N>\` — 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 <N> --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 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 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.${ - **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 * Refresh the regression-review target inside the GitNexus block of an
* already-generated AGENTS.md / CLAUDE.md, in place (#1996 tri-review P2). * already-generated AGENTS.md / CLAUDE.md, in place (#1996 P2, #3509).
* *
* The `alreadyUpToDate` analyze fast path returns before the normal * The `alreadyUpToDate` analyze fast path returns before the normal
* {@link generateAIContextFiles} call, so a changed `.gitnexusrc` defaultBranch * {@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 — * block — including community-skill rows written by a prior `--skills` run —
* rather than regenerating (which would drop those rows on a no-`--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 * Legacy blocks that passed the branch tip as `base_ref` are migrated to the
* `base_ref` line (e.g. a user-trimmed keep block) are silently skipped. Writes * 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. * only when the value actually changes, so a routine up-to-date run is a no-op.
*/ */
export async function refreshBaseRefLine( export async function refreshBaseRefLine(
@ -746,7 +757,6 @@ export async function refreshBaseRefLine(
options?: { skipAgentsMd?: boolean }, options?: { skipAgentsMd?: boolean },
): Promise<{ files: string[] }> { ): Promise<{ files: string[] }> {
if (options?.skipAgentsMd) return { files: [] }; if (options?.skipAgentsMd) return { files: [] };
const replacement = `base_ref: ${JSON.stringify(markdownSafeBranch(defaultBranch))}`;
const updated: string[] = []; const updated: string[] = [];
for (const name of ['AGENTS.md', 'CLAUDE.md']) { for (const name of ['AGENTS.md', 'CLAUDE.md']) {
const filePath = path.join(repoPath, name); const filePath = path.join(repoPath, name);
@ -763,9 +773,36 @@ export async function refreshBaseRefLine(
if (endIdx === -1 || endIdx <= startIdx) continue; if (endIdx === -1 || endIdx <= startIdx) continue;
const blockEnd = endIdx + GITNEXUS_END_MARKER.length; const blockEnd = endIdx + GITNEXUS_END_MARKER.length;
const block = content.substring(startIdx, blockEnd); const block = content.substring(startIdx, blockEnd);
// Only the generated regression example carries a base_ref line, and only // Preserve the runner emitted with the block. Its location follows the
// one per block; replace its quoted value while leaving the rest untouched. // configured storage path, so assuming `.gitnexus/run.cjs` would break the
const newBlock = block.replace(/base_ref: "(?:[^"\\]|\\.)*"/, replacement); // 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: "<merge-base SHA>"')) {
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: "<merge-base SHA>"\}\)`(?: or `[^`\n]+? detect-changes --scope compare --base-ref "<merge-base SHA>" --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 if (newBlock === block) continue; // no base_ref line present, or already current
const newContent = content.substring(0, startIdx) + newBlock + content.substring(blockEnd); const newContent = content.substring(0, startIdx) + newBlock + content.substring(blockEnd);
try { try {

View file

@ -1663,7 +1663,7 @@ const analyzeCommandImpl = async (
} }
if (baseRefRefreshed.length > 0) { if (baseRefRefreshed.length > 0) {
console.log( 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 // #2639: opt-in self-commit of any AGENTS.md/CLAUDE.md churn from this

View file

@ -191,8 +191,13 @@ describe('generateAIContextFiles', () => {
); );
expect(content).toContain('detect_changes({scope: "all"})'); expect(content).toContain('detect_changes({scope: "all"})');
expect(content).toContain('node .gitnexus/run.cjs detect-changes --scope all --repo .'); 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('target branch "develop"');
expect(content).toContain('--scope compare --base-ref "develop" --repo .'); 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: "<merge-base SHA>"})',
);
expect(content).toContain('--scope compare --base-ref "<merge-base SHA>" --repo .');
expect(content).toContain('Never substitute grep for graph analysis'); expect(content).toContain('Never substitute grep for graph analysis');
expect(content).not.toContain('gitnexus impact --target'); 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 stats = { nodes: 50, edges: 100, processes: 5 };
const develop = generateGitNexusContent('P', stats, { defaultBranch: 'develop' }); const develop = generateGitNexusContent('P', stats, { defaultBranch: 'develop' });
expect(develop).toContain('base_ref: "develop"'); expect(develop).toContain('target branch "develop" from its remote');
expect(develop).not.toContain('base_ref: "main"'); 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: "<merge-base SHA>"');
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 }); 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: "<merge-base SHA>"');
expect(content).not.toContain('base_ref: "main"');
}); });
it('references MCP tools by their registered (unprefixed) names (#2059)', () => { 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 // A branch name with a double-quote must be JSON-escaped, not concatenated
// raw, so it stays inside the inline code span. // raw, so it stays inside the inline code span.
const content = generateGitNexusContent('P', { nodes: 1 }, { defaultBranch: 'we"ird' }); const content = generateGitNexusContent('P', { nodes: 1 }, { defaultBranch: 'we"ird' });
expect(content).toContain('base_ref: "we\\"ird"'); expect(content).toContain('target branch "we\\"ird"');
expect(content).toContain('--base-ref "we\\"ird" --repo .'); expect(content).toContain('--base-ref "<merge-base SHA>" --repo .');
}); });
it('a backtick branch cannot break the generated Markdown code span (#1996 P1)', () => { 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 // The branch is embedded inside a backtick inline-code span; a stray
// backtick would close it early. markdownSafeBranch strips it at the sink. // backtick would close it early. markdownSafeBranch strips it at the sink.
const content = generateGitNexusContent('P', { nodes: 1 }, { defaultBranch: 'main`evil' }); 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 // Even backtick count ⇒ every span is balanced (the regression line opens
// and closes exactly one). // and closes exactly one).
expect((line.match(/`/g) || []).length % 2).toBe(0); expect((line.match(/`/g) || []).length % 2).toBe(0);
expect(line).not.toContain('main`evil'); expect(line).not.toContain('main`evil');
expect(line).toContain('target branch "mainevil" from its remote');
expect(markdownSafeBranch('a`b`c')).toBe('abc'); 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-')); const dir = await fs.mkdtemp(path.join(os.tmpdir(), 'gn-baseref-'));
try { try {
// Seed a realistic block: a configured base_ref "main" plus a community // 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:start --> <!-- gitnexus:start -->
# GitNexus — Code Intelligence # 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 | | 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']) { for (const f of ['AGENTS.md', 'CLAUDE.md']) {
const after = await fs.readFile(path.join(dir, f), 'utf-8'); 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: "<merge-base SHA>"');
expect(after).not.toContain('base_ref: "develop"');
expect(after).not.toContain('base_ref: "main"'); expect(after).not.toContain('base_ref: "main"');
// The community-skill row (and everything else) is preserved. // The community-skill row (and everything else) is preserved.
expect(after).toContain('.claude/skills/gitnexus-area-auth/SKILL.md'); 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 }); const skipped = await refreshBaseRefLine(dir, 'master', { skipAgentsMd: true });
expect(skipped.files).toEqual([]); expect(skipped.files).toEqual([]);
expect(await fs.readFile(path.join(dir, 'AGENTS.md'), 'utf-8')).toContain( expect(await fs.readFile(path.join(dir, 'AGENTS.md'), 'utf-8')).toContain(
'base_ref: "develop"', 'target branch "develop"',
); );
} finally { } finally {
await fs.rm(dir, { recursive: true, force: true }); 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-')); const dir = await fs.mkdtemp(path.join(os.tmpdir(), 'gn-baseref-noop-'));
try { try {
// No AGENTS.md/CLAUDE.md at all → no files updated, no throw. // 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'); await fs.writeFile(path.join(dir, 'CLAUDE.md'), seed, 'utf-8');
expect((await refreshBaseRefLine(dir, 'develop')).files).toEqual([]); expect((await refreshBaseRefLine(dir, 'develop')).files).toEqual([]);
expect(await fs.readFile(path.join(dir, 'CLAUDE.md'), 'utf-8')).toBe(seed); expect(await fs.readFile(path.join(dir, 'CLAUDE.md'), 'utf-8')).toBe(seed);
const custom = `<!-- gitnexus:start -->
Custom target branch "main" and base_ref: "main" notes.
<!-- gitnexus:end -->
`;
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 = `<!-- gitnexus:start -->
Before release, run \`detect_changes({scope: "compare", base_ref: "main"})\` and inspect every returned flow.
<!-- gitnexus:end -->
`;
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: "<merge-base SHA>"');
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 = `<!-- gitnexus:start -->
For regression review: \`detect_changes({scope: "compare", base_ref: "main"})\` or \`.external-index/run.cjs detect-changes --scope compare --base-ref "main" --repo .\`.
<!-- gitnexus:end -->
`;
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: "<merge-base SHA>"');
} 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 = `<!-- gitnexus:start -->
For regression review: \`detect_changes({scope: "compare", base_ref: "main"})\`. Always inspect src/security.ts before release.
<!-- gitnexus:end -->
`;
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 =
'<!-- gitnexus:start -->\n' +
`Regression base: ${source}; target branch "main" via tracking remote. Fetch both; pass merge-base SHA to ` +
'`detect_changes({scope: "compare", base_ref: "<merge-base SHA>"})`. Keep this instruction.\n' +
'<!-- gitnexus:end -->\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 = `<!-- gitnexus:start -->
For regression review: \`detect_changes({scope: "compare", base_ref: "main"})\`.
<!-- gitnexus:end -->
`;
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 = `<!-- gitnexus:start -->
- run detect_changes({scope: "compare", base_ref: "main"})
<!-- gitnexus:end -->
`;
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 { } finally {
await fs.rm(dir, { recursive: true, force: true }); await fs.rm(dir, { recursive: true, force: true });
} }
@ -1601,8 +1766,9 @@ Indexed as **P**. Custom.
}); });
for (const f of ['CLAUDE.md', 'AGENTS.md']) { for (const f of ['CLAUDE.md', 'AGENTS.md']) {
const content = await fs.readFile(path.join(subDir, f), 'utf-8'); const content = await fs.readFile(path.join(subDir, f), 'utf-8');
expect(content).toContain('base_ref: "release/1.0"'); expect(content).toContain('target branch "release/1.0"');
expect(content).not.toContain('base_ref: "main"'); expect(content).toContain('base_ref: "<merge-base SHA>"');
expect(content).not.toContain('base_ref: "release/1.0"');
} }
} finally { } finally {
await fs.rm(subDir, { recursive: true, force: true }); await fs.rm(subDir, { recursive: true, force: true });