You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Part of #9515 (keystone group). LoopOver's tool/API contracts live in four unshared zod sites — src/openapi/schemas.ts (~150 response schemas, spec-only), src/api/routes.ts (38 inline request schemas), src/mcp/server.ts (~116 tools' input/output shapes), packages/loopover-mcp/bin/loopover-mcp.ts (~85 shapes hand-mirroring the remote server, per their own comments, e.g. :376 "stdio mirror of the remote loopover_get_repo_onboarding_pack shape") — plus enum literals hand-copied from the engine (bin/loopover-mcp.ts:154-166: MAINTAIN_ACTION_CLASSES, MAINTAIN_AUTONOMY_LEVELS, PROPOSE_ACTION_CLASSES, "The drift this invites is real and has bitten once already") and a circular-import workaround that forces inline duplication of a REST schema (src/mcp/server.ts:262-266, fileIncidentReportShape vs postMergeIncidentReportSchema).
metagraphed proved the fix (landed as types-epics #7860 / #8068–#8076): a dedicated zod schema tree with one naming convention, converted with z.toJSONSchema(…, { target: "draft-2020-12" }) at module load (no build step, nothing to regenerate), and a single listToolDefinitions() projection that every consumer derives from. This issue creates LoopOver's equivalent as a workspace package and proves it end-to-end on a pilot batch.
Decision record — recorded here, not re-litigated per PR
A new published workspace package packages/loopover-contract (@loopover/contract), not engine subpaths. Rationale: (a) the Worker src/, control-plane/, both stdio bins, packages/loopover-miner/lib, and apps/loopover-ui all need to import it — the Worker and UI deliberately do not depend on the full engine (the engine-parity:drift-check twin system exists precisely because they don't), and a zod-only leaf package keeps every bundle lean; (b) @loopover/mcp and @loopover/miner are published, so their schema dependency must be publishable too; (c) widening the engine's public export map was already considered and rejected once (bin/loopover-mcp.ts:154-157). Consequences: the package joins the release-please linked-versions lockstep group (engine,mcp,miner → add contract) and gets a test:contract-pack allowlist check mirroring scripts/check-mcp-package.ts. Overturn condition: only if the lockstep/publish overhead proves to outweigh the bundle-size cost of engine subpaths — record any reversal here first.
Requirements
Package skeleton.packages/loopover-contract: zod v4 only (no other runtime deps), type: module, Workers-safe (no node builtins), subpath exports per domain (@loopover/contract/tools/<family>, @loopover/contract/enums, …). Wire into the workspace build (turbo), test:ci, release-please (lockstep group + .release-please-manifest.json + release-manifest:sync:check), and a scripts/check-contract-package.ts pack allowlist run as test:contract-pack.
Schema conventions (documented in the package README, enforced by meta-test): one file per tool family; <ToolNamePascal>InputSchema / <ToolNamePascal>OutputSchema + z.infer type aliases; a shared.ts only for shapes reused 3+ times; output schemas are real object schemas — shallower than the REST response where wire-compat demands (metagraphed's documented wire-compat rule), but never bare z.unknown() at the top level.
Tool metadata model. Each tool contributes a typed registry entry: name, title, description, category (existing MCP_TOOL_CATEGORIES vocabulary), MCP annotations (readOnlyHint/destructiveHint), auth (public | token | session | maintainer | operator | mcp-admin | internal), locality (remote | local-git | miner), availability (cloud | selfhost | both). The registry entry carries inputSchema/outputSchemaon the entry itself — no name-keyed side maps (metagraphed residue: a typo'd key silently drops the schema).
Enums single-sourced. Move MAINTAIN_ACTION_CLASSES, MAINTAIN_AUTONOMY_LEVELS, PROPOSE_ACTION_CLASSES, TEST_FRAMEWORKS, and the claim/plan status enums into @loopover/contract/enums; consumers (engine, Worker, both bins, miner) import them; the hand-copied literals and their pin-tests (test/unit/mcp-cli-maintain.test.ts) are replaced by direct imports.
Pilot batch — six tools, end to end, both servers. Migrate exactly: loopover_get_repo_context, loopover_get_pr_reviewability, loopover_predict_gate, loopover_preflight_pr, loopover_local_status_structured, loopover_admin_get_config. For each: contract file with real input+output schemas; the remote server (src/mcp/server.ts) and the stdio server (packages/loopover-mcp/bin/loopover-mcp.ts) both register from the contract entry; the stdio handler for these six becomes fully typed (no any); the remote tool's placeholder output schema is replaced by the real one. The fileIncidentReportShape circular-import duplication is NOT in the pilot — it dissolves in the full migration.
Tests. Registry meta-tests (unique names, description length, every entry has a compilable object outputSchema via Ajv, annotations present, category ∈ vocabulary, locality/auth/availability present); per-pilot-tool schema tests (valid + invalid input, sample output validates); 99% branch-counted patch coverage per house rule.
Fix what you find (program principle 4) — any defect surfaced in the six pilot tools' handlers is fixed in the same PR series and noted in the PR body.
Non-goals
Migrating the remaining ~110 remote / 96 stdio tools (next issue — the batches).
REST route schemas and the OpenAPI seam (separate sub-issue).
New tools of any kind.
Deliverables
packages/loopover-contract published-ready package wired into build/CI/release
Conventions README + meta-tests enforcing them
Six pilot tools registered from the contract on both servers, fully typed, with real validated output schemas
Enums single-sourced with the hand-copies deleted
Decision record above kept current
Expected outcome
A schema change to a pilot tool is made in exactly one file and immediately propagates (as a compile error or Ajv failure, not silent drift) to both MCP servers and every projection — proving the mechanism the remaining batches will follow.
References
Part of #9515. Feeds #9183 / #9184 (tool catalogs) and #9282 (the UI imports response types from this package once routes migrate).
Context
Part of #9515 (keystone group). LoopOver's tool/API contracts live in four unshared zod sites —
src/openapi/schemas.ts(~150 response schemas, spec-only),src/api/routes.ts(38 inline request schemas),src/mcp/server.ts(~116 tools' input/output shapes),packages/loopover-mcp/bin/loopover-mcp.ts(~85 shapes hand-mirroring the remote server, per their own comments, e.g.:376"stdio mirror of the remote loopover_get_repo_onboarding_pack shape") — plus enum literals hand-copied from the engine (bin/loopover-mcp.ts:154-166:MAINTAIN_ACTION_CLASSES,MAINTAIN_AUTONOMY_LEVELS,PROPOSE_ACTION_CLASSES, "The drift this invites is real and has bitten once already") and a circular-import workaround that forces inline duplication of a REST schema (src/mcp/server.ts:262-266,fileIncidentReportShapevspostMergeIncidentReportSchema).metagraphed proved the fix (landed as types-epics #7860 / #8068–#8076): a dedicated zod schema tree with one naming convention, converted with
z.toJSONSchema(…, { target: "draft-2020-12" })at module load (no build step, nothing to regenerate), and a singlelistToolDefinitions()projection that every consumer derives from. This issue creates LoopOver's equivalent as a workspace package and proves it end-to-end on a pilot batch.Decision record — recorded here, not re-litigated per PR
A new published workspace package
packages/loopover-contract(@loopover/contract), not engine subpaths. Rationale: (a) the Workersrc/,control-plane/, both stdio bins,packages/loopover-miner/lib, andapps/loopover-uiall need to import it — the Worker and UI deliberately do not depend on the full engine (theengine-parity:drift-checktwin system exists precisely because they don't), and a zod-only leaf package keeps every bundle lean; (b)@loopover/mcpand@loopover/minerare published, so their schema dependency must be publishable too; (c) widening the engine's public export map was already considered and rejected once (bin/loopover-mcp.ts:154-157). Consequences: the package joins the release-pleaselinked-versionslockstep group (engine,mcp,miner→ addcontract) and gets atest:contract-packallowlist check mirroringscripts/check-mcp-package.ts. Overturn condition: only if the lockstep/publish overhead proves to outweigh the bundle-size cost of engine subpaths — record any reversal here first.Requirements
packages/loopover-contract: zod v4 only (no other runtime deps),type: module, Workers-safe (no node builtins), subpath exports per domain (@loopover/contract/tools/<family>,@loopover/contract/enums, …). Wire into the workspace build (turbo),test:ci, release-please (lockstep group +.release-please-manifest.json+release-manifest:sync:check), and ascripts/check-contract-package.tspack allowlist run astest:contract-pack.<ToolNamePascal>InputSchema/<ToolNamePascal>OutputSchema+z.infertype aliases; ashared.tsonly for shapes reused 3+ times; output schemas are real object schemas — shallower than the REST response where wire-compat demands (metagraphed's documented wire-compat rule), but never barez.unknown()at the top level.name,title,description,category(existingMCP_TOOL_CATEGORIESvocabulary), MCPannotations(readOnlyHint/destructiveHint),auth(public|token|session|maintainer|operator|mcp-admin|internal),locality(remote|local-git|miner),availability(cloud|selfhost|both). The registry entry carriesinputSchema/outputSchemaon the entry itself — no name-keyed side maps (metagraphed residue: a typo'd key silently drops the schema).listToolDefinitions()as the single projection point (JSON Schema emitted viaz.toJSONSchemaat module load);buildAnthropicToolSpecs()/buildOpenAIToolSpecs()/buildAgentToolsIndex()as pure functions over it (feeds Epic: ORB maintainer chat platform — conversational command center for maintainers and repo owners (hosted-ready) #9183/Epic: hosted AMS chat platform — sign up, connect GitHub, converse with your miner #9184 grounding catalogs); an explicitMcpToolDefinitioninterface annotation on the registry array (metagraphed's documented TS-bivariance lesson — without it heterogeneous handler types collapse).MAINTAIN_ACTION_CLASSES,MAINTAIN_AUTONOMY_LEVELS,PROPOSE_ACTION_CLASSES,TEST_FRAMEWORKS, and the claim/plan status enums into@loopover/contract/enums; consumers (engine, Worker, both bins, miner) import them; the hand-copied literals and their pin-tests (test/unit/mcp-cli-maintain.test.ts) are replaced by direct imports.loopover_get_repo_context,loopover_get_pr_reviewability,loopover_predict_gate,loopover_preflight_pr,loopover_local_status_structured,loopover_admin_get_config. For each: contract file with real input+output schemas; the remote server (src/mcp/server.ts) and the stdio server (packages/loopover-mcp/bin/loopover-mcp.ts) both register from the contract entry; the stdio handler for these six becomes fully typed (noany); the remote tool's placeholder output schema is replaced by the real one. ThefileIncidentReportShapecircular-import duplication is NOT in the pilot — it dissolves in the full migration.outputSchemavia Ajv, annotations present, category ∈ vocabulary, locality/auth/availability present); per-pilot-tool schema tests (valid + invalid input, sample output validates); 99% branch-counted patch coverage per house rule.Non-goals
Deliverables
packages/loopover-contractpublished-ready package wired into build/CI/releaseExpected outcome
A schema change to a pilot tool is made in exactly one file and immediately propagates (as a compile error or Ajv failure, not silent drift) to both MCP servers and every projection — proving the mechanism the remaining batches will follow.
References
Part of #9515. Feeds #9183 / #9184 (tool catalogs) and #9282 (the UI imports response types from this package once routes migrate).