diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 77b5775de5..0ee5b9dd7e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -467,8 +467,16 @@ jobs: key: turbo-code-${{ hashFiles('package-lock.json') }}-${{ github.run_id }} restore-keys: | turbo-code-${{ hashFiles('package-lock.json') }}- + # mcp/miner are in this gate because "Typecheck" below already runs for them, and typecheck's real + # surface reaches @loopover/engine -- test/** imports it directly, and its "types" resolve to + # packages/loopover-engine/dist/index.d.ts, which only exists once this step has run. An mcp-only or + # miner-only PR therefore used to run Typecheck against an unbuilt engine and fail with a wall of + # phantom "Cannot find module '@loopover/engine'" errors (plus every TS7006 implicit-any that + # cascades from them), with no relation to the actual diff. Deliberately the same widening the + # Typecheck step itself already received for the mirror-image gap; the two conditions must stay in + # sync, since any trigger that typechecks must also have built what typechecking reads. - name: Build engine package - if: ${{ github.event_name == 'push' || needs.changes.outputs.backend == 'true' || needs.changes.outputs.engine == 'true' || needs.changes.outputs.ui == 'true' }} + if: ${{ github.event_name == 'push' || needs.changes.outputs.backend == 'true' || needs.changes.outputs.engine == 'true' || needs.changes.outputs.ui == 'true' || needs.changes.outputs.mcp == 'true' || needs.changes.outputs.miner == 'true' }} run: npx turbo run build --filter=@loopover/engine # Mirrors "MCP package check"/"Miner package check" below: the published npm tarball is a # different surface than the workspace build above (files field, forbidden paths/content, diff --git a/cliff.mcp.toml b/cliff.mcp.toml deleted file mode 100644 index 792c4329f5..0000000000 --- a/cliff.mcp.toml +++ /dev/null @@ -1,48 +0,0 @@ -[changelog] -header = "# Changelog\n\n" -body = """ -{% if version %}\ -## {{ version }} - {{ timestamp | date(format="%Y-%m-%d") }} -{% else %}\ -## Unreleased -{% endif %}\ -{% for group, commits in commits | group_by(attribute="group") %} - -### {{ group | striptags | trim | upper_first }} -{% for commit in commits %} -- {{ commit.message | upper_first }} -{% endfor %} -{% endfor %}\n -""" -trim = true -footer = "" - -[git] -conventional_commits = true -filter_unconventional = true -split_commits = false -tag_pattern = "^mcp-v[0-9].*" -sort_commits = "oldest" - -commit_preprocessors = [ - { pattern = " \\(#[0-9]+\\)$", replace = "" }, - { pattern = "^fix\\(docs\\): use Gittensor screenshot in homepage hero$", replace = "fix(release): create GitHub releases for MCP tag publishes" }, -] - -commit_parsers = [ - { message = "^feat", group = "Features" }, - { message = "^fix\\(changelog\\)", skip = true }, - { message = "^fix", group = "Fixes" }, - { message = "^test", group = "Tests" }, - { message = "^docs\\(changelog\\)", skip = true }, - { message = "^chore\\(release\\)", skip = true }, - { message = "^ci\\(release\\)", skip = true }, - { message = "^ci\\(changelog\\)", skip = true }, - { message = "^ci", group = "CI" }, - { message = "^build", group = "Build" }, - { message = "^docs", group = "Docs" }, - { message = "^refactor", group = "Refactors" }, - { message = "^chore\\(deps\\)", group = "Dependencies" }, - { message = "^chore", group = "Chores" }, - { message = "^revert", group = "Reverts" }, -] diff --git a/packages/loopover-mcp/bin/loopover-mcp.ts b/packages/loopover-mcp/bin/loopover-mcp.ts index 51f35b5ed8..f661042705 100644 --- a/packages/loopover-mcp/bin/loopover-mcp.ts +++ b/packages/loopover-mcp/bin/loopover-mcp.ts @@ -1732,8 +1732,8 @@ export const server = new McpServer({ version: packageVersion, }); -// #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. +// #4777: register a stdio tool under its loopover_ name. Every tool registers through this wrapper, +// so registration style stays uniform across the file. // Telemetry await/flush lives in wrapStdioToolHandler (lib/telemetry.ts, unit-tested) — #6238 / #8690. // Reads telemetryState() HERE on purpose: registerStdioTool's second parameter is the TOOL's config and // shadows the module-level `config`, so a read inside a nested function would silently see the wrong object. @@ -2954,68 +2954,9 @@ registerStdioTool( async (input: any) => toolResult("LoopOver base-agent public-safe PR packet.", await agentPreparePrPacket(await withClientWorkspaceRoots(input))), ); -// ── Output schemas for structured tool responses (#291) ────────────────────── - -const repoContextOutputSchema = { - type: "object", - properties: { - repoFullName: { type: "string" }, - lane: { type: "string" }, - primaryLanguage: { type: ["string", "null"] }, - openIssueCount: { type: "number" }, - openPrCount: { type: "number" }, - }, - additionalProperties: true, -}; - -const preflightOutputSchema = { - type: "object", - properties: { - status: { type: "string", enum: ["pass", "warn", "fail", "unknown"] }, - signals: { type: "array", items: { type: "object" } }, - summary: { type: "string" }, - }, - additionalProperties: true, -}; - -const decisionPackOutputSchema = { - type: "object", - properties: { - login: { type: "string" }, - decisions: { type: "array", items: { type: "object" } }, - cachedAt: { type: ["string", "null"] }, - }, - additionalProperties: true, -}; - -const localStatusOutputSchema = { - type: "object", - properties: { - apiUrl: { type: "string" }, - package: { type: "object", properties: { name: { type: "string" }, version: { type: "string" } }, additionalProperties: true }, - hasToken: { type: "boolean" }, - profile: { type: "object", additionalProperties: true }, - authLogin: { type: ["string", "null"] }, - sessionExpiresAt: { type: ["string", "null"] }, - sourceUploadDefault: { type: "boolean" }, - sourceUploadSupported: { type: "boolean" }, - git: { type: "object", additionalProperties: true }, - }, - additionalProperties: true, -}; - -const agentPlanOutputSchema = { - type: "object", - properties: { - login: { type: "string" }, - actions: { type: "array", items: { type: "object" } }, - topAction: { type: ["object", "null"] }, - }, - additionalProperties: true, -}; - -// Attach outputSchema to key tools via registerTool with zod output schemas. -// All other tools continue to return unschematized text+structured content. +// Only this tool declares an outputSchema today; every other tool returns text + unschematized +// structured content. #9518 finishes the job by registering all of them from @loopover/contract, +// where each tool's output schema lives beside its input schema and is enforced by validate:mcp. registerStdioTool( "loopover_local_status_structured", @@ -5192,10 +5133,11 @@ function toolsCommand(args: any) { }); } -// `tools search ` — fuzzy discovery across the ~150-tool combined surface (#6300). Matches the -// query against each registered tool's name AND description (not name-only), so "stake" surfaces -// get_subnet_stake_quote even though "stake" is only in its description. Reuses this CLI's existing -// levenshteinDistance for typo tolerance rather than pulling in a fuzzy-match dependency. +// `tools search ` — fuzzy discovery across the whole registered tool surface (#6300). Matches +// the query against each registered tool's name AND description (not name-only), so "duplicate" +// surfaces loopover_check_before_start even though "duplicate" is only in its description. Reuses +// this CLI's existing levenshteinDistance for typo tolerance rather than pulling in a fuzzy-match +// dependency. function toolsSearchCommand(args: any) { const options = parseOptions(args); const query = args.find((arg: any) => !arg.startsWith("--")); @@ -6427,10 +6369,6 @@ function optionalNumber(value: any) { return Number.isFinite(parsed) ? parsed : undefined; } -function isValidationStatus(value: any) { - return Boolean(normalizeValidationStatus(value)); -} - function normalizeValidationStatus(value: any) { const text = String(value ?? "").trim().toLowerCase().replace(/[-\s]+/g, "_"); if (["passed", "pass", "success", "ok", "exit_0", "0"].includes(text)) return "passed"; diff --git a/packages/loopover-mcp/lib/local-branch.ts b/packages/loopover-mcp/lib/local-branch.ts index c6f67f5a0a..d7945ba050 100644 --- a/packages/loopover-mcp/lib/local-branch.ts +++ b/packages/loopover-mcp/lib/local-branch.ts @@ -1,6 +1,6 @@ import { execFileSync } from "node:child_process"; import { realpathSync } from "node:fs"; -import { isAbsolute, join, relative, resolve } from "node:path"; +import { isAbsolute, relative, resolve } from "node:path"; import { fileURLToPath } from "node:url"; import { assertScenarioLocalBranchInputSafe } from "@loopover/engine"; import { isCodeFile, isTestPath as isTestFile } from "@loopover/engine/signals/test-evidence"; diff --git a/packages/loopover-mcp/tsconfig.json b/packages/loopover-mcp/tsconfig.json index 3795e72d26..9aa54d7746 100644 --- a/packages/loopover-mcp/tsconfig.json +++ b/packages/loopover-mcp/tsconfig.json @@ -25,7 +25,11 @@ // Without this, the inherited root value ("./.tsbuildinfo") resolves relative to the ROOT config's // location, not this one -- both packages would then read/write the exact same cache file at the // repo root and corrupt each other's incremental state. - "tsBuildInfoFile": "./.tsbuildinfo" + "tsBuildInfoFile": "./.tsbuildinfo", + // #9516: five hand-written output-schema literals sat here unreferenced because nothing rejected + // dead locals. tsc is the gate now, so the next one fails the build instead of accumulating. + "noUnusedLocals": true, + "noUnusedParameters": true }, // Every bin/lib runtime module is real TypeScript (#7291, phased across #7328/#7329/#7330): tsc owns // the dist/ .js emit, which is gitignored -- contributors and tests never touch it. The glob stays