Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions packages/loopover-contract/src/api-schemas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -452,6 +452,7 @@ export const RepositorySettingsSchema = z
// the compile-time parity assertion in test/unit/openapi-settings-schema-parity.test.ts.
expectedCiContexts: z.array(z.string()).readonly().nullable().optional(),
advisoryCheckRuns: z.array(z.object({ name: z.string(), appSlug: z.string() })).readonly().nullable().optional(),
ignoredCheckRuns: z.array(z.object({ name: z.string(), appSlug: z.string() })).readonly().nullable().optional(),
copycatGateMode: z.enum(["off", "warn", "label", "block"]).optional(),
copycatGateMinScore: z.number().nullable().optional(),
gateDryRun: z.boolean().optional(),
Expand Down
19 changes: 19 additions & 0 deletions packages/loopover-contract/src/enums.ts
Original file line number Diff line number Diff line change
Expand Up @@ -194,3 +194,22 @@ export type TenantProduct = (typeof TENANT_PRODUCTS)[number];
/** One doctor check's verdict (#9522). `warn` exists so a degraded-but-working instance is not reported as broken. */
export const INSTANCE_CHECK_STATUSES = ["pass", "warn", "fail"] as const;
export type InstanceCheckStatus = (typeof INSTANCE_CHECK_STATUSES)[number];

/**
* The AMS miner's own vocabularies (#9660).
*
* Restated here for the usual reason -- this package cannot import the miner -- and pinned against the live
* lists by test/unit/contract-registry.test.ts. They lived in tools/miner.ts until now, which put them
* outside even the convention that a shared vocabulary lives in this file, and two of them appear in OUTPUT
* schemas: a new queue status or run state would make a tool's real `structuredContent` fail the schema that
* same tool advertises.
*/
/** packages/loopover-miner/lib/portfolio-queue.ts */
export const QUEUE_STATUSES = ["queued", "in_progress", "done"] as const;
export type QueueStatus = (typeof QUEUE_STATUSES)[number];
/** packages/loopover-miner/lib/claim-ledger.ts */
export const CLAIM_STATUSES = ["active", "released", "expired"] as const;
export type ClaimStatus = (typeof CLAIM_STATUSES)[number];
/** packages/loopover-miner/lib/run-state.ts */
export const MINER_RUN_STATES = ["idle", "discovering", "planning", "preparing"] as const;
export type MinerRunState = (typeof MINER_RUN_STATES)[number];
6 changes: 3 additions & 3 deletions packages/loopover-contract/src/tools/agent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
// Descriptions are relocated verbatim, same discipline as the other batches.
import { z } from "zod";
import { defineTool } from "../tool-definition.js";
import { AUTONOMY_LEVELS, MAINTAIN_ACTION_CLASSES, PLAN_STEP_STATUSES, PROPOSE_ACTION_CLASSES, TEST_FRAMEWORKS } from "../enums.js";
import { AUTONOMY_LEVELS, FEASIBILITY_VERDICTS, MAINTAIN_ACTION_CLASSES, PLAN_STEP_STATUSES, PROPOSE_ACTION_CLASSES, TEST_FRAMEWORKS } from "../enums.js";
import { SCENARIO_LIMITS, WRITE_TOOL_LIMITS } from "../limits.js";
import { AgentRunBundleOutput } from "./branch.js";
import { ownerRepoInput } from "../shared.js";
Expand Down Expand Up @@ -45,7 +45,7 @@ export const IntakeIdeaInput = z.object({
* a zod message the caller cannot act on. */
export const IntakeIdeaOutput = z.looseObject({
ok: z.boolean(),
verdict: z.enum(["go", "raise", "avoid"]).optional(),
verdict: z.enum(FEASIBILITY_VERDICTS).optional(),
taskGraph: z.unknown().optional(),
errors: z.array(z.string()).optional(),
});
Expand All @@ -64,7 +64,7 @@ export const intakeIdeaTool = defineTool({

export const PlanIdeaClaimsOutput = z.looseObject({
ok: z.boolean(),
verdict: z.enum(["go", "raise", "avoid"]).optional(),
verdict: z.enum(FEASIBILITY_VERDICTS).optional(),
claimPlan: z.unknown().optional(),
errors: z.array(z.string()).optional(),
});
Expand Down
4 changes: 2 additions & 2 deletions packages/loopover-contract/src/tools/branch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ import { PreflightPrInput, PreflightPrOutput } from "./preflight-pr.js";
* `content` gets a rejected call, not a silently-stripped one, so it cannot believe it uploaded
* source and be wrong about it.
*/
const changedFileSchema = z.strictObject({
export const changedFileSchema = z.strictObject({
path: z.string().min(1),
previousPath: z.string().min(1).optional(),
additions: z.number().int().min(0).optional(),
Expand All @@ -38,7 +38,7 @@ const changedFileSchema = z.strictObject({
});

/** One locally-executed validation command and its result. Strict for the same reason. */
const validationEntrySchema = z.strictObject({
export const validationEntrySchema = z.strictObject({
command: z.string().min(1),
status: z.enum(["passed", "failed", "not_run", "skipped", "focused", "unknown"]),
summary: z.string().optional(),
Expand Down
135 changes: 122 additions & 13 deletions packages/loopover-contract/src/tools/local-branch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@
// constructed with, so requiring the caller to restate it was always redundant.
import { z } from "zod";
import { defineTool } from "../tool-definition.js";
import { PREFLIGHT_LIMITS, SCENARIO_LIMITS } from "../limits.js";
import { PREFLIGHT_LIMITS, SCENARIO_LIMITS, SCENARIO_MAX_LINKED_ISSUE_NUMBERS } from "../limits.js";
import { FEASIBILITY_VERDICTS } from "../enums.js";
import {
AgentRunBundleOutput,
Expand All @@ -46,6 +46,8 @@ import {
WatchIssuesOutput,
} from "./discovery-utility.js";
import { callerBranchEligibilitySchema } from "./review.js";
import { changedFileSchema, validationEntrySchema } from "./branch.js";
import { focusManifestInputSchema } from "../api-requests.js";

/** One locally-executed validation command and its result, as the local-branch surfaces accept it. */
const localValidationEntry = z.object({
Expand Down Expand Up @@ -87,6 +89,47 @@ export const CurrentBranchInput = z.object({
scorePreviewCommand: z.string().optional(),
});

/**
* The FULL current-branch vocabulary: everything a caller may supply about a branch (#9662).
*
* Previously this lived as a 40-line `localBranchAnalysisShape` literal inside `src/mcp/server.ts` -- the
* hand-written kind of declaration this package exists to remove -- and the ten tools below advertised a
* strictly NARROWER `CurrentBranchInput` in the registry. So `listToolDefinitions()`, and with it the
* OpenAI/Anthropic spec builders and the `.well-known` catalogs, described a tool that rejected eight
* fields the remote server accepts, and nothing could see the gap.
*
* The contract is the wider surface, per the rule a server narrows FROM: the remote accepts this whole
* shape because a caller may supply the metadata itself, and the stdio server narrows to `CurrentBranchInput`
* because it reads the shas, the diff and the scorer probe off the local checkout instead of taking them
* from the caller -- serving less, and saying so, rather than advertising a field it ignores.
*/
export const LocalBranchAnalysisInput = CurrentBranchInput.extend({
login: z.string().min(1).max(SCENARIO_LIMITS.branchRefChars),
repoFullName: z.string().min(3).max(SCENARIO_LIMITS.repoFullNameChars),
baseSha: z.string().min(1).optional(),
headSha: z.string().min(1).optional(),
mergeBaseSha: z.string().min(1).optional(),
remoteTrackingSha: z.string().min(1).optional(),
commitMessages: z.array(z.string()).max(30).optional(),
changedFiles: z.array(changedFileSchema).max(500).optional(),
validation: z.array(validationEntrySchema).max(50).optional(),
linkedIssues: z.array(z.number().int().positive()).max(SCENARIO_MAX_LINKED_ISSUE_NUMBERS).optional(),
focusManifest: focusManifestInputSchema.optional(),
/** What a local scoring run reported, when the caller ran one. */
localScorer: z
.object({
mode: z.enum(["metadata_only", "external_command", "gittensor_root"]),
activeModel: z.string().optional(),
sourceTokenScore: z.number().min(0).optional(),
totalTokenScore: z.number().min(0).optional(),
sourceLines: z.number().min(0).optional(),
testTokenScore: z.number().min(0).optional(),
nonCodeTokenScore: z.number().min(0).optional(),
warnings: z.array(z.string()).optional(),
})
.optional(),
});

export const preflightCurrentBranchTool = defineTool({
name: "loopover_preflight_current_branch",
title: "Preflight current branch",
Expand All @@ -96,7 +139,7 @@ export const preflightCurrentBranchTool = defineTool({
auth: "token",
locality: "local-git",
availability: "both",
input: CurrentBranchInput,
input: LocalBranchAnalysisInput,
output: PreflightCurrentBranchOutput,
});

Expand All @@ -109,7 +152,7 @@ export const previewCurrentBranchScoreTool = defineTool({
auth: "token",
locality: "local-git",
availability: "both",
input: CurrentBranchInput,
input: LocalBranchAnalysisInput,
output: PreviewCurrentBranchScoreOutput,
});

Expand All @@ -122,7 +165,7 @@ export const rankLocalNextActionsTool = defineTool({
auth: "token",
locality: "local-git",
availability: "both",
input: CurrentBranchInput,
input: LocalBranchAnalysisInput,
output: RankLocalNextActionsOutput,
});

Expand All @@ -135,7 +178,7 @@ export const explainLocalBlockersTool = defineTool({
auth: "token",
locality: "local-git",
availability: "both",
input: CurrentBranchInput,
input: LocalBranchAnalysisInput,
output: ExplainLocalBlockersOutput,
});

Expand All @@ -148,7 +191,7 @@ export const remediationPlanTool = defineTool({
auth: "token",
locality: "local-git",
availability: "both",
input: CurrentBranchInput,
input: LocalBranchAnalysisInput,
output: RemediationPlanOutput,
});

Expand All @@ -161,7 +204,7 @@ export const preparePrPacketTool = defineTool({
auth: "token",
locality: "local-git",
availability: "both",
input: CurrentBranchInput,
input: LocalBranchAnalysisInput,
output: PrepareLocalPrPacketOutput,
});

Expand All @@ -176,7 +219,7 @@ export const agentPreparePrPacketTool = defineTool({
auth: "token",
locality: "local-git",
availability: "both",
input: CurrentBranchInput,
input: LocalBranchAnalysisInput,
output: AgentRunBundleOutput,
});

Expand All @@ -193,7 +236,7 @@ export const reviewPrBeforePushTool = defineTool({
output: PreflightCurrentBranchOutput,
});

export const DraftPrBodyInput = CurrentBranchInput.extend({
export const DraftPrBodyInput = LocalBranchAnalysisInput.extend({
format: z.enum(["json", "markdown"]).optional(),
});
export const draftPrBodyTool = defineTool({
Expand All @@ -210,6 +253,10 @@ export const draftPrBodyTool = defineTool({
});

export const CompareLocalVariantsInput = z.object({
variants: z.array(LocalBranchAnalysisInput).min(1).max(10),
});
/** What the STDIO server serves: each variant is analysed from the checkout, not from caller-supplied shas. */
export const StdioCompareLocalVariantsInput = z.object({
variants: z.array(CurrentBranchInput).min(1).max(10),
});
export const compareLocalVariantsTool = defineTool({
Expand Down Expand Up @@ -265,6 +312,52 @@ export const LocalScoreInput = z.object({
scorePreviewCommand: z.string().optional(),
});

/**
* The score-preview family's full vocabulary (#9662), and each server's declared narrowing of it.
*
* Same story as `LocalBranchAnalysisInput` above: the remote registered from a hand-written
* `scorePreviewShape` in `src/mcp/server.ts` carrying eight scenario knobs the registry never described,
* while `LocalScoreInput` carries nine fields only a server with a checkout can honour. Neither is the
* contract on its own -- the union is, and each server states which part of it it serves rather than
* advertising a field it silently overrides.
*/
/** The linked-issue provenance a caller may supply, as the remote has always accepted it. */
const linkedIssueContextSchema = z.strictObject({
status: z.enum(["raw", "plausible", "validated", "invalid", "unavailable"]).optional(),
source: z.enum(["user_supplied", "official_mirror", "github_cache", "issue_quality", "missing"]).optional(),
issueNumbers: z.array(z.number().int().positive()).max(50).optional(),
solvedByPullRequests: z.array(z.number().int().positive()).max(50).optional(),
reason: z.string().optional(),
warnings: z.array(z.string()).max(20).optional(),
});

export const LocalScorePreviewInput = LocalScoreInput.extend({
targetType: z.enum(["planned_pr", "pull_request", "local_diff", "variant"]).default("local_diff"),
linkedIssueContext: linkedIssueContextSchema.optional(),
testTokenScore: z.number().min(0).optional(),
nonCodeTokenScore: z.number().min(0).optional(),
existingContributorTokenScore: z.number().min(0).optional(),
prAgeHours: z.number().min(0).optional(),
duplicateRiskCount: z.number().int().min(0).optional(),
metadataOnly: z.boolean().default(true),
});

/**
* What the REMOTE serves: everything except the fields that only mean something to a server holding the
* checkout. It cannot read a working directory, run a scorer command, or diff a branch, so it says so.
*/
export const RemoteLocalScorePreviewInput = LocalScorePreviewInput.omit({
cwd: true,
baseRef: true,
title: true,
body: true,
linkedIssues: true,
tests: true,
authorAssociation: true,
commitMessage: true,
scorePreviewCommand: true,
});

export const previewLocalPrScoreTool = defineTool({
name: "loopover_preview_local_pr_score",
title: "Preview local PR score",
Expand All @@ -274,7 +367,7 @@ export const previewLocalPrScoreTool = defineTool({
auth: "token",
locality: "local-git",
availability: "both",
input: LocalScoreInput,
input: LocalScorePreviewInput,
output: PreviewLocalPrScoreOutput,
});

Expand All @@ -287,11 +380,17 @@ export const getEligibilityPlanTool = defineTool({
auth: "token",
locality: "local-git",
availability: "both",
input: LocalScoreInput,
input: LocalScorePreviewInput,
output: GetEligibilityPlanOutput,
});

export const ComparePrVariantsInput = z.object({
// #9662: the same union each variant's SINGULAR tool takes, for the same reason -- the element type was
// the stdio server's narrower one, so the remote's own handler accepted variants the registry rejected.
variants: z.array(LocalScorePreviewInput).min(1).max(10),
});
/** What the STDIO server serves: each variant goes through its local preview, which supplies the rest. */
export const StdioComparePrVariantsInput = z.object({
variants: z.array(LocalScoreInput).min(1).max(10),
});
export const comparePrVariantsTool = defineTool({
Expand Down Expand Up @@ -342,9 +441,15 @@ export const feasibilityGateTool = defineTool({
// ── remote-proxying tools whose inputs converge here ────────────────────────────────────────────

export const MarkNotificationsReadInput = z.object({
login: z.string().min(1).optional(),
// REQUIRED (#9662): the notifications are per-contributor, and a server with no session cannot infer
// whose they are. The stdio server, which does have one, narrows it away below -- the catalog told a
// caller this was optional while the remote rejected the call, which is the drift that check exists for.
login: z.string().min(1),
ids: z.array(z.string().min(1).max(128)).max(100).optional(),
});

/** What the STDIO server serves: it resolves the login from the active profile's session. */
export const StdioMarkNotificationsReadInput = MarkNotificationsReadInput.partial({ login: true });
export const markNotificationsReadTool = defineTool({
name: "loopover_mark_notifications_read",
title: "Mark notifications read",
Expand All @@ -360,14 +465,18 @@ export const markNotificationsReadTool = defineTool({
});

export const WatchIssuesInput = z.object({
login: z.string().min(1).optional(),
/** REQUIRED for the same reason as `MarkNotificationsReadInput.login`; the stdio server narrows it. */
login: z.string().min(1),
// `.default("list")` is a runtime coercion the emitted JSON Schema cannot round-trip, so it is
// expressed as an optional field with the default stated in the description instead. Both servers
// already treat an omitted action as "list".
action: z.enum(["watch", "unwatch", "list"]).optional(),
repoFullName: z.string().min(3).max(SCENARIO_LIMITS.repoFullNameChars).optional(),
labels: z.array(z.string().min(1).max(100)).max(50).optional(),
});
/** What the STDIO server serves: same session-filled `login` narrowing as its notifications sibling. */
export const StdioWatchIssuesInput = WatchIssuesInput.partial({ login: true });

export const watchIssuesTool = defineTool({
name: "loopover_watch_issues",
title: "Watch issues",
Expand Down
5 changes: 1 addition & 4 deletions packages/loopover-contract/src/tools/miner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,11 @@
import { z } from "zod";
import { defineTool } from "../tool-definition.js";
import { toolErrorFields } from "../shared.js";
import { PLAN_STEP_STATUSES } from "../enums.js";
import { CLAIM_STATUSES, MINER_RUN_STATES, PLAN_STEP_STATUSES, QUEUE_STATUSES } from "../enums.js";

/** Statuses a portfolio-queue entry can hold. */
export const QUEUE_STATUSES = ["queued", "in_progress", "done"] as const;

/** Statuses a local claim-ledger row can hold. */
export const CLAIM_STATUSES = ["active", "released", "expired"] as const;

/** Per-status counts, repeated at both the global and per-repo level of the dashboard. */
const queueStatusCounts = z.looseObject({
Expand Down Expand Up @@ -231,7 +229,6 @@ export const minerAuditFeedTool = defineTool({
// ── run state ───────────────────────────────────────────────────────────────────────────────────

/** `RunState` (packages/loopover-miner/lib/run-state.ts). */
export const MINER_RUN_STATES = ["idle", "discovering", "planning", "preparing"] as const;

export const MinerGetRunStateInput = z.object({
repoFullName: z.string().min(1).optional(),
Expand Down
Loading