diff --git a/.claude/skills/contributing-to-loopover/SKILL.md b/.claude/skills/contributing-to-loopover/SKILL.md index ed40691627..4e10ca0cb2 100644 --- a/.claude/skills/contributing-to-loopover/SKILL.md +++ b/.claude/skills/contributing-to-loopover/SKILL.md @@ -106,9 +106,7 @@ Use that generator instead of hand-writing config (**Codex uses TOML, Claude/Cur pasted JSON block will not work in Codex). You'll use these tools in Phases 1 and 6 (inputs in `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. +(no source upload, no secrets). --- diff --git a/.claude/skills/contributing-to-loopover/reference.md b/.claude/skills/contributing-to-loopover/reference.md index f8114ccf8e..676a16f074 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 `loopover_validate_linked_issue`, aliased for one deprecation cycle as `gittensory_validate_linked_issue`). + with `loopover_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,15 +149,13 @@ 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). 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: +All tools are metadata-only (no source upload). Run in this order: -1. `loopover_check_before_start` (was `gittensory_check_before_start`) — `{owner, repo, issueNumber, +1. `loopover_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, +2. `loopover_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, +3. `loopover_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}], @@ -165,14 +163,14 @@ one full minor-version deprecation cycle. Run in this order: (insufficient-signal/none/minor/moderate/significant) + findings. The positive-axis mirror of `loopover_check_slop_risk` — deterministic tier only (no LLM judgment); complexityDeltas/ duplicationDeltas are optional precomputed deltas the calling agent supplies, never raw source. -5. `loopover_lint_pr_text` (was `gittensory_lint_pr_text`) — `{commitMessages[], prBody, linkedIssue}` → verdict +5. `loopover_lint_pr_text` — `{commitMessages[], prBody, linkedIssue}` → verdict strong/adequate/weak + specific fixes. -6. `loopover_validate_config` (was `gittensory_validate_config`) — `{content, source?}` → normalized manifest fields, +6. `loopover_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: `loopover_preflight_pr` / `…_local_diff` (was `gittensory_preflight_pr` / `…_local_diff`) +(Auth'd extras: `loopover_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 257822b44f..65ae08ff6c 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 `loopover_review_pr_before_push` (aliased for one deprecation cycle as `gittensory_review_pr_before_push`). +The same composed check is exposed to MCP clients as `loopover_review_pr_before_push`. ## Auth @@ -188,8 +188,6 @@ The same capabilities are exposed to MCP clients as: - `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 `init-client --print ` prints the stdio MCP config for a host: `codex` (TOML), `claude`, `cursor`, and `mcp` (the shared `mcpServers` JSON shape), and `vscode` (VS Code's native `servers` map with `"type": "stdio"`, for `.vscode/mcp.json`). It prints config only; it never edits client files. @@ -273,7 +271,7 @@ gittensory-mcp changelog ## Offline decision-pack fallback -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. +Successful `decision-pack` and MCP `loopover_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. 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 9871adc608..e449bf9cf1 100755 --- a/packages/gittensory-mcp/bin/gittensory-mcp.js +++ b/packages/gittensory-mcp/bin/gittensory-mcp.js @@ -427,8 +427,6 @@ 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: "loopover_get_repo_context", @@ -582,25 +580,8 @@ const STDIO_TOOL_DESCRIPTORS = [ }, ]; -// #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 = ALL_STDIO_TOOL_DESCRIPTORS.find((entry) => entry.name === name); + const tool = STDIO_TOOL_DESCRIPTORS.find((entry) => entry.name === name); if (!tool) throw new Error(`Unknown stdio tool descriptor: ${name}`); return tool.description; } @@ -615,18 +596,13 @@ const server = new McpServer({ version: packageVersion, }); -// #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) { +// #4777: register a stdio tool under its loopover_ name. Thin wrapper kept so all 37 call sites +// stay uniform with the rest of this file's registration style. +function registerStdioTool(name, config, handler) { server.registerTool(name, config, handler); - const legacyName = legacyAliasName(name); - server.registerTool(legacyName, { ...config, description: stdioToolDescription(legacyName) }, handler); } -registerToolWithLegacyAlias( +registerStdioTool( "loopover_get_repo_context", { description: stdioToolDescription("loopover_get_repo_context"), @@ -638,7 +614,7 @@ registerToolWithLegacyAlias( }, ); -registerToolWithLegacyAlias( +registerStdioTool( "loopover_get_maintainer_noise", { description: stdioToolDescription("loopover_get_maintainer_noise"), @@ -650,7 +626,7 @@ registerToolWithLegacyAlias( }, ); -registerToolWithLegacyAlias( +registerStdioTool( "loopover_preflight_pr", { description: stdioToolDescription("loopover_preflight_pr"), @@ -659,7 +635,7 @@ registerToolWithLegacyAlias( async (input) => toolResult("Gittensory PR preflight.", await apiPost("/v1/preflight/pr", input)), ); -registerToolWithLegacyAlias( +registerStdioTool( "loopover_validate_linked_issue", { description: stdioToolDescription("loopover_validate_linked_issue"), @@ -672,7 +648,7 @@ registerToolWithLegacyAlias( }, ); -registerToolWithLegacyAlias( +registerStdioTool( "loopover_check_before_start", { description: stdioToolDescription("loopover_check_before_start"), @@ -689,7 +665,7 @@ registerToolWithLegacyAlias( }, ); -registerToolWithLegacyAlias( +registerStdioTool( "loopover_find_opportunities", { description: stdioToolDescription("loopover_find_opportunities"), @@ -706,7 +682,7 @@ registerToolWithLegacyAlias( }, ); -registerToolWithLegacyAlias( +registerStdioTool( "loopover_retrieve_issue_context", { description: stdioToolDescription("loopover_retrieve_issue_context"), @@ -725,7 +701,7 @@ registerToolWithLegacyAlias( }, ); -registerToolWithLegacyAlias( +registerStdioTool( "loopover_lint_pr_text", { description: stdioToolDescription("loopover_lint_pr_text"), @@ -734,7 +710,7 @@ registerToolWithLegacyAlias( async (input) => toolResult("Gittensory PR-text lint.", await apiPost("/v1/lint/pr-text", input)), ); -registerToolWithLegacyAlias( +registerStdioTool( "loopover_validate_config", { description: stdioToolDescription("loopover_validate_config"), @@ -743,7 +719,7 @@ registerToolWithLegacyAlias( async (input) => toolResult("Gittensory manifest validation.", await apiPost("/v1/validate/focus-manifest", input)), ); -registerToolWithLegacyAlias( +registerStdioTool( "loopover_check_slop_risk", { description: stdioToolDescription("loopover_check_slop_risk"), @@ -752,7 +728,7 @@ registerToolWithLegacyAlias( async (input) => toolResult("Gittensory slop-risk self-check.", await apiPost("/v1/lint/slop-risk", input)), ); -registerToolWithLegacyAlias( +registerStdioTool( "loopover_check_issue_slop", { description: stdioToolDescription("loopover_check_issue_slop"), @@ -761,7 +737,7 @@ registerToolWithLegacyAlias( async (input) => toolResult("Gittensory issue-slop self-check.", await apiPost("/v1/lint/issue-slop", input)), ); -registerToolWithLegacyAlias( +registerStdioTool( "loopover_preflight_local_diff", { description: stdioToolDescription("loopover_preflight_local_diff"), @@ -788,7 +764,7 @@ registerToolWithLegacyAlias( }, ); -registerToolWithLegacyAlias( +registerStdioTool( "loopover_get_registry_changes", { description: stdioToolDescription("loopover_get_registry_changes"), @@ -797,7 +773,7 @@ registerToolWithLegacyAlias( async () => toolResult("Gittensory registry changes.", await apiGet("/v1/registry/changes")), ); -registerToolWithLegacyAlias( +registerStdioTool( "loopover_get_upstream_drift", { description: stdioToolDescription("loopover_get_upstream_drift"), @@ -806,7 +782,7 @@ registerToolWithLegacyAlias( async () => toolResult("Gittensory upstream drift status.", await apiGet("/v1/upstream/drift")), ); -registerToolWithLegacyAlias( +registerStdioTool( "loopover_get_label_audit", { description: stdioToolDescription("loopover_get_label_audit"), @@ -823,7 +799,7 @@ registerToolWithLegacyAlias( }, ); -registerToolWithLegacyAlias( +registerStdioTool( "loopover_get_burden_forecast", { description: stdioToolDescription("loopover_get_burden_forecast"), @@ -841,7 +817,7 @@ registerToolWithLegacyAlias( }, ); -registerToolWithLegacyAlias( +registerStdioTool( "loopover_preview_local_pr_score", { description: stdioToolDescription("loopover_preview_local_pr_score"), @@ -850,7 +826,7 @@ registerToolWithLegacyAlias( async (input) => toolResult("Gittensory private local PR scoring preview.", await previewLocalScore(await withClientWorkspaceRoots(input))), ); -registerToolWithLegacyAlias( +registerStdioTool( "loopover_explain_score_breakdown", { description: stdioToolDescription("loopover_explain_score_breakdown"), @@ -898,7 +874,7 @@ registerToolWithLegacyAlias( }, ); -registerToolWithLegacyAlias( +registerStdioTool( "loopover_get_decision_pack", { description: stdioToolDescription("loopover_get_decision_pack"), @@ -910,7 +886,7 @@ registerToolWithLegacyAlias( }, ); -registerToolWithLegacyAlias( +registerStdioTool( "loopover_explain_repo_decision", { description: stdioToolDescription("loopover_explain_repo_decision"), @@ -922,7 +898,7 @@ registerToolWithLegacyAlias( }, ); -registerToolWithLegacyAlias( +registerStdioTool( "loopover_compare_pr_variants", { description: stdioToolDescription("loopover_compare_pr_variants"), @@ -937,7 +913,7 @@ registerToolWithLegacyAlias( }, ); -registerToolWithLegacyAlias( +registerStdioTool( "loopover_local_status", { description: stdioToolDescription("loopover_local_status"), @@ -973,7 +949,7 @@ registerToolWithLegacyAlias( }, ); -registerToolWithLegacyAlias( +registerStdioTool( "loopover_preflight_current_branch", { description: stdioToolDescription("loopover_preflight_current_branch"), @@ -990,7 +966,7 @@ registerToolWithLegacyAlias( }, ); -registerToolWithLegacyAlias( +registerStdioTool( "loopover_review_pr_before_push", { description: stdioToolDescription("loopover_review_pr_before_push"), @@ -999,7 +975,7 @@ registerToolWithLegacyAlias( async (input) => toolResult("Gittensory pre-PR review.", await reviewLocalPr(await withClientWorkspaceRoots(input))), ); -registerToolWithLegacyAlias( +registerStdioTool( "loopover_preview_current_branch_score", { description: stdioToolDescription("loopover_preview_current_branch_score"), @@ -1017,7 +993,7 @@ registerToolWithLegacyAlias( }, ); -registerToolWithLegacyAlias( +registerStdioTool( "loopover_rank_local_next_actions", { description: stdioToolDescription("loopover_rank_local_next_actions"), @@ -1029,7 +1005,7 @@ registerToolWithLegacyAlias( }, ); -registerToolWithLegacyAlias( +registerStdioTool( "loopover_explain_local_blockers", { description: stdioToolDescription("loopover_explain_local_blockers"), @@ -1049,7 +1025,7 @@ registerToolWithLegacyAlias( }, ); -registerToolWithLegacyAlias( +registerStdioTool( "loopover_remediation_plan", { description: stdioToolDescription("loopover_remediation_plan"), @@ -1063,7 +1039,7 @@ registerToolWithLegacyAlias( }, ); -registerToolWithLegacyAlias( +registerStdioTool( "loopover_prepare_pr_packet", { description: stdioToolDescription("loopover_prepare_pr_packet"), @@ -1075,7 +1051,7 @@ registerToolWithLegacyAlias( }, ); -registerToolWithLegacyAlias( +registerStdioTool( "loopover_compare_local_variants", { description: stdioToolDescription("loopover_compare_local_variants"), @@ -1102,7 +1078,7 @@ registerToolWithLegacyAlias( }, ); -registerToolWithLegacyAlias( +registerStdioTool( "loopover_agent_plan_next_work", { description: stdioToolDescription("loopover_agent_plan_next_work"), @@ -1111,7 +1087,7 @@ registerToolWithLegacyAlias( async (input) => toolResult(`Gittensory base-agent plan for ${input.login}.`, await apiPost("/v1/agent/plan-next-work", input)), ); -registerToolWithLegacyAlias( +registerStdioTool( "loopover_agent_start_run", { description: stdioToolDescription("loopover_agent_start_run"), @@ -1133,7 +1109,7 @@ registerToolWithLegacyAlias( ), ); -registerToolWithLegacyAlias( +registerStdioTool( "loopover_agent_get_run", { description: stdioToolDescription("loopover_agent_get_run"), @@ -1142,7 +1118,7 @@ registerToolWithLegacyAlias( async ({ runId }) => toolResult(`Gittensory base-agent run ${runId}.`, await apiGet(`/v1/agent/runs/${encodeURIComponent(runId)}`)), ); -registerToolWithLegacyAlias( +registerStdioTool( "loopover_agent_explain_next_action", { description: stdioToolDescription("loopover_agent_explain_next_action"), @@ -1157,7 +1133,7 @@ registerToolWithLegacyAlias( }, ); -registerToolWithLegacyAlias( +registerStdioTool( "loopover_agent_prepare_pr_packet", { description: stdioToolDescription("loopover_agent_prepare_pr_packet"), @@ -1229,7 +1205,7 @@ const agentPlanOutputSchema = { // Attach outputSchema to key tools via registerTool with zod output schemas. // All other tools continue to return unschematized text+structured content. -registerToolWithLegacyAlias( +registerStdioTool( "loopover_local_status_structured", { description: stdioToolDescription("loopover_local_status_structured"), @@ -1273,7 +1249,7 @@ registerToolWithLegacyAlias( }, ); -registerToolWithLegacyAlias( +registerStdioTool( "loopover_feasibility_gate", { description: stdioToolDescription("loopover_feasibility_gate"), @@ -2411,7 +2387,7 @@ function printVersion(options) { } function toolsCommand(options) { - const tools = ALL_STDIO_TOOL_DESCRIPTORS.map(({ name, description }) => ({ name, description })); + const tools = 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-cli-burden-forecast.test.ts b/test/unit/mcp-cli-burden-forecast.test.ts index 8e44c5060e..e9eb7f9092 100644 --- a/test/unit/mcp-cli-burden-forecast.test.ts +++ b/test/unit/mcp-cli-burden-forecast.test.ts @@ -46,17 +46,17 @@ async function disconnect() { if (configDir) rmSync(configDir, { recursive: true, force: true }); } -describe("gittensory_get_burden_forecast stdio proxy", () => { +describe("loopover_get_burden_forecast stdio proxy", () => { beforeEach(connect); afterEach(disconnect); it("registers the tool in the stdio server tool list", async () => { const { tools } = await client.listTools(); - expect(tools.map((t) => t.name)).toContain("gittensory_get_burden_forecast"); + expect(tools.map((t) => t.name)).toContain("loopover_get_burden_forecast"); }); it("proxies owner/repo to /v1/repos/:owner/:repo/intelligence via apiGet and returns the burden forecast", async () => { - const result = await client.callTool({ name: "gittensory_get_burden_forecast", arguments: { owner: "owner", repo: "repo" } }); + const result = await client.callTool({ name: "loopover_get_burden_forecast", arguments: { owner: "owner", repo: "repo" } }); expect(capturedRequests.length).toBe(1); const captured = capturedRequests[0]!; expect(captured.url).toContain("/v1/repos/owner/repo/intelligence"); diff --git a/test/unit/mcp-cli-find-opportunities.test.ts b/test/unit/mcp-cli-find-opportunities.test.ts index 8a9f1d0dad..3d313ab1b3 100644 --- a/test/unit/mcp-cli-find-opportunities.test.ts +++ b/test/unit/mcp-cli-find-opportunities.test.ts @@ -54,19 +54,19 @@ async function disconnect() { if (configDir) rmSync(configDir, { recursive: true, force: true }); } -describe("gittensory_find_opportunities stdio proxy", () => { +describe("loopover_find_opportunities stdio proxy", () => { beforeEach(connect); afterEach(disconnect); it("registers the tool in the stdio server's tool list", async () => { const { tools } = await client.listTools(); const names = tools.map((t) => t.name); - expect(names).toContain("gittensory_find_opportunities"); + expect(names).toContain("loopover_find_opportunities"); }); it("proxies the call to /v1/opportunities/find via apiPost", async () => { await client.callTool({ - name: "gittensory_find_opportunities", + name: "loopover_find_opportunities", arguments: { searchQuery: "test coverage", limit: 3 }, }); expect(capturedRequests.length).toBe(1); @@ -80,7 +80,7 @@ describe("gittensory_find_opportunities stdio proxy", () => { it("returns a ranked, public-safe list of opportunities", async () => { const result = await client.callTool({ - name: "gittensory_find_opportunities", + name: "loopover_find_opportunities", arguments: { searchQuery: "scoring", limit: 2 }, }); expect(result.isError).toBeFalsy(); @@ -93,7 +93,7 @@ describe("gittensory_find_opportunities stdio proxy", () => { it("strips undefined optional fields from the proxied body", async () => { await client.callTool({ - name: "gittensory_find_opportunities", + name: "loopover_find_opportunities", arguments: { searchQuery: "minimum" }, }); expect(capturedRequests.length).toBe(1); diff --git a/test/unit/mcp-cli-issue-rag.test.ts b/test/unit/mcp-cli-issue-rag.test.ts index 950ff8469d..7ed97a42f3 100644 --- a/test/unit/mcp-cli-issue-rag.test.ts +++ b/test/unit/mcp-cli-issue-rag.test.ts @@ -54,19 +54,19 @@ async function disconnect() { if (configDir) rmSync(configDir, { recursive: true, force: true }); } -describe("gittensory_retrieve_issue_context stdio proxy", () => { +describe("loopover_retrieve_issue_context stdio proxy", () => { beforeEach(connect); afterEach(disconnect); it("registers the tool in the stdio server's tool list", async () => { const { tools } = await client.listTools(); const names = tools.map((t) => t.name); - expect(names).toContain("gittensory_retrieve_issue_context"); + expect(names).toContain("loopover_retrieve_issue_context"); }); it("proxies the call to /v1/issue-rag/retrieve via apiPost", async () => { await client.callTool({ - name: "gittensory_retrieve_issue_context", + name: "loopover_retrieve_issue_context", arguments: { owner: "JSONbored", repo: "gittensory", @@ -97,7 +97,7 @@ describe("gittensory_retrieve_issue_context stdio proxy", () => { it("returns metadata-only retrieval telemetry", async () => { const result = await client.callTool({ - name: "gittensory_retrieve_issue_context", + name: "loopover_retrieve_issue_context", arguments: { owner: "JSONbored", repo: "gittensory", diff --git a/test/unit/mcp-cli-label-audit.test.ts b/test/unit/mcp-cli-label-audit.test.ts index a9bcd414ca..48e5e811b8 100644 --- a/test/unit/mcp-cli-label-audit.test.ts +++ b/test/unit/mcp-cli-label-audit.test.ts @@ -46,17 +46,17 @@ async function disconnect() { if (configDir) rmSync(configDir, { recursive: true, force: true }); } -describe("gittensory_get_label_audit stdio proxy", () => { +describe("loopover_get_label_audit stdio proxy", () => { beforeEach(connect); afterEach(disconnect); it("registers the tool in the stdio server tool list", async () => { const { tools } = await client.listTools(); - expect(tools.map((t) => t.name)).toContain("gittensory_get_label_audit"); + expect(tools.map((t) => t.name)).toContain("loopover_get_label_audit"); }); it("proxies owner/repo to /v1/repos/:owner/:repo/intelligence via apiGet and returns the label audit", async () => { - const result = await client.callTool({ name: "gittensory_get_label_audit", arguments: { owner: "owner", repo: "repo" } }); + const result = await client.callTool({ name: "loopover_get_label_audit", arguments: { owner: "owner", repo: "repo" } }); expect(capturedRequests.length).toBe(1); const captured = capturedRequests[0]!; expect(captured.url).toContain("/v1/repos/owner/repo/intelligence"); diff --git a/test/unit/mcp-cli-maintainer-noise.test.ts b/test/unit/mcp-cli-maintainer-noise.test.ts index 3e3b07b8a7..b35b6286c7 100644 --- a/test/unit/mcp-cli-maintainer-noise.test.ts +++ b/test/unit/mcp-cli-maintainer-noise.test.ts @@ -46,18 +46,18 @@ async function disconnect() { if (configDir) rmSync(configDir, { recursive: true, force: true }); } -describe("gittensory_get_maintainer_noise stdio proxy", () => { +describe("loopover_get_maintainer_noise stdio proxy", () => { beforeEach(connect); afterEach(disconnect); it("registers the tool in the stdio server tool list", async () => { const { tools } = await client.listTools(); - expect(tools.map((tool) => tool.name)).toContain("gittensory_get_maintainer_noise"); + expect(tools.map((tool) => tool.name)).toContain("loopover_get_maintainer_noise"); }); it("proxies the call to /maintainer-noise via apiGet and returns the payload", async () => { const result = await client.callTool({ - name: "gittensory_get_maintainer_noise", + name: "loopover_get_maintainer_noise", arguments: { owner: "owner", repo: "repo" }, }); expect(capturedRequests.length).toBe(1); @@ -76,7 +76,7 @@ describe("gittensory_get_maintainer_noise stdio proxy", () => { const payload = JSON.parse(run(["tools", "--json"])) as { tools: Array<{ name: string; description: string }>; }; - const tool = payload.tools.find((entry) => entry.name === "gittensory_get_maintainer_noise"); + const tool = payload.tools.find((entry) => entry.name === "loopover_get_maintainer_noise"); expect(tool?.description).toMatch(/maintainer queue-noise triage report/i); expect(tool?.description.trim().length).toBeGreaterThan(0); }); diff --git a/test/unit/mcp-cli-upstream-drift.test.ts b/test/unit/mcp-cli-upstream-drift.test.ts index 09c02a363d..cb039d2058 100644 --- a/test/unit/mcp-cli-upstream-drift.test.ts +++ b/test/unit/mcp-cli-upstream-drift.test.ts @@ -46,17 +46,17 @@ async function disconnect() { if (configDir) rmSync(configDir, { recursive: true, force: true }); } -describe("gittensory_get_upstream_drift stdio proxy", () => { +describe("loopover_get_upstream_drift stdio proxy", () => { beforeEach(connect); afterEach(disconnect); it("registers the tool in the stdio server tool list", async () => { const { tools } = await client.listTools(); - expect(tools.map((t) => t.name)).toContain("gittensory_get_upstream_drift"); + expect(tools.map((t) => t.name)).toContain("loopover_get_upstream_drift"); }); it("proxies the call to /v1/upstream/drift via apiGet and returns the payload", async () => { - const result = await client.callTool({ name: "gittensory_get_upstream_drift", arguments: {} }); + const result = await client.callTool({ name: "loopover_get_upstream_drift", arguments: {} }); expect(capturedRequests.length).toBe(1); const captured = capturedRequests[0]!; expect(captured.url).toContain("/v1/upstream/drift"); diff --git a/test/unit/mcp-discovery.test.ts b/test/unit/mcp-discovery.test.ts index 58f9a6b3f5..22085171a4 100644 --- a/test/unit/mcp-discovery.test.ts +++ b/test/unit/mcp-discovery.test.ts @@ -66,7 +66,7 @@ describe("MCP workspace root boundaries", () => { try { await rootedClient.connect(rootedTransport); - const result = await rootedClient.callTool({ name: "gittensory_local_status_structured", arguments: { cwd: privateRepo } }); + const result = await rootedClient.callTool({ name: "loopover_local_status_structured", arguments: { cwd: privateRepo } }); expect(result.isError).toBeFalsy(); expect(result.structuredContent).toMatchObject({ git: { error: "Selected workspace is outside the MCP roots exposed by the client." }, diff --git a/test/unit/mcp-feasibility-gate.test.ts b/test/unit/mcp-feasibility-gate.test.ts index c70ef53ef0..194866cd1b 100644 --- a/test/unit/mcp-feasibility-gate.test.ts +++ b/test/unit/mcp-feasibility-gate.test.ts @@ -32,20 +32,20 @@ async function disconnect() { if (configDir) rmSync(configDir, { recursive: true, force: true }); } -describe("gittensory_feasibility_gate stdio tool (#4270)", () => { +describe("loopover_feasibility_gate stdio tool (#4270)", () => { beforeEach(connect); afterEach(disconnect); it("registers the tool in the stdio server tool list", async () => { const { tools } = await client.listTools(); - const tool = tools.find((t) => t.name === "gittensory_feasibility_gate"); + const tool = tools.find((t) => t.name === "loopover_feasibility_gate"); expect(tool).toBeDefined(); expect(tool?.description).toContain("No API round-trip"); }); it("returns a go verdict for a clean, unclaimed, low-risk issue — with no network call", async () => { const result = await client.callTool({ - name: "gittensory_feasibility_gate", + name: "loopover_feasibility_gate", arguments: { claimStatus: "unclaimed", duplicateClusterRisk: "none", issueStatus: "ready" }, }); expect(result.isError).toBeFalsy(); @@ -60,7 +60,7 @@ describe("gittensory_feasibility_gate stdio tool (#4270)", () => { it("returns an avoid verdict when the issue is already solved", async () => { const result = await client.callTool({ - name: "gittensory_feasibility_gate", + name: "loopover_feasibility_gate", arguments: { claimStatus: "solved", duplicateClusterRisk: "none", issueStatus: "ready" }, }); expect(result.isError).toBeFalsy(); @@ -71,7 +71,7 @@ describe("gittensory_feasibility_gate stdio tool (#4270)", () => { it("returns a raise verdict when found is explicitly false", async () => { const result = await client.callTool({ - name: "gittensory_feasibility_gate", + name: "loopover_feasibility_gate", arguments: { claimStatus: "unclaimed", duplicateClusterRisk: "none", issueStatus: "ready", found: false }, }); expect(result.isError).toBeFalsy(); @@ -82,7 +82,7 @@ describe("gittensory_feasibility_gate stdio tool (#4270)", () => { it("rejects an invalid duplicateClusterRisk enum value", async () => { const result = await client.callTool({ - name: "gittensory_feasibility_gate", + name: "loopover_feasibility_gate", arguments: { claimStatus: "unclaimed", duplicateClusterRisk: "extreme", issueStatus: "ready" }, }); expect(result.isError).toBe(true); @@ -90,7 +90,7 @@ describe("gittensory_feasibility_gate stdio tool (#4270)", () => { it("never leaks private financial terminology in the response", async () => { const result = await client.callTool({ - name: "gittensory_feasibility_gate", + name: "loopover_feasibility_gate", arguments: { claimStatus: "claimed", duplicateClusterRisk: "high", issueStatus: "duplicate" }, }); expect(result.isError).toBeFalsy(); @@ -99,7 +99,7 @@ describe("gittensory_feasibility_gate stdio tool (#4270)", () => { }); }); -describe("gittensory_feasibility_gate: local claim-ledger sourcing (#5157)", () => { +describe("loopover_feasibility_gate: local claim-ledger sourcing (#5157)", () => { let ledgerRoot: string; let ledgerDbPath: string; let ledgerClient: Client; @@ -134,7 +134,7 @@ describe("gittensory_feasibility_gate: local claim-ledger sourcing (#5157)", () await connectWithLedgerDb(ledgerDbPath); const result = await ledgerClient.callTool({ - name: "gittensory_feasibility_gate", + name: "loopover_feasibility_gate", arguments: { claimStatus: "unclaimed", // caller-supplied, contradicts the real ledger state duplicateClusterRisk: "none", @@ -158,7 +158,7 @@ describe("gittensory_feasibility_gate: local claim-ledger sourcing (#5157)", () await connectWithLedgerDb(ledgerDbPath); const result = await ledgerClient.callTool({ - name: "gittensory_feasibility_gate", + name: "loopover_feasibility_gate", arguments: { claimStatus: "solved", // caller-supplied, would normally trigger an avoid verdict duplicateClusterRisk: "none", @@ -176,7 +176,7 @@ describe("gittensory_feasibility_gate: local claim-ledger sourcing (#5157)", () await connectWithLedgerDb(join(ledgerRoot, "does-not-exist.sqlite3")); const result = await ledgerClient.callTool({ - name: "gittensory_feasibility_gate", + name: "loopover_feasibility_gate", arguments: { claimStatus: "solved", duplicateClusterRisk: "none", @@ -203,7 +203,7 @@ describe("gittensory_feasibility_gate: local claim-ledger sourcing (#5157)", () await connectWithLedgerDb(ledgerDbPath); const result = await ledgerClient.callTool({ - name: "gittensory_feasibility_gate", + name: "loopover_feasibility_gate", arguments: { claimStatus: "solved", duplicateClusterRisk: "none", @@ -229,7 +229,7 @@ describe("gittensory_feasibility_gate: local claim-ledger sourcing (#5157)", () await connectWithLedgerDb(ledgerDbPath); const result = await ledgerClient.callTool({ - name: "gittensory_feasibility_gate", + name: "loopover_feasibility_gate", arguments: { claimStatus: "unclaimed", duplicateClusterRisk: "none", @@ -256,7 +256,7 @@ describe("gittensory_feasibility_gate: local claim-ledger sourcing (#5157)", () await connectWithLedgerDb(ledgerDbPath); const result = await ledgerClient.callTool({ - name: "gittensory_feasibility_gate", + name: "loopover_feasibility_gate", arguments: { claimStatus: "solved", duplicateClusterRisk: "none", issueStatus: "ready" }, }); expect(result.isError).toBeFalsy(); @@ -273,7 +273,7 @@ describe("gittensory_feasibility_gate: local claim-ledger sourcing (#5157)", () await connectWithLedgerDb(ledgerDbPath); const result = await ledgerClient.callTool({ - name: "gittensory_feasibility_gate", + name: "loopover_feasibility_gate", arguments: { claimStatus: "unclaimed", duplicateClusterRisk: "none", @@ -295,7 +295,7 @@ describe("gittensory_feasibility_gate: local claim-ledger sourcing (#5157)", () it("tool description documents the ledger-sourcing behavior and advisory-only guarantee", async () => { await connectWithLedgerDb(undefined); const { tools } = await ledgerClient.listTools(); - const tool = tools.find((t) => t.name === "gittensory_feasibility_gate"); + const tool = tools.find((t) => t.name === "loopover_feasibility_gate"); expect(tool?.description).toContain("Advisory-only"); expect(tool?.description).toContain("local gittensory-miner install's claim ledger"); }); diff --git a/test/unit/mcp-tool-rename-aliases.test.ts b/test/unit/mcp-tool-rename-aliases.test.ts index 38058133e1..25aca92103 100644 --- a/test/unit/mcp-tool-rename-aliases.test.ts +++ b/test/unit/mcp-tool-rename-aliases.test.ts @@ -1,9 +1,7 @@ -// #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 +// #4777: retire every gittensory_-prefixed deprecated alias that #4775 left in place for one +// minor-version deprecation cycle. This suite pins the post-retirement shape: exactly the 37 +// canonical loopover_-prefixed stdio tools are registered, none of their old gittensory_-prefixed +// alias names resolve anymore, no description carries a stale 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"; @@ -41,88 +39,61 @@ async function disconnect() { if (configDir) rmSync(configDir, { recursive: true, force: true }); } -describe("MCP tool rename (#4775) — discovery invariants", () => { +describe("MCP legacy alias retirement (#4777) — 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 () => { + it("lists exactly 37 loopover_ tools and zero gittensory_-prefixed 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()); + expect(legacy.length).toBe(0); + expect(names.length).toBe(37); }); - it("every gittensory_ alias's description is marked deprecated and names its loopover_ replacement", async () => { + it("no loopover_ tool's description carries a stale deprecation notice", 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_"))) { + for (const tool of tools) { 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 () => { + it("`gittensory-mcp tools --json` reports the same 37-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.count).toBe(37); expect([...payload.tools.map((t) => t.name)].sort()).toEqual([...tools.map((t) => t.name)].sort()); }); }); -describe("MCP tool rename (#4775) — old/new behavioral identity", () => { +describe("MCP legacy alias retirement (#4777) — old names no longer resolve", () => { 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: {} }, + // Representative sample spanning distinct tool categories (mirrors the pre-retirement suite's + // coverage): an authenticated API GET proxy, a source-free API POST self-check, a no-argument + // API GET, an API GET with a path parameter, and pure local logic with no network call. + const retiredNames = [ + "gittensory_get_repo_context", + "gittensory_check_slop_risk", + "gittensory_get_upstream_drift", + "gittensory_agent_get_run", + "gittensory_feasibility_gate", + "gittensory_local_status_structured", + "gittensory_local_status", ]; - 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); + it.each(retiredNames)("calling the retired alias %s errors instead of falling through to the handler", async (oldName) => { + const result = await client.callTool({ name: oldName, arguments: {} }); + expect(result.isError).toBe(true); }); });