Skip to content

Commit a71cd20

Browse files
committed
wip(contract): first four AMS miner tool contracts
Refs #9536
1 parent 78061a0 commit a71cd20

1 file changed

Lines changed: 164 additions & 0 deletions

File tree

  • packages/loopover-contract/src/tools
Lines changed: 164 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,164 @@
1+
// AMS miner tool contracts (#9536).
2+
//
3+
// Every tool here is `locality: "miner"`: it reads the miner box's own local SQLite stores (plan,
4+
// event ledger, governor ledger, run state, portfolio queue, claim ledger, prediction ledger).
5+
// That is not a deployment preference -- no hosted Worker can reach those files, which is exactly
6+
// why LoopOver runs a separate MCP server for AMS rather than collapsing to one process.
7+
//
8+
// Before this migration not one of these tools declared an output schema or returned
9+
// structuredContent; every handler stringified JSON into a text block. The schemas below are
10+
// modelled from the aggregators the handlers actually call, so the structured payload each tool
11+
// gains is a description of what it already returns, not a new shape.
12+
import { z } from "zod";
13+
import { defineTool } from "../tool-definition.js";
14+
15+
/** Statuses a portfolio-queue entry can hold. */
16+
export const QUEUE_STATUSES = ["queued", "in_progress", "done"] as const;
17+
18+
/** Statuses a local claim-ledger row can hold. */
19+
export const CLAIM_STATUSES = ["active", "released", "expired"] as const;
20+
21+
/** Per-status counts, repeated at both the global and per-repo level of the dashboard. */
22+
const queueStatusCounts = z.looseObject({
23+
queued: z.number(),
24+
in_progress: z.number(),
25+
done: z.number(),
26+
});
27+
28+
// ── ping ────────────────────────────────────────────────────────────────────────────────────────
29+
30+
export const MinerPingInput = z.object({});
31+
32+
/** Static, and deliberately so: this tool exists to prove the server is reachable without touching
33+
* any store, so its output is a fixed literal rather than anything derived. */
34+
export const MinerPingOutput = z.looseObject({
35+
status: z.literal("ok"),
36+
tool: z.literal("loopover_miner_ping"),
37+
});
38+
39+
export const minerPingTool = defineTool({
40+
name: "loopover_miner_ping",
41+
title: "Miner health check",
42+
description:
43+
"Health check for the loopover-miner MCP server. Returns a static status object confirming the server is reachable. Reads no AMS state and takes no arguments.",
44+
category: "utility",
45+
auth: "public",
46+
locality: "miner",
47+
availability: "selfhost",
48+
input: MinerPingInput,
49+
output: MinerPingOutput,
50+
});
51+
52+
// ── portfolio dashboard ─────────────────────────────────────────────────────────────────────────
53+
54+
export const MinerPortfolioDashboardInput = z.object({});
55+
56+
/** `PortfolioDashboardSummary` (packages/loopover-miner/lib/portfolio-dashboard.ts).
57+
* `oldestQueuedAgeMs` is null when no clock was supplied or nothing is queued -- a real absence,
58+
* not a zero. */
59+
export const MinerPortfolioDashboardOutput = z.looseObject({
60+
total: z.number(),
61+
byStatus: queueStatusCounts,
62+
repos: z.array(
63+
z.looseObject({
64+
apiBaseUrl: z.string(),
65+
repoFullName: z.string(),
66+
byStatus: queueStatusCounts,
67+
total: z.number(),
68+
}),
69+
),
70+
oldestQueuedAgeMs: z.number().nullable(),
71+
});
72+
73+
export const minerPortfolioDashboardTool = defineTool({
74+
name: "loopover_miner_get_portfolio_dashboard",
75+
title: "Miner portfolio dashboard",
76+
description:
77+
"Read-only per-repo portfolio-queue backlog dashboard: status counts (queued/in_progress/done), totals, and the oldest-queued age in ms. Wraps the existing collectPortfolioDashboard aggregator (no new logic) -- the same data `loopover-miner queue dashboard --json` prints locally. Takes no arguments; mutates nothing.",
78+
category: "agent",
79+
auth: "public",
80+
locality: "miner",
81+
availability: "selfhost",
82+
input: MinerPortfolioDashboardInput,
83+
output: MinerPortfolioDashboardOutput,
84+
});
85+
86+
// ── manage status ───────────────────────────────────────────────────────────────────────────────
87+
88+
/** `ManageStatusRow`. Every field but the identifiers is nullable: a PR can be tracked before CI
89+
* has reported, before the gate has run, and before it has an outcome. */
90+
export const manageStatusRowSchema = z.looseObject({
91+
repoFullName: z.string(),
92+
prNumber: z.number(),
93+
branch: z.string().nullable(),
94+
ciState: z.string().nullable(),
95+
gateVerdict: z.string().nullable(),
96+
outcome: z.string().nullable(),
97+
lastPolledAt: z.string().nullable(),
98+
queueStatus: z.enum(QUEUE_STATUSES).nullable(),
99+
priority: z.number().nullable(),
100+
});
101+
102+
export const MinerManageStatusInput = z.object({});
103+
104+
/** `{ rows, runPortfolio }` -- the same pair `manage status --json` prints. */
105+
export const MinerManageStatusOutput = z.looseObject({
106+
rows: z.array(manageStatusRowSchema),
107+
runPortfolio: z.array(
108+
z.looseObject({
109+
repoFullName: z.string(),
110+
runState: z.string().nullable(),
111+
runStateUpdatedAt: z.string().nullable(),
112+
prCount: z.number(),
113+
prs: z.array(manageStatusRowSchema),
114+
}),
115+
),
116+
});
117+
118+
export const minerManageStatusTool = defineTool({
119+
name: "loopover_miner_get_manage_status",
120+
title: "Miner manage-phase status",
121+
description:
122+
"Read-only manage-phase status: the per-managed-PR rows `loopover-miner manage status` reports (branch, CI state, gate verdict, outcome, last-polled-at, queue status/priority) plus the run-level portfolio view (one row per tracked repo: run state, updated-at, PR count). Joins the portfolio queue, the append-only event ledger, and run-state by reusing the existing collectManageStatus/collectRunPortfolio aggregators -- no new join logic. Read-only: never calls GitHub, never mutates local stores. Takes no arguments.",
123+
category: "agent",
124+
auth: "public",
125+
locality: "miner",
126+
availability: "selfhost",
127+
input: MinerManageStatusInput,
128+
output: MinerManageStatusOutput,
129+
});
130+
131+
// ── claims ──────────────────────────────────────────────────────────────────────────────────────
132+
133+
export const MinerListClaimsInput = z.object({
134+
repoFullName: z.string().optional(),
135+
status: z.enum(CLAIM_STATUSES).optional(),
136+
});
137+
138+
/** The ledger's own row shape. Left open below the named fields because `listClaims` returns rows
139+
* straight from SQLite, and the store has added columns over time without the MCP surface
140+
* changing -- pinning it closed would make the next column a breaking change. */
141+
export const MinerListClaimsOutput = z.looseObject({
142+
claims: z.array(
143+
z.looseObject({
144+
repoFullName: z.string(),
145+
issueNumber: z.number(),
146+
status: z.string(),
147+
claimedAt: z.string().nullish(),
148+
note: z.string().nullish(),
149+
}),
150+
),
151+
});
152+
153+
export const minerListClaimsTool = defineTool({
154+
name: "loopover_miner_list_claims",
155+
title: "List miner claims",
156+
description:
157+
"Read-only listing of the local claim ledger: which issues this miner has claimed (repo, issue number, status, claimed-at, note). Optional repoFullName/status filters pass through to the existing listClaims query. Exposes no claim/release mutation and no conflict-resolution logic.",
158+
category: "agent",
159+
auth: "public",
160+
locality: "miner",
161+
availability: "selfhost",
162+
input: MinerListClaimsInput,
163+
output: MinerListClaimsOutput,
164+
});

0 commit comments

Comments
 (0)