diff --git a/.claude/skills/contributing-to-loopover/SKILL.md b/.claude/skills/contributing-to-loopover/SKILL.md index da4d06c6b8..077a674353 100644 --- a/.claude/skills/contributing-to-loopover/SKILL.md +++ b/.claude/skills/contributing-to-loopover/SKILL.md @@ -104,9 +104,11 @@ gittensory-mcp init-client --print claude # or --print cursor — prints the co Use that generator instead of hand-writing config (**Codex uses TOML, Claude/Cursor use JSON** — a pasted JSON block will not work in Codex). You'll use these tools in Phases 1 and 6 (inputs in -`reference.md`): `gittensory_check_before_start`, `gittensory_validate_linked_issue`, -`gittensory_check_slop_risk`, `gittensory_lint_pr_text`, `gittensory_predict_gate` — all metadata-only -(no source upload, no secrets). +`reference.md`): `loopover_check_before_start`, `loopover_validate_linked_issue`, +`loopover_check_slop_risk`, `loopover_lint_pr_text`, `gittensory_predict_gate` — all metadata-only +(no source upload, no secrets). The first four are the new `loopover_`-prefixed primary names (#4775); +their old `gittensory_`-prefixed names still work, unchanged, as deprecated aliases for one full +minor-version deprecation cycle. --- @@ -116,14 +118,14 @@ pasted JSON block will not work in Codex). You'll use these tools in Phases 1 an signal. And only link an issue that is **open, not assigned to someone else, not maintainer-only, and (on scored repos) carries a point label**: linking an owner-assigned / maintainer-only / ineligible issue trips a **deterministic linked-issue hard rule that auto-closes your PR**. Verify - with `gittensory_check_before_start` + `gittensory_validate_linked_issue`. + with `loopover_check_before_start` + `loopover_validate_linked_issue`. - **A linked, currently-open, unassigned, eligible issue is always required before opening a PR** — there is no "small enough to skip it" exemption, no matter how self-evident the fix looks. This holds regardless of what the committed root `.gittensory.yml`'s `linkedIssuePolicy` says: that file is a non-representative **example** checked into the repo, not the live enforced rule. If no suitable open issue exists, open one yourself first, then link it from the PR. -- **Run the pre-start checks** via MCP: `gittensory_check_before_start` (is it claimed / a duplicate - cluster / already solved?) and, if linking an issue, `gittensory_validate_linked_issue`. +- **Run the pre-start checks** via MCP: `loopover_check_before_start` (is it claimed / a duplicate + cluster / already solved?) and, if linking an issue, `loopover_validate_linked_issue`. - **Stay in scope.** The gate's `wantedPaths` are `src/`, `packages/`, `test/`, `migrations/`, `scripts/`, `review-enrichment/`, `.github/workflows/`, `wrangler.jsonc`, `apps/gittensory-ui/`. Avoid `blockedPaths` (`site/`, `CNAME`, `**/lovable/**`). Keep the PR narrow — one coherent change. @@ -243,9 +245,9 @@ named **`Gittensory Orb Review Agent`** — watch both go green/passing. Run the MCP predictor with your actual PR shape: -- `gittensory_check_slop_risk` — keep slop **low**: fill the PR description, include tests, keep the +- `loopover_check_slop_risk` — keep slop **low**: fill the PR description, include tests, keep the diff focused (no lockfile/docs/generated noise dominating), real source ratio. -- `gittensory_lint_pr_text` — your commit + PR body must read as **strong**: Conventional Commit +- `loopover_lint_pr_text` — your commit + PR body must read as **strong**: Conventional Commit subject, traceability (a linked, currently-open, eligible issue — no no-issue rationale accepted), and a body that says what changed, why, and how it was validated. - `gittensory_predict_gate` — simulate the repo's public `.gittensory.yml` gate. Resolve any diff --git a/.claude/skills/contributing-to-loopover/reference.md b/.claude/skills/contributing-to-loopover/reference.md index 6bd31a49e3..878e4ec4bc 100644 --- a/.claude/skills/contributing-to-loopover/reference.md +++ b/.claude/skills/contributing-to-loopover/reference.md @@ -110,7 +110,7 @@ Implications for you: a coverage miss closes the PR. This is why Phases 3–5 are non-negotiable. - **A merge conflict closes the PR** — keep your branch current with `main`. - **Linking the wrong issue closes the PR** — only link an open, unassigned, eligible issue (verify - with `gittensory_validate_linked_issue`). + with `loopover_validate_linked_issue`, aliased for one deprecation cycle as `gittensory_validate_linked_issue`). - Owner / automation-bot PRs are exempt from auto-close, and crucial guarded-path PRs are held — but **assume you are a contributor** and that adverse = close. @@ -149,30 +149,33 @@ gittensory-mcp init-client --print codex # → ~/.codex/config.toml ([mcp_se gittensory-mcp init-client --print claude # or --print cursor (→ mcpServers JSON) ``` -All tools are metadata-only (no source upload). Run in this order: +All tools are metadata-only (no source upload). Each local-package tool below is now primarily named +`loopover_*` (#4775) — the old `gittensory_*` name still works, unchanged, as a deprecated alias for +one full minor-version deprecation cycle. Run in this order: -1. `gittensory_check_before_start` — `{owner, repo, issueNumber, plannedChange{title, paths}}` → - go/raise/avoid (claimed? duplicate cluster? already solved?). -2. `gittensory_validate_linked_issue` — `{owner, repo, issueNumber, plannedChange}` → is the issue - open, valid, single-owner, solvable by this PR. -3. `gittensory_check_slop_risk` — `{changedFiles[{path,additions,deletions}], description, tests, +1. `loopover_check_before_start` (was `gittensory_check_before_start`) — `{owner, repo, issueNumber, + plannedChange{title, paths}}` → go/raise/avoid (claimed? duplicate cluster? already solved?). +2. `loopover_validate_linked_issue` (was `gittensory_validate_linked_issue`) — `{owner, repo, + issueNumber, plannedChange}` → is the issue open, valid, single-owner, solvable by this PR. +3. `loopover_check_slop_risk` (was `gittensory_check_slop_risk`) — `{changedFiles[{path,additions,deletions}], description, tests, testFiles}` → band + findings. 4. `gittensory_check_improvement_potential` — `{changedFiles?[{path,additions,deletions}], tests?, testFiles?, patchCoverageDeltaPercent?, complexityDeltas?[{file,line,name,before,after,delta}], duplicationDeltas?[{file,line,duplicateOfLine,lines}]}` → improvementScore + band (insufficient-signal/none/minor/moderate/significant) + findings. The positive-axis mirror of - `gittensory_check_slop_risk` — deterministic tier only (no LLM judgment); complexityDeltas/ + `loopover_check_slop_risk` — deterministic tier only (no LLM judgment); complexityDeltas/ duplicationDeltas are optional precomputed deltas the calling agent supplies, never raw source. -5. `gittensory_lint_pr_text` — `{commitMessages[], prBody, linkedIssue}` → verdict +5. `loopover_lint_pr_text` (was `gittensory_lint_pr_text`) — `{commitMessages[], prBody, linkedIssue}` → verdict strong/adequate/weak + specific fixes. -6. `gittensory_validate_config` — `{content, source?}` → normalized manifest fields, +6. `loopover_validate_config` (was `gittensory_validate_config`) — `{content, source?}` → normalized manifest fields, warnings, and ok/warn/error status. 7. `gittensory_predict_gate` — `{login, owner, repo, title, body, labels, linkedIssues}` → predicted conclusion + blockers + warnings + readiness score. -(Auth'd extras: `gittensory_preflight_pr` / `…_local_diff` for lane fit + collision + queue health; -`gittensory_get_pr_ai_review_findings` — `{login, owner, repo, pullNumber}` → structured post-submission -AI-review inline findings (category/path/severity) for your own PR.) +(Auth'd extras: `loopover_preflight_pr` / `…_local_diff` (was `gittensory_preflight_pr` / `…_local_diff`) +for lane fit + collision + queue health; `gittensory_get_pr_ai_review_findings` — `{login, owner, repo, +pullNumber}` → structured post-submission AI-review inline findings (category/path/severity) for your +own PR.) --- diff --git a/packages/gittensory-mcp/README.md b/packages/gittensory-mcp/README.md index faf3fffe01..3681be958d 100644 --- a/packages/gittensory-mcp/README.md +++ b/packages/gittensory-mcp/README.md @@ -142,7 +142,7 @@ The report has an `overallStatus` (`pass`/`warn`/`fail`) and a `sections` array degrades to `fail` with a public-safe `slopRiskError`/`prTextLintError` reason instead of aborting the whole report — the other sections still return. -The same composed check is exposed to MCP clients as `gittensory_review_pr_before_push`. +The same composed check is exposed to MCP clients as `loopover_review_pr_before_push` (aliased for one deprecation cycle as `gittensory_review_pr_before_push`). ## Auth @@ -182,11 +182,13 @@ gittensory-mcp agent packet --login jsonbored --repo we-promise/sure --base orig The same capabilities are exposed to MCP clients as: -- `gittensory_agent_plan_next_work` -- `gittensory_agent_start_run` -- `gittensory_agent_get_run` -- `gittensory_agent_explain_next_action` -- `gittensory_agent_prepare_pr_packet` +- `loopover_agent_plan_next_work` +- `loopover_agent_start_run` +- `loopover_agent_get_run` +- `loopover_agent_explain_next_action` +- `loopover_agent_prepare_pr_packet` + +(Each is aliased for one deprecation cycle under its old `gittensory_agent_*` name.) ### Client config @@ -271,7 +273,7 @@ gittensory-mcp changelog ## Offline decision-pack fallback -Successful `decision-pack` and MCP `gittensory_get_decision_pack` calls store a bounded last-good local cache entry keyed by API version and login. If the API or network is temporarily unavailable, the wrapper can return that last-good guidance as `source: "local_cache"` with `stale: true`, `cachedAt`, and rerun guidance. Auth and permission failures do not use stale fallback data. +Successful `decision-pack` and MCP `loopover_get_decision_pack` calls (aliased for one deprecation cycle as `gittensory_get_decision_pack`) store a bounded last-good local cache entry keyed by API version and login. If the API or network is temporarily unavailable, the wrapper can return that last-good guidance as `source: "local_cache"` with `stale: true`, `cachedAt`, and rerun guidance. Auth and permission failures do not use stale fallback data. The cache excludes source contents and local paths, is bounded, and can be removed with: diff --git a/packages/gittensory-mcp/bin/gittensory-mcp.js b/packages/gittensory-mcp/bin/gittensory-mcp.js index aeb9c7ec33..53d6d30838 100755 --- a/packages/gittensory-mcp/bin/gittensory-mcp.js +++ b/packages/gittensory-mcp/bin/gittensory-mcp.js @@ -427,161 +427,180 @@ const agentRunIdShape = { // Single source of truth for stdio tool name + one-line description (#2233). // Registration and `gittensory-mcp tools` both read this list. +// #4775: these are the canonical loopover_-prefixed primary names. Each also gets a thin, +// fully-working gittensory_-prefixed deprecated alias — see ALL_STDIO_TOOL_DESCRIPTORS below. const STDIO_TOOL_DESCRIPTORS = [ { - name: "gittensory_get_repo_context", + name: "loopover_get_repo_context", description: "Return the canonical repo intelligence bundle from the private Gittensory API.", }, { - name: "gittensory_get_maintainer_noise", + name: "loopover_get_maintainer_noise", description: "Return the maintainer queue-noise triage report for a repo: a noise score/level, the specific noise sources to clear first, and recommended maintainer actions. Maintainer-authenticated; advisory only.", }, { - name: "gittensory_preflight_pr", + name: "loopover_preflight_pr", description: "Preflight planned PR metadata against lane, duplicate, linked issue, test, and queue signals.", }, { - name: "gittensory_validate_linked_issue", + name: "loopover_validate_linked_issue", description: "Report whether linking an issue will actually earn the standard linked-issue scoring multiplier for a planned PR — open, valid, single-owner, solvable by this PR — with the blocking reason if not. The raw multiplier value stays private.", }, { - name: "gittensory_check_before_start", + name: "loopover_check_before_start", description: "Before writing any code, check whether an issue is already claimed or solved, whether a duplicate cluster is forming, and whether it is a valid target. Returns a go/raise/avoid recommendation with public-safe reasons from cached metadata.", }, { - name: "gittensory_find_opportunities", + name: "loopover_find_opportunities", description: "Cross-repo discovery: find high-fit contribution opportunities across registered Gittensor repos. Returns a ranked, public-safe list filtered by your MinerGoalSpec (lane, min rank score, languages). Metadata-only, no GitHub writes.", }, { - name: "gittensory_retrieve_issue_context", + name: "loopover_retrieve_issue_context", description: "Repo-scoped issue-centric RAG retrieval for the miner analyze phase. Returns related file paths and retrieval scores from issue title/body/labels — metadata only, never source text.", }, { - name: "gittensory_lint_pr_text", + name: "loopover_lint_pr_text", description: "Lint a commit message + PR body against the gittensor traceability/no-issue-rationale and Conventional Commit rubric before submitting. Returns a deterministic verdict (strong/adequate/weak) plus specific public-safe fixes. No source upload.", }, { - name: "gittensory_validate_config", + name: "loopover_validate_config", description: "Parse and validate a .gittensory.yml manifest string using the same focus-manifest parser as the server. Returns normalized config fields, parse warnings, and an ok/warn/error status. Metadata-only, no GitHub writes.", }, { - name: "gittensory_check_slop_risk", + name: "loopover_check_slop_risk", description: "Assess the deterministic slop risk of a planned change from local diff metadata (paths + line counts) + the PR description — an agent-native, source-free quality self-check. Returns slopRisk (0-100), band, findings, and the rubric. No repo data needed.", }, { - name: "gittensory_check_issue_slop", + name: "loopover_check_issue_slop", description: "Assess the deterministic slop risk of an issue from its title + body alone (no repo data) — flags clearly low-effort issues (empty body, an unfilled template) for triage. Returns slopRisk (0-100), band, findings, and the rubric. Advisory-only.", }, { - name: "gittensory_preflight_local_diff", + name: "loopover_preflight_local_diff", description: "Inspect local git diff metadata and run Gittensory preflight without uploading source contents.", }, { - name: "gittensory_get_registry_changes", + name: "loopover_get_registry_changes", description: "Return latest cached Gittensor registry change report.", }, { - name: "gittensory_get_upstream_drift", + name: "loopover_get_upstream_drift", description: "Return the latest cached Gittensor upstream ruleset drift status (stale/drift warnings) for MCP planning.", }, { - name: "gittensory_get_label_audit", + name: "loopover_get_label_audit", description: "Return the repo's label-policy audit (configured-vs-live labels, missing configured labels, suspicious status/source-style labels, and trusted-label-pipeline readiness) from the private Gittensory API.", }, { - name: "gittensory_get_burden_forecast", + name: "loopover_get_burden_forecast", description: "Return the repo's cached maintainer burden forecast (projected review load, queue-growth risk, and stale-PR signals) with a freshness marker, from the private Gittensory API.", }, { - name: "gittensory_preview_local_pr_score", + name: "loopover_preview_local_pr_score", description: "Inspect local diff metadata and request a private Gittensory scoring preview. No source contents are uploaded.", }, { - name: "gittensory_explain_score_breakdown", + name: "loopover_explain_score_breakdown", description: "Explain a private score preview multiplier-by-multiplier with plain-English levers and the highest-impact improvement.", }, { - name: "gittensory_get_decision_pack", + name: "loopover_get_decision_pack", description: "Return the canonical private contributor decision pack for a GitHub login.", }, { - name: "gittensory_explain_repo_decision", + name: "loopover_explain_repo_decision", description: "Return the contributor/repo decision from the canonical decision pack.", }, { - name: "gittensory_compare_pr_variants", + name: "loopover_compare_pr_variants", description: "Compare private Gittensory scoring previews across local/metadata variants.", }, { - name: "gittensory_local_status", + name: "loopover_local_status", description: "Return local Gittensory MCP status, inferred git repo metadata, and privacy defaults.", }, { - name: "gittensory_preflight_current_branch", + name: "loopover_preflight_current_branch", description: "Analyze the current git branch and return PR readiness. Sends metadata only.", }, { - name: "gittensory_review_pr_before_push", + name: "loopover_review_pr_before_push", description: "Run a single composed pre-PR review of the current branch: preflight (lane/duplicate/linked-issue/test/queue fit), slop-risk, and PR-text lint, merged into one report with an overall pass/warn/fail status. Thin composition of the existing checks — does not reimplement any of them. Sends metadata only, no source upload.", }, { - name: "gittensory_preview_current_branch_score", + name: "loopover_preview_current_branch_score", description: "Analyze the current git branch and return private scoreability context. Sends metadata only.", }, { - name: "gittensory_rank_local_next_actions", + name: "loopover_rank_local_next_actions", description: "Analyze the current git branch and rank local next actions by private reward/risk and review friction.", }, { - name: "gittensory_explain_local_blockers", + name: "loopover_explain_local_blockers", description: "Analyze the current git branch and explain private scoreability, lane, and review blockers.", }, { - name: "gittensory_remediation_plan", + name: "loopover_remediation_plan", description: "Analyze the current git branch and return an ordered public-safe remediation checklist with rerun conditions.", }, { - name: "gittensory_prepare_pr_packet", + name: "loopover_prepare_pr_packet", description: "Analyze the current git branch and return a public-safe PR packet. Sends metadata only.", }, { - name: "gittensory_compare_local_variants", + name: "loopover_compare_local_variants", description: "Compare current-branch metadata variants without uploading source contents.", }, { - name: "gittensory_agent_plan_next_work", + name: "loopover_agent_plan_next_work", description: "Run the deterministic Gittensory base-agent planner for a GitHub login.", }, { - name: "gittensory_agent_start_run", + name: "loopover_agent_start_run", description: "Create a queued copilot-only Gittensory base-agent run.", }, { - name: "gittensory_agent_get_run", + name: "loopover_agent_get_run", description: "Fetch a persisted Gittensory base-agent run.", }, { - name: "gittensory_agent_explain_next_action", + name: "loopover_agent_explain_next_action", description: "Explain the next deterministic action and blocker context for a GitHub login.", }, { - name: "gittensory_agent_prepare_pr_packet", + name: "loopover_agent_prepare_pr_packet", description: "Prepare a public-safe PR packet from current branch metadata. Sends metadata only.", }, { - name: "gittensory_local_status_structured", + name: "loopover_local_status_structured", description: "Return local Gittensory MCP status with a validated structured output schema.", }, { - name: "gittensory_feasibility_gate", + name: "loopover_feasibility_gate", description: "Pure local go/raise/avoid feasibility verdict from claim status, duplicate-cluster risk, and issue quality/lifecycle status — the same discriminants the analyze-phase feasibility gate branches on. When repoFullName/issueNumber are supplied and a local gittensory-miner install's claim ledger is present, claimStatus is read from that ledger instead of the caller-supplied value; otherwise falls back to the caller-supplied claimStatus unchanged. Advisory-only — never blocks, cancels, or overrides a claim or attempt; real claim-conflict resolution authority stays with the maintainer-only path. No API round-trip.", }, ]; +// #4775: derive the deprecated gittensory_-prefixed alias name for a loopover_-prefixed primary name. +function legacyAliasName(name) { + return name.replace(/^loopover_/, "gittensory_"); +} + +// #4775: every primary loopover_ tool plus its thin gittensory_ deprecated alias -- single source +// of truth for stdio tool name + description, aliases included. `gittensory-mcp tools` and +// `stdioToolDescription` both read this derived list so the alias count/descriptions stay in sync +// with STDIO_TOOL_DESCRIPTORS automatically. +const ALL_STDIO_TOOL_DESCRIPTORS = STDIO_TOOL_DESCRIPTORS.flatMap(({ name, description }) => [ + { name, description }, + { + name: legacyAliasName(name), + description: `${description} Deprecated: use \`${name}\` instead -- this alias will be removed in a future minor release.`, + }, +]); + function stdioToolDescription(name) { - const tool = STDIO_TOOL_DESCRIPTORS.find((entry) => entry.name === name); + const tool = ALL_STDIO_TOOL_DESCRIPTORS.find((entry) => entry.name === name); if (!tool) throw new Error(`Unknown stdio tool descriptor: ${name}`); return tool.description; } @@ -596,10 +615,21 @@ const server = new McpServer({ version: packageVersion, }); -server.registerTool( - "gittensory_get_repo_context", +// #4775: register a tool under its new loopover_ primary name, plus a thin, fully-working +// gittensory_ alias with a deprecation notice appended to its description. Both names share the +// exact same handler function reference -- identical behavior, no duplicated logic. Safe because +// McpServer#registerTool keys tools purely by name string (no shared mutable state per name) and +// no handler in this file reads back its own registered tool name. +function registerToolWithLegacyAlias(name, config, handler) { + server.registerTool(name, config, handler); + const legacyName = legacyAliasName(name); + server.registerTool(legacyName, { ...config, description: stdioToolDescription(legacyName) }, handler); +} + +registerToolWithLegacyAlias( + "loopover_get_repo_context", { - description: stdioToolDescription("gittensory_get_repo_context"), + description: stdioToolDescription("loopover_get_repo_context"), inputSchema: ownerRepoShape, }, async ({ owner, repo }) => { @@ -608,10 +638,10 @@ server.registerTool( }, ); -server.registerTool( - "gittensory_get_maintainer_noise", +registerToolWithLegacyAlias( + "loopover_get_maintainer_noise", { - description: stdioToolDescription("gittensory_get_maintainer_noise"), + description: stdioToolDescription("loopover_get_maintainer_noise"), inputSchema: ownerRepoShape, }, async ({ owner, repo }) => { @@ -620,19 +650,19 @@ server.registerTool( }, ); -server.registerTool( - "gittensory_preflight_pr", +registerToolWithLegacyAlias( + "loopover_preflight_pr", { - description: stdioToolDescription("gittensory_preflight_pr"), + description: stdioToolDescription("loopover_preflight_pr"), inputSchema: preflightShape, }, async (input) => toolResult("Gittensory PR preflight.", await apiPost("/v1/preflight/pr", input)), ); -server.registerTool( - "gittensory_validate_linked_issue", +registerToolWithLegacyAlias( + "loopover_validate_linked_issue", { - description: stdioToolDescription("gittensory_validate_linked_issue"), + description: stdioToolDescription("loopover_validate_linked_issue"), inputSchema: validateLinkedIssueShape, }, async ({ owner, repo, issueNumber, plannedChange }) => { @@ -642,10 +672,10 @@ server.registerTool( }, ); -server.registerTool( - "gittensory_check_before_start", +registerToolWithLegacyAlias( + "loopover_check_before_start", { - description: stdioToolDescription("gittensory_check_before_start"), + description: stdioToolDescription("loopover_check_before_start"), inputSchema: checkBeforeStartShape, }, async ({ owner, repo, issueNumber, title, plannedPaths }) => { @@ -659,10 +689,10 @@ server.registerTool( }, ); -server.registerTool( - "gittensory_find_opportunities", +registerToolWithLegacyAlias( + "loopover_find_opportunities", { - description: stdioToolDescription("gittensory_find_opportunities"), + description: stdioToolDescription("loopover_find_opportunities"), inputSchema: findOpportunitiesShape, }, async ({ targets, searchQuery, goalSpec, limit }) => { @@ -676,10 +706,10 @@ server.registerTool( }, ); -server.registerTool( - "gittensory_retrieve_issue_context", +registerToolWithLegacyAlias( + "loopover_retrieve_issue_context", { - description: stdioToolDescription("gittensory_retrieve_issue_context"), + description: stdioToolDescription("loopover_retrieve_issue_context"), inputSchema: issueRagShape, }, async ({ owner, repo, title, body, labels, topK }) => { @@ -695,46 +725,46 @@ server.registerTool( }, ); -server.registerTool( - "gittensory_lint_pr_text", +registerToolWithLegacyAlias( + "loopover_lint_pr_text", { - description: stdioToolDescription("gittensory_lint_pr_text"), + description: stdioToolDescription("loopover_lint_pr_text"), inputSchema: lintPrTextShape, }, async (input) => toolResult("Gittensory PR-text lint.", await apiPost("/v1/lint/pr-text", input)), ); -server.registerTool( - "gittensory_validate_config", +registerToolWithLegacyAlias( + "loopover_validate_config", { - description: stdioToolDescription("gittensory_validate_config"), + description: stdioToolDescription("loopover_validate_config"), inputSchema: validateConfigShape, }, async (input) => toolResult("Gittensory manifest validation.", await apiPost("/v1/validate/focus-manifest", input)), ); -server.registerTool( - "gittensory_check_slop_risk", +registerToolWithLegacyAlias( + "loopover_check_slop_risk", { - description: stdioToolDescription("gittensory_check_slop_risk"), + description: stdioToolDescription("loopover_check_slop_risk"), inputSchema: checkSlopRiskShape, }, async (input) => toolResult("Gittensory slop-risk self-check.", await apiPost("/v1/lint/slop-risk", input)), ); -server.registerTool( - "gittensory_check_issue_slop", +registerToolWithLegacyAlias( + "loopover_check_issue_slop", { - description: stdioToolDescription("gittensory_check_issue_slop"), + description: stdioToolDescription("loopover_check_issue_slop"), inputSchema: checkIssueSlopShape, }, async (input) => toolResult("Gittensory issue-slop self-check.", await apiPost("/v1/lint/issue-slop", input)), ); -server.registerTool( - "gittensory_preflight_local_diff", +registerToolWithLegacyAlias( + "loopover_preflight_local_diff", { - description: stdioToolDescription("gittensory_preflight_local_diff"), + description: stdioToolDescription("loopover_preflight_local_diff"), inputSchema: localDiffShape, }, async (input) => { @@ -758,28 +788,28 @@ server.registerTool( }, ); -server.registerTool( - "gittensory_get_registry_changes", +registerToolWithLegacyAlias( + "loopover_get_registry_changes", { - description: stdioToolDescription("gittensory_get_registry_changes"), + description: stdioToolDescription("loopover_get_registry_changes"), inputSchema: {}, }, async () => toolResult("Gittensory registry changes.", await apiGet("/v1/registry/changes")), ); -server.registerTool( - "gittensory_get_upstream_drift", +registerToolWithLegacyAlias( + "loopover_get_upstream_drift", { - description: stdioToolDescription("gittensory_get_upstream_drift"), + description: stdioToolDescription("loopover_get_upstream_drift"), inputSchema: {}, }, async () => toolResult("Gittensory upstream drift status.", await apiGet("/v1/upstream/drift")), ); -server.registerTool( - "gittensory_get_label_audit", +registerToolWithLegacyAlias( + "loopover_get_label_audit", { - description: stdioToolDescription("gittensory_get_label_audit"), + description: stdioToolDescription("loopover_get_label_audit"), inputSchema: ownerRepoShape, }, async ({ owner, repo }) => { @@ -793,10 +823,10 @@ server.registerTool( }, ); -server.registerTool( - "gittensory_get_burden_forecast", +registerToolWithLegacyAlias( + "loopover_get_burden_forecast", { - description: stdioToolDescription("gittensory_get_burden_forecast"), + description: stdioToolDescription("loopover_get_burden_forecast"), inputSchema: ownerRepoShape, }, async ({ owner, repo }) => { @@ -811,19 +841,19 @@ server.registerTool( }, ); -server.registerTool( - "gittensory_preview_local_pr_score", +registerToolWithLegacyAlias( + "loopover_preview_local_pr_score", { - description: stdioToolDescription("gittensory_preview_local_pr_score"), + description: stdioToolDescription("loopover_preview_local_pr_score"), inputSchema: localScoreShape, }, async (input) => toolResult("Gittensory private local PR scoring preview.", await previewLocalScore(await withClientWorkspaceRoots(input))), ); -server.registerTool( - "gittensory_explain_score_breakdown", +registerToolWithLegacyAlias( + "loopover_explain_score_breakdown", { - description: stdioToolDescription("gittensory_explain_score_breakdown"), + description: stdioToolDescription("loopover_explain_score_breakdown"), inputSchema: localScoreShape, }, async (input) => { @@ -868,10 +898,10 @@ server.registerTool( }, ); -server.registerTool( - "gittensory_get_decision_pack", +registerToolWithLegacyAlias( + "loopover_get_decision_pack", { - description: stdioToolDescription("gittensory_get_decision_pack"), + description: stdioToolDescription("loopover_get_decision_pack"), inputSchema: loginShape, }, async ({ login }) => { @@ -880,10 +910,10 @@ server.registerTool( }, ); -server.registerTool( - "gittensory_explain_repo_decision", +registerToolWithLegacyAlias( + "loopover_explain_repo_decision", { - description: stdioToolDescription("gittensory_explain_repo_decision"), + description: stdioToolDescription("loopover_explain_repo_decision"), inputSchema: loginRepoShape, }, async ({ login, owner, repo }) => { @@ -892,10 +922,10 @@ server.registerTool( }, ); -server.registerTool( - "gittensory_compare_pr_variants", +registerToolWithLegacyAlias( + "loopover_compare_pr_variants", { - description: stdioToolDescription("gittensory_compare_pr_variants"), + description: stdioToolDescription("loopover_compare_pr_variants"), inputSchema: variantsShape, }, async ({ variants }) => { @@ -907,10 +937,10 @@ server.registerTool( }, ); -server.registerTool( - "gittensory_local_status", +registerToolWithLegacyAlias( + "loopover_local_status", { - description: stdioToolDescription("gittensory_local_status"), + description: stdioToolDescription("loopover_local_status"), inputSchema: { cwd: z.string().optional(), baseRef: z.string().optional(), @@ -943,10 +973,10 @@ server.registerTool( }, ); -server.registerTool( - "gittensory_preflight_current_branch", +registerToolWithLegacyAlias( + "loopover_preflight_current_branch", { - description: stdioToolDescription("gittensory_preflight_current_branch"), + description: stdioToolDescription("loopover_preflight_current_branch"), inputSchema: currentBranchShape, }, async (input) => { @@ -960,19 +990,19 @@ server.registerTool( }, ); -server.registerTool( - "gittensory_review_pr_before_push", +registerToolWithLegacyAlias( + "loopover_review_pr_before_push", { - description: stdioToolDescription("gittensory_review_pr_before_push"), + description: stdioToolDescription("loopover_review_pr_before_push"), inputSchema: currentBranchShape, }, async (input) => toolResult("Gittensory pre-PR review.", await reviewLocalPr(await withClientWorkspaceRoots(input))), ); -server.registerTool( - "gittensory_preview_current_branch_score", +registerToolWithLegacyAlias( + "loopover_preview_current_branch_score", { - description: stdioToolDescription("gittensory_preview_current_branch_score"), + description: stdioToolDescription("loopover_preview_current_branch_score"), inputSchema: currentBranchShape, }, async (input) => { @@ -987,10 +1017,10 @@ server.registerTool( }, ); -server.registerTool( - "gittensory_rank_local_next_actions", +registerToolWithLegacyAlias( + "loopover_rank_local_next_actions", { - description: stdioToolDescription("gittensory_rank_local_next_actions"), + description: stdioToolDescription("loopover_rank_local_next_actions"), inputSchema: currentBranchShape, }, async (input) => { @@ -999,10 +1029,10 @@ server.registerTool( }, ); -server.registerTool( - "gittensory_explain_local_blockers", +registerToolWithLegacyAlias( + "loopover_explain_local_blockers", { - description: stdioToolDescription("gittensory_explain_local_blockers"), + description: stdioToolDescription("loopover_explain_local_blockers"), inputSchema: currentBranchShape, }, async (input) => { @@ -1019,10 +1049,10 @@ server.registerTool( }, ); -server.registerTool( - "gittensory_remediation_plan", +registerToolWithLegacyAlias( + "loopover_remediation_plan", { - description: stdioToolDescription("gittensory_remediation_plan"), + description: stdioToolDescription("loopover_remediation_plan"), inputSchema: currentBranchShape, }, async (input) => { @@ -1033,10 +1063,10 @@ server.registerTool( }, ); -server.registerTool( - "gittensory_prepare_pr_packet", +registerToolWithLegacyAlias( + "loopover_prepare_pr_packet", { - description: stdioToolDescription("gittensory_prepare_pr_packet"), + description: stdioToolDescription("loopover_prepare_pr_packet"), inputSchema: currentBranchShape, }, async (input) => { @@ -1045,10 +1075,10 @@ server.registerTool( }, ); -server.registerTool( - "gittensory_compare_local_variants", +registerToolWithLegacyAlias( + "loopover_compare_local_variants", { - description: stdioToolDescription("gittensory_compare_local_variants"), + description: stdioToolDescription("loopover_compare_local_variants"), inputSchema: currentBranchVariantsShape, }, async ({ variants }) => { @@ -1072,19 +1102,19 @@ server.registerTool( }, ); -server.registerTool( - "gittensory_agent_plan_next_work", +registerToolWithLegacyAlias( + "loopover_agent_plan_next_work", { - description: stdioToolDescription("gittensory_agent_plan_next_work"), + description: stdioToolDescription("loopover_agent_plan_next_work"), inputSchema: agentPlanShape, }, async (input) => toolResult(`Gittensory base-agent plan for ${input.login}.`, await apiPost("/v1/agent/plan-next-work", input)), ); -server.registerTool( - "gittensory_agent_start_run", +registerToolWithLegacyAlias( + "loopover_agent_start_run", { - description: stdioToolDescription("gittensory_agent_start_run"), + description: stdioToolDescription("loopover_agent_start_run"), inputSchema: agentRunShape, }, async (input) => @@ -1103,19 +1133,19 @@ server.registerTool( ), ); -server.registerTool( - "gittensory_agent_get_run", +registerToolWithLegacyAlias( + "loopover_agent_get_run", { - description: stdioToolDescription("gittensory_agent_get_run"), + description: stdioToolDescription("loopover_agent_get_run"), inputSchema: agentRunIdShape, }, async ({ runId }) => toolResult(`Gittensory base-agent run ${runId}.`, await apiGet(`/v1/agent/runs/${encodeURIComponent(runId)}`)), ); -server.registerTool( - "gittensory_agent_explain_next_action", +registerToolWithLegacyAlias( + "loopover_agent_explain_next_action", { - description: stdioToolDescription("gittensory_agent_explain_next_action"), + description: stdioToolDescription("loopover_agent_explain_next_action"), inputSchema: agentPlanShape, }, async (input) => { @@ -1127,10 +1157,10 @@ server.registerTool( }, ); -server.registerTool( - "gittensory_agent_prepare_pr_packet", +registerToolWithLegacyAlias( + "loopover_agent_prepare_pr_packet", { - description: stdioToolDescription("gittensory_agent_prepare_pr_packet"), + description: stdioToolDescription("loopover_agent_prepare_pr_packet"), inputSchema: currentBranchShape, }, async (input) => toolResult("Gittensory base-agent public-safe PR packet.", await agentPreparePrPacket(await withClientWorkspaceRoots(input))), @@ -1199,10 +1229,10 @@ const agentPlanOutputSchema = { // Attach outputSchema to key tools via registerTool with zod output schemas. // All other tools continue to return unschematized text+structured content. -server.registerTool( - "gittensory_local_status_structured", +registerToolWithLegacyAlias( + "loopover_local_status_structured", { - description: stdioToolDescription("gittensory_local_status_structured"), + description: stdioToolDescription("loopover_local_status_structured"), inputSchema: { cwd: z.string().optional(), baseRef: z.string().optional(), @@ -1243,10 +1273,10 @@ server.registerTool( }, ); -server.registerTool( - "gittensory_feasibility_gate", +registerToolWithLegacyAlias( + "loopover_feasibility_gate", { - description: stdioToolDescription("gittensory_feasibility_gate"), + description: stdioToolDescription("loopover_feasibility_gate"), inputSchema: feasibilityGateShape, }, async ({ claimStatus, duplicateClusterRisk, issueStatus, found, repoFullName, issueNumber }) => { @@ -2381,7 +2411,7 @@ function printVersion(options) { } function toolsCommand(options) { - const tools = STDIO_TOOL_DESCRIPTORS.map(({ name, description }) => ({ name, description })); + const tools = ALL_STDIO_TOOL_DESCRIPTORS.map(({ name, description }) => ({ name, description })); const payload = { count: tools.length, tools }; if (options.json) { process.stdout.write(`${JSON.stringify(payload, null, 2)}\n`); diff --git a/test/unit/mcp-tool-rename-aliases.test.ts b/test/unit/mcp-tool-rename-aliases.test.ts new file mode 100644 index 0000000000..38058133e1 --- /dev/null +++ b/test/unit/mcp-tool-rename-aliases.test.ts @@ -0,0 +1,128 @@ +// #4775: MCP tool rename (gittensory_ -> loopover_). Every stdio tool now has a primary +// loopover_-prefixed name plus a thin, fully-working gittensory_-prefixed deprecated alias that +// shares the exact same handler. This suite verifies the acceptance criteria literally: calling a +// representative sample of tools by BOTH the old and new name produces IDENTICAL behavior (same +// result shape, same content), every legacy alias's description says it's deprecated and names its +// replacement, no primary loopover_ tool's description carries a deprecation notice, and the CLI's +// `tools --json` listing stays in lockstep with what the live server actually registers. +import { Client } from "@modelcontextprotocol/sdk/client/index.js"; +import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js"; +import { mkdtempSync, rmSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { afterEach, beforeEach, describe, expect, it } from "vitest"; +import { closeFixtureServer, run, startFixtureServer } from "./support/mcp-cli-harness"; + +const bin = join(process.cwd(), "packages/gittensory-mcp/bin/gittensory-mcp.js"); + +let client: Client; +let transport: StdioClientTransport; +let configDir: string; + +async function connect(apiUrl: string) { + configDir = mkdtempSync(join(tmpdir(), "gittensory-rename-alias-")); + transport = new StdioClientTransport({ + command: "node", + args: [bin, "--stdio"], + env: { + ...process.env, + GITTENSORY_CONFIG_DIR: configDir, + GITTENSORY_API_URL: apiUrl, + GITTENSORY_API_TIMEOUT_MS: "5000", + }, + }); + client = new Client({ name: "rename-alias-test", version: "0.0.1" }); + await client.connect(transport); +} + +async function disconnect() { + await client.close().catch(() => undefined); + await closeFixtureServer(); + if (configDir) rmSync(configDir, { recursive: true, force: true }); +} + +describe("MCP tool rename (#4775) — discovery invariants", () => { + beforeEach(async () => { + const apiUrl = await startFixtureServer(); + await connect(apiUrl); + }); + afterEach(disconnect); + + it("lists exactly 74 tools: 37 loopover_ primary names plus their 37 gittensory_ aliases", async () => { + const { tools } = await client.listTools(); + const names = tools.map((t) => t.name); + const primary = names.filter((n) => n.startsWith("loopover_")); + const legacy = names.filter((n) => n.startsWith("gittensory_")); + expect(primary.length).toBe(37); + expect(legacy.length).toBe(37); + expect(names.length).toBe(74); + // Every legacy alias has a corresponding primary name, and vice versa. + const primarySuffixes = new Set(primary.map((n) => n.slice("loopover_".length))); + const legacySuffixes = new Set(legacy.map((n) => n.slice("gittensory_".length))); + expect([...legacySuffixes].sort()).toEqual([...primarySuffixes].sort()); + }); + + it("every gittensory_ alias's description is marked deprecated and names its loopover_ replacement", async () => { + const { tools } = await client.listTools(); + for (const tool of tools.filter((t) => t.name.startsWith("gittensory_"))) { + const replacement = `loopover_${tool.name.slice("gittensory_".length)}`; + expect(tool.description ?? "", `${tool.name} description`).toMatch(/deprecated/i); + expect(tool.description ?? "", `${tool.name} description should name ${replacement}`).toContain(replacement); + } + }); + + it("no loopover_ primary tool's description carries a deprecation notice", async () => { + const { tools } = await client.listTools(); + for (const tool of tools.filter((t) => t.name.startsWith("loopover_"))) { + expect(tool.description ?? "", `${tool.name} description`).not.toMatch(/deprecated/i); + } + }); + + it("`gittensory-mcp tools --json` reports the same 74-tool count the live server registers", async () => { + const { tools } = await client.listTools(); + const payload = JSON.parse(run(["tools", "--json"])) as { count: number; tools: Array<{ name: string }> }; + expect(payload.count).toBe(tools.length); + expect(payload.count).toBe(74); + expect([...payload.tools.map((t) => t.name)].sort()).toEqual([...tools.map((t) => t.name)].sort()); + }); +}); + +describe("MCP tool rename (#4775) — old/new behavioral identity", () => { + beforeEach(async () => { + const apiUrl = await startFixtureServer(); + await connect(apiUrl); + }); + afterEach(disconnect); + + // Representative sample spanning distinct tool categories: an authenticated API GET proxy + // (repo intelligence), a source-free API POST self-check (slop risk), a no-argument API GET + // (upstream drift), an API GET with a path parameter (agent run), pure local logic with no + // network call (feasibility gate), and a tool with a validated Zod outputSchema (structured + // local status). + const cases: Array<{ label: string; newName: string; oldName: string; args: Record }> = [ + { label: "repo intelligence (API GET proxy)", newName: "loopover_get_repo_context", oldName: "gittensory_get_repo_context", args: { owner: "owner", repo: "repo" } }, + { label: "slop-risk self-check (API POST, source-free)", newName: "loopover_check_slop_risk", oldName: "gittensory_check_slop_risk", args: { description: "fix a bug", changedFiles: [{ path: "src/x.ts", additions: 3, deletions: 1 }] } }, + { label: "upstream drift (no-argument API GET)", newName: "loopover_get_upstream_drift", oldName: "gittensory_get_upstream_drift", args: {} }, + { label: "agent run lookup (API GET with path param)", newName: "loopover_agent_get_run", oldName: "gittensory_agent_get_run", args: { runId: "run-1" } }, + { label: "feasibility gate (pure local logic, no network)", newName: "loopover_feasibility_gate", oldName: "gittensory_feasibility_gate", args: { claimStatus: "unclaimed", duplicateClusterRisk: "none", issueStatus: "ready" } }, + { label: "structured local status (validated outputSchema)", newName: "loopover_local_status_structured", oldName: "gittensory_local_status_structured", args: {} }, + ]; + + it.each(cases)("$label: calling by the old name and the new name produces identical content", async ({ newName, oldName, args }) => { + const viaNew = await client.callTool({ name: newName, arguments: args }); + const viaOld = await client.callTool({ name: oldName, arguments: args }); + + expect(viaOld.isError).toBe(viaNew.isError); + expect(viaOld.content).toEqual(viaNew.content); + if (viaNew.structuredContent !== undefined || viaOld.structuredContent !== undefined) { + expect(viaOld.structuredContent).toEqual(viaNew.structuredContent); + } + }); + + it("local status (no outputSchema variant) also behaves identically under both names", async () => { + const viaNew = await client.callTool({ name: "loopover_local_status", arguments: {} }); + const viaOld = await client.callTool({ name: "gittensory_local_status", arguments: {} }); + expect(viaOld.isError).toBe(viaNew.isError); + expect(viaOld.content).toEqual(viaNew.content); + }); +});