diff --git a/packages/loopover-miner/bin/loopover-miner-mcp.d.ts b/packages/loopover-miner/bin/loopover-miner-mcp.d.ts index 74e1b7f117..a826e9828c 100644 --- a/packages/loopover-miner/bin/loopover-miner-mcp.d.ts +++ b/packages/loopover-miner/bin/loopover-miner-mcp.d.ts @@ -1,74 +1,86 @@ -import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; -import type { EventLedger } from "../lib/event-ledger.js"; -import type { PredictionLedgerEntry } from "../lib/prediction-ledger.js"; - -/** The static, non-secret payload the loopover_miner_ping tool always returns, independent of input. */ -export const MINER_PING_STATUS: { status: "ok"; tool: "loopover_miner_ping" }; - +#!/usr/bin/env node +import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; +import { type EventLedger } from "../lib/event-ledger.js"; +import { type PredictionLedgerEntry } from "../lib/prediction-ledger.js"; +/** The static, non-secret payload the ping tool always returns, independent of any input or AMS state. */ +export declare const MINER_PING_STATUS: { + status: string; + tool: string; +}; export interface MinerMcpServerOptions { - /** - * Override the portfolio-queue store opener (defaults to the real on-disk store); injection seam for tests. - * Typed to the minimal read surface the dashboard tool uses, mirroring runPortfolioDashboard's own seam. - */ - initPortfolioQueue?: () => { listQueue(repoFullName?: string | null): unknown[]; close(): void }; - /** - * Override the claim-ledger opener (defaults to the real on-disk ledger); injection seam for tests. Typed to - * the minimal read surface the list-claims tool uses. - */ - openClaimLedger?: () => { - listClaims(filter?: { repoFullName?: string | null; status?: string | null }): unknown[]; - close(): void; - }; - /** Override the clock used for the oldest-queued age (defaults to Date.now()); injection seam for tests. */ - nowMs?: number; - /** Override the event-ledger opener (defaults to initEventLedger); injection seam for tests. */ - initEventLedger?: () => EventLedger; - /** - * Override the run-state store opener (defaults to the real on-disk store); injection seam for tests. Typed to - * the minimal read surface the run-state tool uses (never setRunState). - */ - initRunStateStore?: () => { - getRunState(repoFullName: string): unknown; - listRunStates(): unknown[]; - close(): void; - }; - /** - * Override the plan-store opener (defaults to the real on-disk store); injection seam for tests. Typed to the - * minimal read surface the plan tools use (never savePlan). - */ - openPlanStore?: () => { - loadPlan(planId: string): unknown; - listPlans(filter?: { status?: string | null }): unknown[]; - close(): void; - }; - /** - * Override the governor-ledger opener (defaults to the real on-disk ledger); injection seam for tests. Typed - * to the minimal read surface the decisions tool uses (the payload-excluding readGovernorDecisions). - */ - initGovernorLedger?: () => { - readGovernorDecisions(filter?: { repoFullName?: string | null }): unknown[]; - close(): void; - }; - /** Override the status reader (defaults to status.js's collectStatus); injection seam for tests. */ - collectStatus?: () => unknown; - /** Override the doctor-checks reader (defaults to status.js's runDoctorChecks); injection seam for tests. */ - runDoctorChecks?: () => unknown[]; - /** - * Override the prediction-ledger opener (defaults to the real on-disk ledger); injection seam for tests. Typed - * to the minimal read surface the calibration-report tool uses (never appendPrediction). - */ - initPredictionLedger?: () => { - readPredictions(filter?: { repoFullName?: string | null }): PredictionLedgerEntry[]; - close(): void; - }; + /** + * Override the portfolio-queue store opener (defaults to the real on-disk store); injection seam for tests. + * Typed to the minimal read surface the dashboard tool uses, mirroring runPortfolioDashboard's own seam. + */ + initPortfolioQueue?: () => { + listQueue(repoFullName?: string | null): unknown[]; + close(): void; + }; + /** + * Override the claim-ledger opener (defaults to the real on-disk ledger); injection seam for tests. Typed to + * the minimal read surface the list-claims tool uses. + */ + openClaimLedger?: () => { + listClaims(filter?: { + repoFullName?: string | null; + status?: string | null; + }): unknown[]; + close(): void; + }; + /** Override the clock used for the oldest-queued age (defaults to Date.now()); injection seam for tests. */ + nowMs?: number; + /** Override the event-ledger opener (defaults to initEventLedger); injection seam for tests. */ + initEventLedger?: () => EventLedger; + /** + * Override the run-state store opener (defaults to the real on-disk store); injection seam for tests. Typed to + * the minimal read surface the run-state tool uses (never setRunState). + */ + initRunStateStore?: () => { + getRunState(repoFullName: string): unknown; + listRunStates(): unknown[]; + close(): void; + }; + /** + * Override the plan-store opener (defaults to the real on-disk store); injection seam for tests. Typed to the + * minimal read surface the plan tools use (never savePlan). + */ + openPlanStore?: () => { + loadPlan(planId: string): unknown; + listPlans(filter?: { + status?: string | null; + }): unknown[]; + close(): void; + }; + /** + * Override the governor-ledger opener (defaults to the real on-disk ledger); injection seam for tests. Typed + * to the minimal read surface the decisions tool uses (the payload-excluding readGovernorDecisions). + */ + initGovernorLedger?: () => { + readGovernorDecisions(filter?: { + repoFullName?: string | null; + }): unknown[]; + close(): void; + }; + /** Override the status reader (defaults to status.js's collectStatus); injection seam for tests. */ + collectStatus?: () => unknown; + /** Override the doctor-checks reader (defaults to status.js's runDoctorChecks); injection seam for tests. */ + runDoctorChecks?: () => unknown[]; + /** + * Override the prediction-ledger opener (defaults to the real on-disk ledger); injection seam for tests. Typed + * to the minimal read surface the calibration-report tool uses (never appendPrediction). + */ + initPredictionLedger?: () => { + readPredictions(filter?: { + repoFullName?: string | null; + }): PredictionLedgerEntry[]; + close(): void; + }; } - /** - * Build the miner MCP server with its tools registered (loopover_miner_ping, - * loopover_miner_get_portfolio_dashboard, loopover_miner_get_manage_status, loopover_miner_list_claims, - * loopover_miner_get_audit_feed, loopover_miner_get_run_state, loopover_miner_list_plans, - * loopover_miner_get_plan, loopover_miner_get_governor_decisions, loopover_miner_status, - * loopover_miner_get_calibration_report). `options` supplies test injection seams; production callers - * pass nothing. + * Build the miner MCP server with its tools registered. `options.initPortfolioQueue`, `options.openClaimLedger`, + * `options.initEventLedger`, `options.initRunStateStore`, `options.openPlanStore`, `options.initGovernorLedger`, + * `options.collectStatus`, `options.runDoctorChecks`, and `options.nowMs` are injection seams for tests (default + * to the real stores/readers and the wall clock); the ping tool needs none. Each store-backed tool opens its + * store only when invoked and closes any store it opened. */ -export function createMinerMcpServer(options?: MinerMcpServerOptions): McpServer; +export declare function createMinerMcpServer(options?: MinerMcpServerOptions): McpServer; diff --git a/packages/loopover-miner/bin/loopover-miner-mcp.js b/packages/loopover-miner/bin/loopover-miner-mcp.js index a57bf4e4e6..77fa62342a 100755 --- a/packages/loopover-miner/bin/loopover-miner-mcp.js +++ b/packages/loopover-miner/bin/loopover-miner-mcp.js @@ -1,14 +1,12 @@ #!/usr/bin/env node import { readFileSync, realpathSync } from "node:fs"; +import { dirname, join } from "node:path"; import { fileURLToPath } from "node:url"; import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; import { z } from "zod"; import { CLAIM_STATUSES, openClaimLedger } from "../lib/claim-ledger.js"; -import { - collectEventLedgerAuditFeed, - normalizeAuditFeedMcpFilter, -} from "../lib/event-ledger-cli.js"; +import { collectEventLedgerAuditFeed, normalizeAuditFeedMcpFilter, } from "../lib/event-ledger-cli.js"; import { initEventLedger } from "../lib/event-ledger.js"; import { collectManageStatus, collectRunPortfolio } from "../lib/manage-status.js"; import { collectPortfolioDashboard } from "../lib/portfolio-dashboard.js"; @@ -23,7 +21,6 @@ import { initPredictionLedger } from "../lib/prediction-ledger.js"; import { loadMinerFileSecrets } from "../lib/env-file-indirection.js"; import { installCliSignalHandlers } from "../lib/process-lifecycle.js"; import { captureMinerErrorAndFlush, initMinerSentry } from "../lib/sentry.js"; - // MCP stdio server for @loopover/miner (scaffold #5153). Mirrors the packages/loopover-mcp // harness (MCP SDK server + stdio transport). Tools: // - loopover_miner_ping (#5153): trivial static health check, reads no AMS state. @@ -48,21 +45,22 @@ import { captureMinerErrorAndFlush, initMinerSentry } from "../lib/sentry.js"; // the prediction ledger with observed pr_outcome events via calibration-cli.js's existing toPredictionRecords/ // toOutcomeRecords mappers and calibration.js's buildCalibrationReport composer (no new join logic). Distinct // from ORB's hosted, maintainer-authenticated loopover_get_outcome_calibration tool. - // Read the version from this package's own package.json (always shipped) rather than a hand-synced // literal, so a release bump never has a second place to forget -- same approach as the mcp harness. -const ownPackageJson = JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf8")); - +// Resolve via fileURLToPath(import.meta.url) (a string) rather than `new URL(...)` so the path never +// materializes as a `URL` object -- the repo-root tsconfig this file is also checked under (its type +// surface is imported by the MCP unit tests) resolves the global `URL` to a shape whose iterator lacks +// `[Symbol.dispose]`, which readFileSync's node typings reject; a plain string sidesteps that entirely. +const packageJsonPath = join(dirname(fileURLToPath(import.meta.url)), "../package.json"); +const ownPackageJson = JSON.parse(readFileSync(packageJsonPath, "utf8")); /** Optional filters accepted by loopover_miner_get_audit_feed (#5158). */ const auditFeedInputSchema = { - repoFullName: z.string().min(1).optional(), - since: z.number().int().nonnegative().optional(), - type: z.string().min(1).optional(), + repoFullName: z.string().min(1).optional(), + since: z.number().int().nonnegative().optional(), + type: z.string().min(1).optional(), }; - /** The static, non-secret payload the ping tool always returns, independent of any input or AMS state. */ export const MINER_PING_STATUS = { status: "ok", tool: "loopover_miner_ping" }; - /** * Build the miner MCP server with its tools registered. `options.initPortfolioQueue`, `options.openClaimLedger`, * `options.initEventLedger`, `options.initRunStateStore`, `options.openPlanStore`, `options.initGovernorLedger`, @@ -71,276 +69,257 @@ export const MINER_PING_STATUS = { status: "ok", tool: "loopover_miner_ping" }; * store only when invoked and closes any store it opened. */ export function createMinerMcpServer(options = {}) { - const server = new McpServer({ name: "loopover-miner", version: ownPackageJson.version }); - server.registerTool( - "loopover_miner_ping", - { - description: - "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.", - inputSchema: {}, - }, - async () => ({ content: [{ type: "text", text: JSON.stringify(MINER_PING_STATUS) }] }), - ); - server.registerTool( - "loopover_miner_get_portfolio_dashboard", - { - description: - "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.", - inputSchema: {}, - }, - async () => { - const ownsQueue = options.initPortfolioQueue === undefined; - const portfolioQueue = (options.initPortfolioQueue ?? initPortfolioQueueStore)(); - try { - const summary = collectPortfolioDashboard({ portfolioQueue }, { nowMs: options.nowMs ?? Date.now() }); - return { content: [{ type: "text", text: JSON.stringify(summary) }] }; - } finally { - if (ownsQueue) portfolioQueue.close(); - } - }, - ); - server.registerTool( - "loopover_miner_get_manage_status", - { - description: - "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 -- returning the same { rows, runPortfolio } shape `manage status --json` prints. " + - "Read-only: never calls GitHub, never mutates local stores. Takes no arguments.", - inputSchema: {}, - }, - async () => { - const ownsPortfolioQueue = options.initPortfolioQueue === undefined; - const ownsEventLedger = options.initEventLedger === undefined; - const ownsRunStateStore = options.initRunStateStore === undefined; - const portfolioQueue = (options.initPortfolioQueue ?? initPortfolioQueueStore)(); - const eventLedger = (options.initEventLedger ?? initEventLedger)(); - const runStateStore = (options.initRunStateStore ?? initRunStateStore)(); - try { - const rows = collectManageStatus({ portfolioQueue, eventLedger }); - const runPortfolio = collectRunPortfolio({ portfolioQueue, eventLedger, runStateStore }); - return { content: [{ type: "text", text: JSON.stringify({ rows, runPortfolio }) }] }; - } finally { - if (ownsPortfolioQueue) portfolioQueue.close(); - if (ownsEventLedger) eventLedger.close(); - if (ownsRunStateStore) runStateStore.close(); - } - }, - ); - server.registerTool( - "loopover_miner_list_claims", - { - description: - "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.", - inputSchema: { - repoFullName: z.string().optional(), - status: z.enum(CLAIM_STATUSES).optional(), - }, - }, - async ({ repoFullName, status }) => { - const ownsLedger = options.openClaimLedger === undefined; - const ledger = (options.openClaimLedger ?? openClaimLedger)(); - try { - const filter = {}; - if (repoFullName !== undefined) filter.repoFullName = repoFullName; - if (status !== undefined) filter.status = status; - return { content: [{ type: "text", text: JSON.stringify(ledger.listClaims(filter)) }] }; - } finally { - if (ownsLedger) ledger.close(); - } - }, - ); - server.registerTool( - "loopover_miner_get_audit_feed", - { - description: - "Read-only, metadata-only audit feed from the local append-only event ledger: eventType, repoFullName, " + - "outcome, actor, detail, and createdAt per row. Wraps collectEventLedgerAuditFeed() (no new query logic) — " + - "the same read filters as `loopover-miner ledger list` (--repo, --since, --type). Never returns " + - "payload_json or other raw ledger columns; never writes to the ledger.", - inputSchema: auditFeedInputSchema, - }, - async (input) => { - const ownsLedger = options.initEventLedger === undefined; - const eventLedger = (options.initEventLedger ?? initEventLedger)(); - try { - const filter = normalizeAuditFeedMcpFilter(input ?? {}); - const feed = collectEventLedgerAuditFeed(eventLedger, filter); - return { content: [{ type: "text", text: JSON.stringify(feed) }] }; - } catch (error) { - return { - content: [ - { - type: "text", - text: JSON.stringify({ - error: error instanceof Error ? error.message : String(error), - }), - }, - ], - isError: true, - }; - } finally { - if (ownsLedger) eventLedger.close(); - } - }, - ); - server.registerTool( - "loopover_miner_get_run_state", - { - description: - "Read-only per-repo miner run-state (idle/discovering/planning/preparing). Pass repoFullName for a single " + - "repo (a null state means none has been recorded for it yet), or omit it to list every repo's state. The " + - "read-only analog of ORB's loopover_get_automation_state; adds no state-set or mutation capability.", - inputSchema: { - repoFullName: z.string().min(1).optional(), - }, - }, - async ({ repoFullName }) => { - const ownsStore = options.initRunStateStore === undefined; - const store = (options.initRunStateStore ?? initRunStateStore)(); - try { - const result = - repoFullName === undefined - ? { states: store.listRunStates() } - : { repoFullName, state: store.getRunState(repoFullName) }; - return { content: [{ type: "text", text: JSON.stringify(result) }] }; - } finally { - if (ownsStore) store.close(); - } - }, - ); - server.registerTool( - "loopover_miner_list_plans", - { - description: - "Read-only list of the miner's PERSISTED plan store (planId, plan DAG, status, updatedAt), optionally " + - "filtered by status. Wraps plan-store.js's existing listPlans query -- no new logic, no mutation. NOTE: " + - "this is the store-backed AMS plan store; it is distinct from ORB's stateless loopover_plan_status " + - "tool, which reads the caller's in-memory plan object rather than any persisted store.", - inputSchema: { - status: z.enum(PLAN_STATUSES).optional(), - }, - }, - async ({ status }) => { - const ownsStore = options.openPlanStore === undefined; - const store = (options.openPlanStore ?? openPlanStore)(); - try { - const filter = {}; - if (status !== undefined) filter.status = status; - return { content: [{ type: "text", text: JSON.stringify(store.listPlans(filter)) }] }; - } finally { - if (ownsStore) store.close(); - } - }, - ); - server.registerTool( - "loopover_miner_get_plan", - { - description: - "Read-only fetch of one persisted plan record by planId (the full plan DAG, status, updatedAt), or an " + - "explicit { planId, found: false } for an unknown id. Wraps plan-store.js's existing loadPlan lookup -- " + - "no mutation, no DAG/planning logic. Store-backed AMS plan store; distinct from ORB's stateless " + - "loopover_plan_status tool.", - inputSchema: { - planId: z.string().min(1), - }, - }, - async ({ planId }) => { - const ownsStore = options.openPlanStore === undefined; - const store = (options.openPlanStore ?? openPlanStore)(); - try { - const plan = store.loadPlan(planId); - const result = plan === null ? { planId, found: false } : { found: true, plan }; - return { content: [{ type: "text", text: JSON.stringify(result) }] }; - } finally { - if (ownsStore) store.close(); - } - }, - ); - server.registerTool( - "loopover_miner_get_governor_decisions", - { - description: - "Read-only projection of the governor decision log: id, ts, eventType, repoFullName, actionClass, " + - "decision, reason per row. This projection INTENTIONALLY EXCLUDES the internal/sensitive payload column " + - "(reputation / self-plagiarism / budget state) by construction -- governor-ledger.js reads it with an " + - "explicit named-column SELECT, never SELECT *. Optional repoFullName filter (the only filter the ledger " + - "supports natively). Read-only; never writes to the ledger.", - inputSchema: { - repoFullName: z.string().min(1).optional(), - }, - }, - async ({ repoFullName }) => { - const ownsLedger = options.initGovernorLedger === undefined; - const ledger = (options.initGovernorLedger ?? initGovernorLedger)(); - try { - const filter = {}; - if (repoFullName !== undefined) filter.repoFullName = repoFullName; - return { content: [{ type: "text", text: JSON.stringify(ledger.readGovernorDecisions(filter)) }] }; - } finally { - if (ownsLedger) ledger.close(); - } - }, - ); - server.registerTool( - "loopover_miner_status", - { - description: - "Read-only miner status + doctor diagnostics. Returns { status, doctor }: status = package/engine versions " + - "(+ skew), node version, state-dir path, config-file path, and the resolved coding-agent driver (provider " + - "name, the model ENV-VAR NAME -- never its value -- and a CLI-present boolean); doctor = the same checks " + - "`loopover-miner doctor` runs (Docker/CLI presence, config validity, ...) as { name, ok, detail }. Reuses " + - "collectStatus/runDoctorChecks so it can never drift from the CLI. Only names / booleans / paths -- never " + - "any env-var value, token, key, or credential. Read-only; no writes or state changes.", - inputSchema: {}, - }, - async () => { - const status = (options.collectStatus ?? collectStatus)(); - const doctor = (options.runDoctorChecks ?? runDoctorChecks)(); - return { content: [{ type: "text", text: JSON.stringify({ status, doctor }) }] }; - }, - ); - server.registerTool( - "loopover_miner_get_calibration_report", - { - description: - "Read-only miner-local prediction-accuracy report: per-project merge/close precision, joining this " + - "miner's own recorded gate predictions (prediction ledger) with the realized PR outcomes it later " + - "observed (pr_outcome events). Wraps calibration-cli.js's existing toPredictionRecords/toOutcomeRecords " + - "mappers and calibration.js's buildCalibrationReport composer -- no new join/scoring logic, no mutation. " + - "Strictly local and offline; distinct from ORB's hosted, maintainer-authenticated " + - "loopover_get_outcome_calibration tool, which reads a different (D1) data source. Takes no arguments.", - inputSchema: {}, - }, - async () => { - const ownsPredictionLedger = options.initPredictionLedger === undefined; - const ownsEventLedger = options.initEventLedger === undefined; - let predictionLedger; - let eventLedger; - try { - predictionLedger = (options.initPredictionLedger ?? initPredictionLedger)(); - eventLedger = (options.initEventLedger ?? initEventLedger)(); - const report = buildCalibrationReport( - toPredictionRecords(predictionLedger.readPredictions()), - toOutcomeRecords(eventLedger.readEvents()), - ); - return { content: [{ type: "text", text: JSON.stringify(report) }] }; - } finally { - if (ownsPredictionLedger) predictionLedger?.close(); - if (ownsEventLedger) eventLedger?.close(); - } - }, - ); - return server; + const server = new McpServer({ name: "loopover-miner", version: ownPackageJson.version }); + server.registerTool("loopover_miner_ping", { + description: "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.", + inputSchema: {}, + }, async () => ({ content: [{ type: "text", text: JSON.stringify(MINER_PING_STATUS) }] })); + server.registerTool("loopover_miner_get_portfolio_dashboard", { + description: "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.", + inputSchema: {}, + }, async () => { + const ownsQueue = options.initPortfolioQueue === undefined; + const portfolioQueue = (options.initPortfolioQueue ?? initPortfolioQueueStore)(); + try { + const summary = collectPortfolioDashboard({ portfolioQueue }, { nowMs: options.nowMs ?? Date.now() }); + return { content: [{ type: "text", text: JSON.stringify(summary) }] }; + } + finally { + if (ownsQueue) + portfolioQueue.close(); + } + }); + server.registerTool("loopover_miner_get_manage_status", { + description: "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 -- returning the same { rows, runPortfolio } shape `manage status --json` prints. " + + "Read-only: never calls GitHub, never mutates local stores. Takes no arguments.", + inputSchema: {}, + }, async () => { + const ownsPortfolioQueue = options.initPortfolioQueue === undefined; + const ownsEventLedger = options.initEventLedger === undefined; + const ownsRunStateStore = options.initRunStateStore === undefined; + const portfolioQueue = (options.initPortfolioQueue ?? initPortfolioQueueStore)(); + const eventLedger = (options.initEventLedger ?? initEventLedger)(); + const runStateStore = (options.initRunStateStore ?? initRunStateStore)(); + try { + // The injection seams above are typed to the minimal read surface each tool touches (mirroring the + // dashboard tool's `{ listQueue }` seam), but collectManageStatus/collectRunPortfolio's declared source + // types name the full stores. Both aggregators only ever read (listQueue/getRunState/listRunStates) at + // runtime -- exactly what the seam guarantees -- so widening the resolved stores back to the store types + // the signatures ask for is sound; it never reaches a write/lifecycle method the minimal seam omits. + const rows = collectManageStatus({ + portfolioQueue: portfolioQueue, + eventLedger, + }); + const runPortfolio = collectRunPortfolio({ + portfolioQueue: portfolioQueue, + eventLedger, + runStateStore: runStateStore, + }); + return { content: [{ type: "text", text: JSON.stringify({ rows, runPortfolio }) }] }; + } + finally { + if (ownsPortfolioQueue) + portfolioQueue.close(); + if (ownsEventLedger) + eventLedger.close(); + if (ownsRunStateStore) + runStateStore.close(); + } + }); + server.registerTool("loopover_miner_list_claims", { + description: "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.", + inputSchema: { + repoFullName: z.string().optional(), + status: z.enum(CLAIM_STATUSES).optional(), + }, + }, async ({ repoFullName, status }) => { + const ownsLedger = options.openClaimLedger === undefined; + const ledger = (options.openClaimLedger ?? openClaimLedger)(); + try { + const filter = {}; + if (repoFullName !== undefined) + filter.repoFullName = repoFullName; + if (status !== undefined) + filter.status = status; + return { content: [{ type: "text", text: JSON.stringify(ledger.listClaims(filter)) }] }; + } + finally { + if (ownsLedger) + ledger.close(); + } + }); + server.registerTool("loopover_miner_get_audit_feed", { + description: "Read-only, metadata-only audit feed from the local append-only event ledger: eventType, repoFullName, " + + "outcome, actor, detail, and createdAt per row. Wraps collectEventLedgerAuditFeed() (no new query logic) — " + + "the same read filters as `loopover-miner ledger list` (--repo, --since, --type). Never returns " + + "payload_json or other raw ledger columns; never writes to the ledger.", + inputSchema: auditFeedInputSchema, + }, async (input) => { + const ownsLedger = options.initEventLedger === undefined; + const eventLedger = (options.initEventLedger ?? initEventLedger)(); + try { + // zod's `.optional()` widens each field to `string | undefined`, whereas normalizeAuditFeedMcpFilter's + // input type spells the same absent-field slot as `string | null`; the normalizer treats missing and + // null identically, so narrowing the parsed input to that shape is exact, not a behavior change. + const filter = normalizeAuditFeedMcpFilter((input ?? {})); + const feed = collectEventLedgerAuditFeed(eventLedger, filter); + return { content: [{ type: "text", text: JSON.stringify(feed) }] }; + } + catch (error) { + return { + content: [ + { + type: "text", + text: JSON.stringify({ + error: error instanceof Error ? error.message : String(error), + }), + }, + ], + isError: true, + }; + } + finally { + if (ownsLedger) + eventLedger.close(); + } + }); + server.registerTool("loopover_miner_get_run_state", { + description: "Read-only per-repo miner run-state (idle/discovering/planning/preparing). Pass repoFullName for a single " + + "repo (a null state means none has been recorded for it yet), or omit it to list every repo's state. The " + + "read-only analog of ORB's loopover_get_automation_state; adds no state-set or mutation capability.", + inputSchema: { + repoFullName: z.string().min(1).optional(), + }, + }, async ({ repoFullName }) => { + const ownsStore = options.initRunStateStore === undefined; + const store = (options.initRunStateStore ?? initRunStateStore)(); + try { + const result = repoFullName === undefined + ? { states: store.listRunStates() } + : { repoFullName, state: store.getRunState(repoFullName) }; + return { content: [{ type: "text", text: JSON.stringify(result) }] }; + } + finally { + if (ownsStore) + store.close(); + } + }); + server.registerTool("loopover_miner_list_plans", { + description: "Read-only list of the miner's PERSISTED plan store (planId, plan DAG, status, updatedAt), optionally " + + "filtered by status. Wraps plan-store.js's existing listPlans query -- no new logic, no mutation. NOTE: " + + "this is the store-backed AMS plan store; it is distinct from ORB's stateless loopover_plan_status " + + "tool, which reads the caller's in-memory plan object rather than any persisted store.", + inputSchema: { + status: z.enum(PLAN_STATUSES).optional(), + }, + }, async ({ status }) => { + const ownsStore = options.openPlanStore === undefined; + const store = (options.openPlanStore ?? openPlanStore)(); + try { + const filter = {}; + if (status !== undefined) + filter.status = status; + return { content: [{ type: "text", text: JSON.stringify(store.listPlans(filter)) }] }; + } + finally { + if (ownsStore) + store.close(); + } + }); + server.registerTool("loopover_miner_get_plan", { + description: "Read-only fetch of one persisted plan record by planId (the full plan DAG, status, updatedAt), or an " + + "explicit { planId, found: false } for an unknown id. Wraps plan-store.js's existing loadPlan lookup -- " + + "no mutation, no DAG/planning logic. Store-backed AMS plan store; distinct from ORB's stateless " + + "loopover_plan_status tool.", + inputSchema: { + planId: z.string().min(1), + }, + }, async ({ planId }) => { + const ownsStore = options.openPlanStore === undefined; + const store = (options.openPlanStore ?? openPlanStore)(); + try { + const plan = store.loadPlan(planId); + const result = plan === null ? { planId, found: false } : { found: true, plan }; + return { content: [{ type: "text", text: JSON.stringify(result) }] }; + } + finally { + if (ownsStore) + store.close(); + } + }); + server.registerTool("loopover_miner_get_governor_decisions", { + description: "Read-only projection of the governor decision log: id, ts, eventType, repoFullName, actionClass, " + + "decision, reason per row. This projection INTENTIONALLY EXCLUDES the internal/sensitive payload column " + + "(reputation / self-plagiarism / budget state) by construction -- governor-ledger.js reads it with an " + + "explicit named-column SELECT, never SELECT *. Optional repoFullName filter (the only filter the ledger " + + "supports natively). Read-only; never writes to the ledger.", + inputSchema: { + repoFullName: z.string().min(1).optional(), + }, + }, async ({ repoFullName }) => { + const ownsLedger = options.initGovernorLedger === undefined; + const ledger = (options.initGovernorLedger ?? initGovernorLedger)(); + try { + const filter = {}; + if (repoFullName !== undefined) + filter.repoFullName = repoFullName; + return { content: [{ type: "text", text: JSON.stringify(ledger.readGovernorDecisions(filter)) }] }; + } + finally { + if (ownsLedger) + ledger.close(); + } + }); + server.registerTool("loopover_miner_status", { + description: "Read-only miner status + doctor diagnostics. Returns { status, doctor }: status = package/engine versions " + + "(+ skew), node version, state-dir path, config-file path, and the resolved coding-agent driver (provider " + + "name, the model ENV-VAR NAME -- never its value -- and a CLI-present boolean); doctor = the same checks " + + "`loopover-miner doctor` runs (Docker/CLI presence, config validity, ...) as { name, ok, detail }. Reuses " + + "collectStatus/runDoctorChecks so it can never drift from the CLI. Only names / booleans / paths -- never " + + "any env-var value, token, key, or credential. Read-only; no writes or state changes.", + inputSchema: {}, + }, async () => { + const status = (options.collectStatus ?? collectStatus)(); + const doctor = (options.runDoctorChecks ?? runDoctorChecks)(); + return { content: [{ type: "text", text: JSON.stringify({ status, doctor }) }] }; + }); + server.registerTool("loopover_miner_get_calibration_report", { + description: "Read-only miner-local prediction-accuracy report: per-project merge/close precision, joining this " + + "miner's own recorded gate predictions (prediction ledger) with the realized PR outcomes it later " + + "observed (pr_outcome events). Wraps calibration-cli.js's existing toPredictionRecords/toOutcomeRecords " + + "mappers and calibration.js's buildCalibrationReport composer -- no new join/scoring logic, no mutation. " + + "Strictly local and offline; distinct from ORB's hosted, maintainer-authenticated " + + "loopover_get_outcome_calibration tool, which reads a different (D1) data source. Takes no arguments.", + inputSchema: {}, + }, async () => { + const ownsPredictionLedger = options.initPredictionLedger === undefined; + const ownsEventLedger = options.initEventLedger === undefined; + let predictionLedger; + let eventLedger; + try { + predictionLedger = (options.initPredictionLedger ?? initPredictionLedger)(); + eventLedger = (options.initEventLedger ?? initEventLedger)(); + const report = buildCalibrationReport(toPredictionRecords(predictionLedger.readPredictions()), toOutcomeRecords(eventLedger.readEvents())); + return { content: [{ type: "text", text: JSON.stringify(report) }] }; + } + finally { + if (ownsPredictionLedger) + predictionLedger?.close(); + if (ownsEventLedger) + eventLedger?.close(); + } + }); + return server; } - // Start the stdio transport only when executed directly as the bin, not when imported by a test. // realpathSync on both sides resolves the npm bin symlink so a global/npx install still matches. const invokedPath = process.argv[1] ? realpathSync(process.argv[1]) : ""; @@ -350,26 +329,27 @@ const invokedPath = process.argv[1] ? realpathSync(process.argv[1]) : ""; * those tests; this is only the top-level "am I actually invoked as the bin" wiring, mirroring * loopover-miner.js's identical exemption and src/server.ts's in codecov.yml. */ if (invokedPath && invokedPath === realpathSync(fileURLToPath(import.meta.url))) { - // Previously this bin had NO crash safety net beyond the startup .catch() below -- an exception thrown while - // handling an MCP tool call, after the server was already connected, had nowhere to go (#6011). Wire in the - // same opt-in Sentry + signal/crash handling loopover-miner.js already gets, sharing process-lifecycle.js. - try { - loadMinerFileSecrets(); - } catch (error) { - console.error(error instanceof Error ? error.message : String(error)); - process.exit(1); - } - await initMinerSentry(process.env); - installCliSignalHandlers({ captureError: captureMinerErrorAndFlush }); - - createMinerMcpServer() - .connect(new StdioServerTransport()) - .catch(async (error) => { - console.error(error); - // Awaited so the captured event has a chance to actually reach Sentry before exit() tears the process - // down -- a bare synchronous capture only queues it (#6011 follow-up). - await captureMinerErrorAndFlush(error, { kind: "mcp_startup_connect_failed" }); - process.exit(1); + // Previously this bin had NO crash safety net beyond the startup .catch() below -- an exception thrown while + // handling an MCP tool call, after the server was already connected, had nowhere to go (#6011). Wire in the + // same opt-in Sentry + signal/crash handling loopover-miner.js already gets, sharing process-lifecycle.js. + try { + loadMinerFileSecrets(); + } + catch (error) { + console.error(error instanceof Error ? error.message : String(error)); + process.exit(1); + } + await initMinerSentry(process.env); + installCliSignalHandlers({ captureError: captureMinerErrorAndFlush }); + createMinerMcpServer() + .connect(new StdioServerTransport()) + .catch(async (error) => { + console.error(error); + // Awaited so the captured event has a chance to actually reach Sentry before exit() tears the process + // down -- a bare synchronous capture only queues it (#6011 follow-up). + await captureMinerErrorAndFlush(error, { kind: "mcp_startup_connect_failed" }); + process.exit(1); }); } /* v8 ignore stop */ +//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoibG9vcG92ZXItbWluZXItbWNwLmpzIiwic291cmNlUm9vdCI6IiIsInNvdXJjZXMiOlsibG9vcG92ZXItbWluZXItbWNwLnRzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiI7QUFDQSxPQUFPLEVBQUUsWUFBWSxFQUFFLFlBQVksRUFBRSxNQUFNLFNBQVMsQ0FBQztBQUNyRCxPQUFPLEVBQUUsT0FBTyxFQUFFLElBQUksRUFBRSxNQUFNLFdBQVcsQ0FBQztBQUMxQyxPQUFPLEVBQUUsYUFBYSxFQUFFLE1BQU0sVUFBVSxDQUFDO0FBQ3pDLE9BQU8sRUFBRSxTQUFTLEVBQUUsTUFBTSx5Q0FBeUMsQ0FBQztBQUNwRSxPQUFPLEVBQUUsb0JBQW9CLEVBQUUsTUFBTSwyQ0FBMkMsQ0FBQztBQUNqRixPQUFPLEVBQUUsQ0FBQyxFQUFFLE1BQU0sS0FBSyxDQUFDO0FBQ3hCLE9BQU8sRUFBRSxjQUFjLEVBQUUsZUFBZSxFQUFFLE1BQU0sd0JBQXdCLENBQUM7QUFDekUsT0FBTyxFQUVMLDJCQUEyQixFQUMzQiwyQkFBMkIsR0FDNUIsTUFBTSw0QkFBNEIsQ0FBQztBQUNwQyxPQUFPLEVBQUUsZUFBZSxFQUFvQixNQUFNLHdCQUF3QixDQUFDO0FBQzNFLE9BQU8sRUFBRSxtQkFBbUIsRUFBRSxtQkFBbUIsRUFBRSxNQUFNLHlCQUF5QixDQUFDO0FBQ25GLE9BQU8sRUFBRSx5QkFBeUIsRUFBRSxNQUFNLCtCQUErQixDQUFDO0FBQzFFLE9BQU8sRUFBRSx1QkFBdUIsRUFBNEIsTUFBTSwyQkFBMkIsQ0FBQztBQUM5RixPQUFPLEVBQUUsaUJBQWlCLEVBQXNCLE1BQU0scUJBQXFCLENBQUM7QUFDNUUsT0FBTyxFQUFFLGFBQWEsRUFBRSxhQUFhLEVBQUUsTUFBTSxzQkFBc0IsQ0FBQztBQUNwRSxPQUFPLEVBQUUsa0JBQWtCLEVBQUUsTUFBTSwyQkFBMkIsQ0FBQztBQUMvRCxPQUFPLEVBQUUsYUFBYSxFQUFFLGVBQWUsRUFBRSxNQUFNLGtCQUFrQixDQUFDO0FBQ2xFLE9B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 \ No newline at end of file diff --git a/packages/loopover-miner/bin/loopover-miner-mcp.ts b/packages/loopover-miner/bin/loopover-miner-mcp.ts new file mode 100755 index 0000000000..b37eddd07e --- /dev/null +++ b/packages/loopover-miner/bin/loopover-miner-mcp.ts @@ -0,0 +1,455 @@ +#!/usr/bin/env node +import { readFileSync, realpathSync } from "node:fs"; +import { dirname, join } from "node:path"; +import { fileURLToPath } from "node:url"; +import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; +import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; +import { z } from "zod"; +import { CLAIM_STATUSES, openClaimLedger } from "../lib/claim-ledger.js"; +import { + type AuditFeedMcpFilterInput, + collectEventLedgerAuditFeed, + normalizeAuditFeedMcpFilter, +} from "../lib/event-ledger-cli.js"; +import { initEventLedger, type EventLedger } from "../lib/event-ledger.js"; +import { collectManageStatus, collectRunPortfolio } from "../lib/manage-status.js"; +import { collectPortfolioDashboard } from "../lib/portfolio-dashboard.js"; +import { initPortfolioQueueStore, type PortfolioQueueStore } from "../lib/portfolio-queue.js"; +import { initRunStateStore, type RunStateStore } from "../lib/run-state.js"; +import { PLAN_STATUSES, openPlanStore } from "../lib/plan-store.js"; +import { initGovernorLedger } from "../lib/governor-ledger.js"; +import { collectStatus, runDoctorChecks } from "../lib/status.js"; +import { buildCalibrationReport } from "../lib/calibration.js"; +import { toOutcomeRecords, toPredictionRecords } from "../lib/calibration-cli.js"; +import { initPredictionLedger, type PredictionLedgerEntry } from "../lib/prediction-ledger.js"; +import { loadMinerFileSecrets } from "../lib/env-file-indirection.js"; +import { installCliSignalHandlers } from "../lib/process-lifecycle.js"; +import { captureMinerErrorAndFlush, initMinerSentry } from "../lib/sentry.js"; + +// MCP stdio server for @loopover/miner (scaffold #5153). Mirrors the packages/loopover-mcp +// harness (MCP SDK server + stdio transport). Tools: +// - loopover_miner_ping (#5153): trivial static health check, reads no AMS state. +// - loopover_miner_get_portfolio_dashboard (#5155): read-only per-repo backlog dashboard, wrapping the +// existing collectPortfolioDashboard aggregator (no new logic; same data as `queue dashboard --json`). +// - loopover_miner_get_manage_status (#5822): read-only manage-phase status joining the portfolio queue, the +// event ledger, and run-state via manage-status.js's collectManageStatus/collectRunPortfolio (no new join +// logic; same { rows, runPortfolio } shape as `manage status --json`). Never calls GitHub, never mutates. +// - loopover_miner_list_claims (#5156): read-only listing of the local claim ledger (optional repo/status +// filter passed through to listClaims); exposes no claim/release mutation. +// - loopover_miner_get_audit_feed (#5158): read-only metadata-only event-ledger audit feed via +// collectEventLedgerAuditFeed() (same filters as `ledger list`; never returns payload_json). +// - loopover_miner_get_run_state (#5160): read-only per-repo run-state via run-state.js's getRunState/ +// listRunStates (read-only analog of ORB's loopover_get_automation_state; no state-set mutation). +// - loopover_miner_list_plans / loopover_miner_get_plan (#5161): read-only access to the persisted +// plan store via plan-store.js's listPlans/loadPlan (distinct from ORB's stateless loopover_plan_status). +// - loopover_miner_get_governor_decisions (#5159): read-only governor decision-log projection via +// governor-ledger.js's readGovernorDecisions -- an explicit named-column read that excludes payload_json. +// - loopover_miner_status (#5154): read-only status + doctor diagnostics via status.js's collectStatus/ +// runDoctorChecks (names/booleans/paths only -- never any env-var value, token, key, or credential). +// - loopover_miner_get_calibration_report (#5821): read-only miner-local prediction-accuracy report, joining +// the prediction ledger with observed pr_outcome events via calibration-cli.js's existing toPredictionRecords/ +// toOutcomeRecords mappers and calibration.js's buildCalibrationReport composer (no new join logic). Distinct +// from ORB's hosted, maintainer-authenticated loopover_get_outcome_calibration tool. + +// Read the version from this package's own package.json (always shipped) rather than a hand-synced +// literal, so a release bump never has a second place to forget -- same approach as the mcp harness. +// Resolve via fileURLToPath(import.meta.url) (a string) rather than `new URL(...)` so the path never +// materializes as a `URL` object -- the repo-root tsconfig this file is also checked under (its type +// surface is imported by the MCP unit tests) resolves the global `URL` to a shape whose iterator lacks +// `[Symbol.dispose]`, which readFileSync's node typings reject; a plain string sidesteps that entirely. +const packageJsonPath = join(dirname(fileURLToPath(import.meta.url)), "../package.json"); +const ownPackageJson = JSON.parse(readFileSync(packageJsonPath, "utf8")); + +/** Optional filters accepted by loopover_miner_get_audit_feed (#5158). */ +const auditFeedInputSchema = { + repoFullName: z.string().min(1).optional(), + since: z.number().int().nonnegative().optional(), + type: z.string().min(1).optional(), +}; + +/** The static, non-secret payload the ping tool always returns, independent of any input or AMS state. */ +export const MINER_PING_STATUS = { status: "ok", tool: "loopover_miner_ping" }; + +export interface MinerMcpServerOptions { + /** + * Override the portfolio-queue store opener (defaults to the real on-disk store); injection seam for tests. + * Typed to the minimal read surface the dashboard tool uses, mirroring runPortfolioDashboard's own seam. + */ + initPortfolioQueue?: () => { listQueue(repoFullName?: string | null): unknown[]; close(): void }; + /** + * Override the claim-ledger opener (defaults to the real on-disk ledger); injection seam for tests. Typed to + * the minimal read surface the list-claims tool uses. + */ + openClaimLedger?: () => { + listClaims(filter?: { repoFullName?: string | null; status?: string | null }): unknown[]; + close(): void; + }; + /** Override the clock used for the oldest-queued age (defaults to Date.now()); injection seam for tests. */ + nowMs?: number; + /** Override the event-ledger opener (defaults to initEventLedger); injection seam for tests. */ + initEventLedger?: () => EventLedger; + /** + * Override the run-state store opener (defaults to the real on-disk store); injection seam for tests. Typed to + * the minimal read surface the run-state tool uses (never setRunState). + */ + initRunStateStore?: () => { + getRunState(repoFullName: string): unknown; + listRunStates(): unknown[]; + close(): void; + }; + /** + * Override the plan-store opener (defaults to the real on-disk store); injection seam for tests. Typed to the + * minimal read surface the plan tools use (never savePlan). + */ + openPlanStore?: () => { + loadPlan(planId: string): unknown; + listPlans(filter?: { status?: string | null }): unknown[]; + close(): void; + }; + /** + * Override the governor-ledger opener (defaults to the real on-disk ledger); injection seam for tests. Typed + * to the minimal read surface the decisions tool uses (the payload-excluding readGovernorDecisions). + */ + initGovernorLedger?: () => { + readGovernorDecisions(filter?: { repoFullName?: string | null }): unknown[]; + close(): void; + }; + /** Override the status reader (defaults to status.js's collectStatus); injection seam for tests. */ + collectStatus?: () => unknown; + /** Override the doctor-checks reader (defaults to status.js's runDoctorChecks); injection seam for tests. */ + runDoctorChecks?: () => unknown[]; + /** + * Override the prediction-ledger opener (defaults to the real on-disk ledger); injection seam for tests. Typed + * to the minimal read surface the calibration-report tool uses (never appendPrediction). + */ + initPredictionLedger?: () => { + readPredictions(filter?: { repoFullName?: string | null }): PredictionLedgerEntry[]; + close(): void; + }; +} + +/** + * Build the miner MCP server with its tools registered. `options.initPortfolioQueue`, `options.openClaimLedger`, + * `options.initEventLedger`, `options.initRunStateStore`, `options.openPlanStore`, `options.initGovernorLedger`, + * `options.collectStatus`, `options.runDoctorChecks`, and `options.nowMs` are injection seams for tests (default + * to the real stores/readers and the wall clock); the ping tool needs none. Each store-backed tool opens its + * store only when invoked and closes any store it opened. + */ +export function createMinerMcpServer(options: MinerMcpServerOptions = {}) { + const server = new McpServer({ name: "loopover-miner", version: ownPackageJson.version }); + server.registerTool( + "loopover_miner_ping", + { + description: + "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.", + inputSchema: {}, + }, + async () => ({ content: [{ type: "text", text: JSON.stringify(MINER_PING_STATUS) }] }), + ); + server.registerTool( + "loopover_miner_get_portfolio_dashboard", + { + description: + "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.", + inputSchema: {}, + }, + async () => { + const ownsQueue = options.initPortfolioQueue === undefined; + const portfolioQueue = (options.initPortfolioQueue ?? initPortfolioQueueStore)(); + try { + const summary = collectPortfolioDashboard({ portfolioQueue }, { nowMs: options.nowMs ?? Date.now() }); + return { content: [{ type: "text", text: JSON.stringify(summary) }] }; + } finally { + if (ownsQueue) portfolioQueue.close(); + } + }, + ); + server.registerTool( + "loopover_miner_get_manage_status", + { + description: + "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 -- returning the same { rows, runPortfolio } shape `manage status --json` prints. " + + "Read-only: never calls GitHub, never mutates local stores. Takes no arguments.", + inputSchema: {}, + }, + async () => { + const ownsPortfolioQueue = options.initPortfolioQueue === undefined; + const ownsEventLedger = options.initEventLedger === undefined; + const ownsRunStateStore = options.initRunStateStore === undefined; + const portfolioQueue = (options.initPortfolioQueue ?? initPortfolioQueueStore)(); + const eventLedger = (options.initEventLedger ?? initEventLedger)(); + const runStateStore = (options.initRunStateStore ?? initRunStateStore)(); + try { + // The injection seams above are typed to the minimal read surface each tool touches (mirroring the + // dashboard tool's `{ listQueue }` seam), but collectManageStatus/collectRunPortfolio's declared source + // types name the full stores. Both aggregators only ever read (listQueue/getRunState/listRunStates) at + // runtime -- exactly what the seam guarantees -- so widening the resolved stores back to the store types + // the signatures ask for is sound; it never reaches a write/lifecycle method the minimal seam omits. + const rows = collectManageStatus({ + portfolioQueue: portfolioQueue as PortfolioQueueStore, + eventLedger, + }); + const runPortfolio = collectRunPortfolio({ + portfolioQueue: portfolioQueue as PortfolioQueueStore, + eventLedger, + runStateStore: runStateStore as RunStateStore, + }); + return { content: [{ type: "text", text: JSON.stringify({ rows, runPortfolio }) }] }; + } finally { + if (ownsPortfolioQueue) portfolioQueue.close(); + if (ownsEventLedger) eventLedger.close(); + if (ownsRunStateStore) runStateStore.close(); + } + }, + ); + server.registerTool( + "loopover_miner_list_claims", + { + description: + "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.", + inputSchema: { + repoFullName: z.string().optional(), + status: z.enum(CLAIM_STATUSES).optional(), + }, + }, + async ({ repoFullName, status }) => { + const ownsLedger = options.openClaimLedger === undefined; + const ledger = (options.openClaimLedger ?? openClaimLedger)(); + try { + const filter: { repoFullName?: string; status?: string } = {}; + if (repoFullName !== undefined) filter.repoFullName = repoFullName; + if (status !== undefined) filter.status = status; + return { content: [{ type: "text", text: JSON.stringify(ledger.listClaims(filter)) }] }; + } finally { + if (ownsLedger) ledger.close(); + } + }, + ); + server.registerTool( + "loopover_miner_get_audit_feed", + { + description: + "Read-only, metadata-only audit feed from the local append-only event ledger: eventType, repoFullName, " + + "outcome, actor, detail, and createdAt per row. Wraps collectEventLedgerAuditFeed() (no new query logic) — " + + "the same read filters as `loopover-miner ledger list` (--repo, --since, --type). Never returns " + + "payload_json or other raw ledger columns; never writes to the ledger.", + inputSchema: auditFeedInputSchema, + }, + async (input) => { + const ownsLedger = options.initEventLedger === undefined; + const eventLedger = (options.initEventLedger ?? initEventLedger)(); + try { + // zod's `.optional()` widens each field to `string | undefined`, whereas normalizeAuditFeedMcpFilter's + // input type spells the same absent-field slot as `string | null`; the normalizer treats missing and + // null identically, so narrowing the parsed input to that shape is exact, not a behavior change. + const filter = normalizeAuditFeedMcpFilter((input ?? {}) as AuditFeedMcpFilterInput); + const feed = collectEventLedgerAuditFeed(eventLedger, filter); + return { content: [{ type: "text", text: JSON.stringify(feed) }] }; + } catch (error) { + return { + content: [ + { + type: "text", + text: JSON.stringify({ + error: error instanceof Error ? error.message : String(error), + }), + }, + ], + isError: true, + }; + } finally { + if (ownsLedger) eventLedger.close(); + } + }, + ); + server.registerTool( + "loopover_miner_get_run_state", + { + description: + "Read-only per-repo miner run-state (idle/discovering/planning/preparing). Pass repoFullName for a single " + + "repo (a null state means none has been recorded for it yet), or omit it to list every repo's state. The " + + "read-only analog of ORB's loopover_get_automation_state; adds no state-set or mutation capability.", + inputSchema: { + repoFullName: z.string().min(1).optional(), + }, + }, + async ({ repoFullName }) => { + const ownsStore = options.initRunStateStore === undefined; + const store = (options.initRunStateStore ?? initRunStateStore)(); + try { + const result = + repoFullName === undefined + ? { states: store.listRunStates() } + : { repoFullName, state: store.getRunState(repoFullName) }; + return { content: [{ type: "text", text: JSON.stringify(result) }] }; + } finally { + if (ownsStore) store.close(); + } + }, + ); + server.registerTool( + "loopover_miner_list_plans", + { + description: + "Read-only list of the miner's PERSISTED plan store (planId, plan DAG, status, updatedAt), optionally " + + "filtered by status. Wraps plan-store.js's existing listPlans query -- no new logic, no mutation. NOTE: " + + "this is the store-backed AMS plan store; it is distinct from ORB's stateless loopover_plan_status " + + "tool, which reads the caller's in-memory plan object rather than any persisted store.", + inputSchema: { + status: z.enum(PLAN_STATUSES).optional(), + }, + }, + async ({ status }) => { + const ownsStore = options.openPlanStore === undefined; + const store = (options.openPlanStore ?? openPlanStore)(); + try { + const filter: { status?: string } = {}; + if (status !== undefined) filter.status = status; + return { content: [{ type: "text", text: JSON.stringify(store.listPlans(filter)) }] }; + } finally { + if (ownsStore) store.close(); + } + }, + ); + server.registerTool( + "loopover_miner_get_plan", + { + description: + "Read-only fetch of one persisted plan record by planId (the full plan DAG, status, updatedAt), or an " + + "explicit { planId, found: false } for an unknown id. Wraps plan-store.js's existing loadPlan lookup -- " + + "no mutation, no DAG/planning logic. Store-backed AMS plan store; distinct from ORB's stateless " + + "loopover_plan_status tool.", + inputSchema: { + planId: z.string().min(1), + }, + }, + async ({ planId }) => { + const ownsStore = options.openPlanStore === undefined; + const store = (options.openPlanStore ?? openPlanStore)(); + try { + const plan = store.loadPlan(planId); + const result = plan === null ? { planId, found: false } : { found: true, plan }; + return { content: [{ type: "text", text: JSON.stringify(result) }] }; + } finally { + if (ownsStore) store.close(); + } + }, + ); + server.registerTool( + "loopover_miner_get_governor_decisions", + { + description: + "Read-only projection of the governor decision log: id, ts, eventType, repoFullName, actionClass, " + + "decision, reason per row. This projection INTENTIONALLY EXCLUDES the internal/sensitive payload column " + + "(reputation / self-plagiarism / budget state) by construction -- governor-ledger.js reads it with an " + + "explicit named-column SELECT, never SELECT *. Optional repoFullName filter (the only filter the ledger " + + "supports natively). Read-only; never writes to the ledger.", + inputSchema: { + repoFullName: z.string().min(1).optional(), + }, + }, + async ({ repoFullName }) => { + const ownsLedger = options.initGovernorLedger === undefined; + const ledger = (options.initGovernorLedger ?? initGovernorLedger)(); + try { + const filter: { repoFullName?: string } = {}; + if (repoFullName !== undefined) filter.repoFullName = repoFullName; + return { content: [{ type: "text", text: JSON.stringify(ledger.readGovernorDecisions(filter)) }] }; + } finally { + if (ownsLedger) ledger.close(); + } + }, + ); + server.registerTool( + "loopover_miner_status", + { + description: + "Read-only miner status + doctor diagnostics. Returns { status, doctor }: status = package/engine versions " + + "(+ skew), node version, state-dir path, config-file path, and the resolved coding-agent driver (provider " + + "name, the model ENV-VAR NAME -- never its value -- and a CLI-present boolean); doctor = the same checks " + + "`loopover-miner doctor` runs (Docker/CLI presence, config validity, ...) as { name, ok, detail }. Reuses " + + "collectStatus/runDoctorChecks so it can never drift from the CLI. Only names / booleans / paths -- never " + + "any env-var value, token, key, or credential. Read-only; no writes or state changes.", + inputSchema: {}, + }, + async () => { + const status = (options.collectStatus ?? collectStatus)(); + const doctor = (options.runDoctorChecks ?? runDoctorChecks)(); + return { content: [{ type: "text", text: JSON.stringify({ status, doctor }) }] }; + }, + ); + server.registerTool( + "loopover_miner_get_calibration_report", + { + description: + "Read-only miner-local prediction-accuracy report: per-project merge/close precision, joining this " + + "miner's own recorded gate predictions (prediction ledger) with the realized PR outcomes it later " + + "observed (pr_outcome events). Wraps calibration-cli.js's existing toPredictionRecords/toOutcomeRecords " + + "mappers and calibration.js's buildCalibrationReport composer -- no new join/scoring logic, no mutation. " + + "Strictly local and offline; distinct from ORB's hosted, maintainer-authenticated " + + "loopover_get_outcome_calibration tool, which reads a different (D1) data source. Takes no arguments.", + inputSchema: {}, + }, + async () => { + const ownsPredictionLedger = options.initPredictionLedger === undefined; + const ownsEventLedger = options.initEventLedger === undefined; + let predictionLedger; + let eventLedger; + try { + predictionLedger = (options.initPredictionLedger ?? initPredictionLedger)(); + eventLedger = (options.initEventLedger ?? initEventLedger)(); + const report = buildCalibrationReport( + toPredictionRecords(predictionLedger.readPredictions()), + toOutcomeRecords(eventLedger.readEvents()), + ); + return { content: [{ type: "text", text: JSON.stringify(report) }] }; + } finally { + if (ownsPredictionLedger) predictionLedger?.close(); + if (ownsEventLedger) eventLedger?.close(); + } + }, + ); + return server; +} + +// Start the stdio transport only when executed directly as the bin, not when imported by a test. +// realpathSync on both sides resolves the npm bin symlink so a global/npx install still matches. +const invokedPath = process.argv[1] ? realpathSync(process.argv[1]) : ""; +/* v8 ignore start -- process entry point: this guard is specifically what makes it unreachable when the + * module is imported (every existing MCP tool test does exactly that, per this file's own comment above), so + * it can never be true in this test run's own process. createMinerMcpServer() itself is fully exercised by + * those tests; this is only the top-level "am I actually invoked as the bin" wiring, mirroring + * loopover-miner.js's identical exemption and src/server.ts's in codecov.yml. */ +if (invokedPath && invokedPath === realpathSync(fileURLToPath(import.meta.url))) { + // Previously this bin had NO crash safety net beyond the startup .catch() below -- an exception thrown while + // handling an MCP tool call, after the server was already connected, had nowhere to go (#6011). Wire in the + // same opt-in Sentry + signal/crash handling loopover-miner.js already gets, sharing process-lifecycle.js. + try { + loadMinerFileSecrets(); + } catch (error) { + console.error(error instanceof Error ? error.message : String(error)); + process.exit(1); + } + await initMinerSentry(process.env); + installCliSignalHandlers({ captureError: captureMinerErrorAndFlush }); + + createMinerMcpServer() + .connect(new StdioServerTransport()) + .catch(async (error) => { + console.error(error); + // Awaited so the captured event has a chance to actually reach Sentry before exit() tears the process + // down -- a bare synchronous capture only queues it (#6011 follow-up). + await captureMinerErrorAndFlush(error, { kind: "mcp_startup_connect_failed" }); + process.exit(1); + }); +} +/* v8 ignore stop */ diff --git a/packages/loopover-miner/bin/loopover-miner.d.ts b/packages/loopover-miner/bin/loopover-miner.d.ts new file mode 100644 index 0000000000..b7988016da --- /dev/null +++ b/packages/loopover-miner/bin/loopover-miner.d.ts @@ -0,0 +1,2 @@ +#!/usr/bin/env node +export {}; diff --git a/packages/loopover-miner/bin/loopover-miner.js b/packages/loopover-miner/bin/loopover-miner.js index ddf22f34c0..bf2fa78c52 100755 --- a/packages/loopover-miner/bin/loopover-miner.js +++ b/packages/loopover-miner/bin/loopover-miner.js @@ -27,13 +27,8 @@ import { createWizardIo, runInteractiveInit } from "../lib/init-wizard.js"; import { loadMinerFileSecrets } from "../lib/env-file-indirection.js"; import { runMigrate } from "../lib/migrate-cli.js"; import { runDoctor, runStatus } from "../lib/status.js"; -import { - awaitOpportunisticUpdateCheck, - resolveUpgradeCommand, - startUpdateCheck, -} from "../lib/update-check.js"; +import { awaitOpportunisticUpdateCheck, resolveUpgradeCommand, startUpdateCheck, } from "../lib/update-check.js"; import { resolveMinerVersion } from "../lib/version.js"; - // Resolve any `_FILE` secret-mount vars (GITHUB_TOKEN_FILE, etc.) into their plain counterparts FIRST, // before anything else reads process.env -- every subcommand below (and the coding-agent driver construction // deeper in the call graph) reads plain env vars, so this single early pass is all that's needed for the whole @@ -42,12 +37,12 @@ import { resolveMinerVersion } from "../lib/version.js"; // Exits 2, not 1: docs/unattended-scheduling.md's contract only defines 0 (success) and 2 (failure -- "Alert // on this"), so an operator alerting strictly on 2 would otherwise miss a broken secret mount entirely. try { - loadMinerFileSecrets(); -} catch (error) { - console.error(error instanceof Error ? error.message : String(error)); - process.exit(2); + loadMinerFileSecrets(); +} +catch (error) { + console.error(error instanceof Error ? error.message : String(error)); + process.exit(2); } - // Opt-in Sentry (#6011): a complete no-op unless the operator sets LOOPOVER_MINER_SENTRY_DSN themselves. Must // run AFTER loadMinerFileSecrets (so a `_FILE`-mounted DSN resolves first) and BEFORE installCliSignalHandlers // (so a startup crash is still captured). @@ -58,176 +53,144 @@ try { * (miner-sentry.test.ts, miner-process-lifecycle.test.ts) -- this is only the top-level wiring that calls * them, mirroring src/server.ts's identical, already-established exemption in codecov.yml. */ await initMinerSentry(process.env); - // Register signal + crash handlers once, before any command runs, so an interrupted run closes its open ledgers // cleanly instead of dying mid-write (#4826). Covers every subcommand below, including the local ones. installCliSignalHandlers({ captureError: captureMinerErrorAndFlush }); /* v8 ignore stop */ - // Peel the global logging flags (--quiet/--verbose/--log-level) off the front of argv and configure the // process-wide logger once (#4835), so every command below shares one level-aware logger without re-parsing // them; the stripped `cliArgs` is what the command dispatch sees. const { options: logOptions, rest: cliArgs } = extractLogOptions(process.argv.slice(2)); configureLogger({ ...logOptions, env: process.env }); - // `status` and `doctor` are strictly local, offline commands — their contract is to make NO network calls. // `init` stays local by default and only makes a network call when the operator explicitly passes // `--verify-token`. // Dispatch the local commands BEFORE the opportunistic npm-registry update check is even started, so they can // never reach that network path (the update check runs for the remaining commands below). if (cliArgs[0] === "init") { - if (cliArgs.includes("--interactive")) { - const wizardIo = createWizardIo(); - try { - process.exit(await runInteractiveInit(process.env, process.cwd(), wizardIo)); - } finally { - wizardIo.close(); + if (cliArgs.includes("--interactive")) { + const wizardIo = createWizardIo(); + try { + process.exit(await runInteractiveInit(process.env, process.cwd(), wizardIo)); + } + finally { + wizardIo.close(); + } } - } - process.exit(await runInit(cliArgs.slice(1))); + process.exit(await runInit(cliArgs.slice(1))); } - if (cliArgs[0] === "status") { - process.exit(runStatus(cliArgs.slice(1))); + process.exit(runStatus(cliArgs.slice(1))); } - if (cliArgs[0] === "doctor") { - process.exit(runDoctor(cliArgs.slice(1))); + process.exit(runDoctor(cliArgs.slice(1))); } - // `migrate` is strictly local + offline like `status`/`doctor` (it only opens the local SQLite stores), so it is // dispatched here too, before the opportunistic npm-registry update check is ever started. if (cliArgs[0] === "migrate") { - process.exit(runMigrate(cliArgs.slice(1))); + process.exit(runMigrate(cliArgs.slice(1))); } - // `metrics` is strictly local + offline like `status`/`doctor` (it reads only the local prediction ledger), so it // is dispatched here, before the opportunistic npm-registry update check is ever started. if (cliArgs[0] === "metrics") { - process.exit(runMetrics(cliArgs.slice(1))); + process.exit(runMetrics(cliArgs.slice(1))); } - if (cliArgs[0] === "manage" && cliArgs[1] === "status") { - process.exit(runManageStatus(cliArgs.slice(2))); + process.exit(runManageStatus(cliArgs.slice(2))); } - if (cliArgs[0] === "queue") { - process.exit(runQueueCli(cliArgs[1], cliArgs.slice(2))); + process.exit(runQueueCli(cliArgs[1], cliArgs.slice(2))); } - if (cliArgs[0] === "orb" && cliArgs[1] === "export") { - process.exit(await runOrbExportCli(cliArgs.slice(2))); + process.exit(await runOrbExportCli(cliArgs.slice(2))); } - // `tenant` (#7275) talks to the hosting control-plane's provisioning API — a deliberate, Bearer-authed admin // action that is inert unless LOOPOVER_MINER_CONTROL_PLANE is set. Grouped with `orb export` above (also a // network command) rather than the strictly-local commands; it fails loud on any control-plane error. if (cliArgs[0] === "tenant") { - process.exit(await runTenantCli(cliArgs[1], cliArgs.slice(2))); + process.exit(await runTenantCli(cliArgs[1], cliArgs.slice(2))); } - if (cliArgs[0] === "claim") { - process.exit(runClaimCli(cliArgs[1], cliArgs.slice(2))); + process.exit(runClaimCli(cliArgs[1], cliArgs.slice(2))); } - if (cliArgs[0] === "ledger") { - process.exit(runLedgerCli(cliArgs[1], cliArgs.slice(2))); + process.exit(runLedgerCli(cliArgs[1], cliArgs.slice(2))); } - if (cliArgs[0] === "calibration") { - process.exit(runCalibrationCli(cliArgs.slice(1))); + process.exit(runCalibrationCli(cliArgs.slice(1))); } - if (cliArgs[0] === "plan") { - process.exit(runPlanCli(cliArgs[1], cliArgs.slice(2))); + process.exit(runPlanCli(cliArgs[1], cliArgs.slice(2))); } - if (cliArgs[0] === "governor") { - process.exit(await runGovernorCli(cliArgs[1], cliArgs.slice(2))); + process.exit(await runGovernorCli(cliArgs[1], cliArgs.slice(2))); } - if (cliArgs[0] === "feasibility") { - process.exit(runFeasibilityCli(cliArgs.slice(1))); + process.exit(runFeasibilityCli(cliArgs.slice(1))); } - if (cliArgs[0] === "idea-feasibility") { - process.exit(runIdeaFeasibilityCli(cliArgs.slice(1))); + process.exit(runIdeaFeasibilityCli(cliArgs.slice(1))); } - // `purge` (#5564) is strictly local + offline like `queue`/`claim`/`governor` above -- it only opens the local // SQLite stores, so it is dispatched here too, before the opportunistic npm-registry update check ever starts. if (cliArgs[0] === "purge") { - process.exit(runPurge(cliArgs.slice(1))); + process.exit(runPurge(cliArgs.slice(1))); } - const packageName = "@loopover/miner"; const packageVersion = resolveMinerVersion(process.env); const upgradeCommand = resolveUpgradeCommand(packageName); - const updateCheck = startUpdateCheck(cliArgs, { - packageName, - packageVersion, - upgradeCommand, - env: process.env, + packageName, + packageVersion, + upgradeCommand, + env: process.env, }); - -if ( - cliArgs.length === 0 || - cliArgs.includes("--help") || - cliArgs.includes("-h") || - cliArgs[0] === "help" -) { - printHelp({ packageName }); - await awaitOpportunisticUpdateCheck(updateCheck); - process.exit(0); -} - -if ( - cliArgs.includes("--version") || - cliArgs.includes("-v") || - cliArgs[0] === "version" -) { - printVersion({ packageName, packageVersion }); - await awaitOpportunisticUpdateCheck(updateCheck); - process.exit(0); -} - +if (cliArgs.length === 0 || + cliArgs.includes("--help") || + cliArgs.includes("-h") || + cliArgs[0] === "help") { + printHelp({ packageName }); + await awaitOpportunisticUpdateCheck(updateCheck); + process.exit(0); +} +if (cliArgs.includes("--version") || + cliArgs.includes("-v") || + cliArgs[0] === "version") { + printVersion({ packageName, packageVersion }); + await awaitOpportunisticUpdateCheck(updateCheck); + process.exit(0); +} if (cliArgs[0] === "hooks" && cliArgs[1] === "check") { - const exitCode = runDenyCheck(cliArgs.slice(2)); - await awaitOpportunisticUpdateCheck(updateCheck); - process.exit(exitCode); + const exitCode = runDenyCheck(cliArgs.slice(2)); + await awaitOpportunisticUpdateCheck(updateCheck); + process.exit(exitCode); } - if (cliArgs[0] === "state") { - const exitCode = runStateCli(cliArgs[1], cliArgs.slice(2)); - await awaitOpportunisticUpdateCheck(updateCheck); - process.exit(exitCode); + const exitCode = runStateCli(cliArgs[1], cliArgs.slice(2)); + await awaitOpportunisticUpdateCheck(updateCheck); + process.exit(exitCode); } - if (cliArgs[0] === "manage" && cliArgs[1] === "poll") { - const exitCode = await runManagePoll(cliArgs.slice(2)); - await awaitOpportunisticUpdateCheck(updateCheck); - process.exit(exitCode); + const exitCode = await runManagePoll(cliArgs.slice(2)); + await awaitOpportunisticUpdateCheck(updateCheck); + process.exit(exitCode); } - if (cliArgs[0] === "discover") { - const exitCode = await runDiscover(cliArgs.slice(1)); - await awaitOpportunisticUpdateCheck(updateCheck); - process.exit(exitCode); + const exitCode = await runDiscover(cliArgs.slice(1)); + await awaitOpportunisticUpdateCheck(updateCheck); + process.exit(exitCode); } - if (cliArgs[0] === "attempt") { - const exitCode = await runAttempt(cliArgs.slice(1)); - await awaitOpportunisticUpdateCheck(updateCheck); - process.exit(exitCode); + const exitCode = await runAttempt(cliArgs.slice(1)); + await awaitOpportunisticUpdateCheck(updateCheck); + process.exit(exitCode); } - if (cliArgs[0] === "loop") { - const exitCode = await runLoop(cliArgs.slice(1)); - await awaitOpportunisticUpdateCheck(updateCheck); - process.exit(exitCode); + const exitCode = await runLoop(cliArgs.slice(1)); + await awaitOpportunisticUpdateCheck(updateCheck); + process.exit(exitCode); } - const exitCode = runCli(cliArgs, { packageName }); await awaitOpportunisticUpdateCheck(updateCheck); process.exit(exitCode); +//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoibG9vcG92ZXItbWluZXIuanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyJsb29wb3Zlci1taW5lci50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiO0FBQ0EsT0FBTyxFQUFFLFVBQVUsRUFBRSxNQUFNLHVCQUF1QixDQUFDO0FBQ25ELE9BQU8sRUFBRSxTQUFTLEVBQUUsWUFBWSxFQUFFLE1BQU0sRUFBRSxNQUFNLGVBQWUsQ0FBQztBQUNoRSxPQUFPLEVBQUUsZUFBZSxFQUFFLGlCQUFpQixFQUFFLE1BQU0sa0JBQWtCLENBQUM7QUFDdEUsT0FBTyxFQUFFLFlBQVksRUFBRSxNQUFNLHNCQUFzQixDQUFDO0FBQ3BELE9BQU8sRUFBRSxXQUFXLEVBQUUsTUFBTSx3QkFBd0IsQ0FBQztBQUNyRCxPQUFPLEVBQUUsaUJBQWlCLEVBQUUsTUFBTSwyQkFBMkIsQ0FBQztBQUM5RCxPQUFPLEVBQUUscUJBQXFCLEVBQUUsTUFBTSxnQ0FBZ0MsQ0FBQztBQUN2RSxPQUFPLEVBQUUsY0FBYyxFQUFFLE1BQU0sK0JBQStCLENBQUM7QUFDL0QsT0FBTyxFQUFFLFlBQVksRUFBRSxNQUFNLDRCQUE0QixDQUFDO0FBQzFELE9BQU8sRUFBRSxpQkFBaUIsRUFBRSxNQUFNLDJCQUEyQixDQUFDO0FBQzlELE9BQU8sRUFBRSxPQUFPLEVBQUUsTUFBTSxvQkFBb0IsQ0FBQztBQUM3QyxPQUFPLEVBQUUsYUFBYSxFQUFFLE1BQU0sdUJBQXVCLENBQUM7QUFDdEQsT0FBTyxFQUFFLGVBQWUsRUFBRSxNQUFNLHlCQUF5QixDQUFDO0FBQzFELE9BQU8sRUFBRSxVQUFVLEVBQUUsTUFBTSx1QkFBdUIsQ0FBQztBQUNuRCxPQUFPLEVBQUUsVUFBVSxFQUFFLE1BQU0sMEJBQTBCLENBQUM7QUFDdEQsT0FBTyxFQUFFLFdBQVcsRUFBRSxNQUFNLDRCQUE0QixDQUFDO0FBQ3pELE9BQU8sRUFBRSxRQUFRLEVBQUUsTUFBTSxxQkFBcUIsQ0FBQztBQUMvQyxPQUFPLEVBQUUsV0FBVyxF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 \ No newline at end of file diff --git a/packages/loopover-miner/bin/loopover-miner.ts b/packages/loopover-miner/bin/loopover-miner.ts new file mode 100755 index 0000000000..ddf22f34c0 --- /dev/null +++ b/packages/loopover-miner/bin/loopover-miner.ts @@ -0,0 +1,233 @@ +#!/usr/bin/env node +import { runAttempt } from "../lib/attempt-cli.js"; +import { printHelp, printVersion, runCli } from "../lib/cli.js"; +import { configureLogger, extractLogOptions } from "../lib/logger.js"; +import { runDenyCheck } from "../lib/deny-check.js"; +import { runDiscover } from "../lib/discover-cli.js"; +import { runFeasibilityCli } from "../lib/feasibility-cli.js"; +import { runIdeaFeasibilityCli } from "../lib/idea-feasibility-cli.js"; +import { runGovernorCli } from "../lib/governor-ledger-cli.js"; +import { runLedgerCli } from "../lib/event-ledger-cli.js"; +import { runCalibrationCli } from "../lib/calibration-cli.js"; +import { runLoop } from "../lib/loop-cli.js"; +import { runManagePoll } from "../lib/manage-poll.js"; +import { runManageStatus } from "../lib/manage-status.js"; +import { runMetrics } from "../lib/metrics-cli.js"; +import { runPlanCli } from "../lib/plan-store-cli.js"; +import { runClaimCli } from "../lib/claim-ledger-cli.js"; +import { runPurge } from "../lib/purge-cli.js"; +import { runQueueCli } from "../lib/portfolio-queue-cli.js"; +import { runOrbExportCli } from "../lib/orb-export.js"; +import { runTenantCli } from "../lib/tenant-cli.js"; +import { installCliSignalHandlers } from "../lib/process-lifecycle.js"; +import { captureMinerErrorAndFlush, initMinerSentry } from "../lib/sentry.js"; +import { runStateCli } from "../lib/run-state-cli.js"; +import { runInit } from "../lib/laptop-init.js"; +import { createWizardIo, runInteractiveInit } from "../lib/init-wizard.js"; +import { loadMinerFileSecrets } from "../lib/env-file-indirection.js"; +import { runMigrate } from "../lib/migrate-cli.js"; +import { runDoctor, runStatus } from "../lib/status.js"; +import { + awaitOpportunisticUpdateCheck, + resolveUpgradeCommand, + startUpdateCheck, +} from "../lib/update-check.js"; +import { resolveMinerVersion } from "../lib/version.js"; + +// Resolve any `_FILE` secret-mount vars (GITHUB_TOKEN_FILE, etc.) into their plain counterparts FIRST, +// before anything else reads process.env -- every subcommand below (and the coding-agent driver construction +// deeper in the call graph) reads plain env vars, so this single early pass is all that's needed for the whole +// CLI (#5178). A broken secret mount fails the process fast and loud with a clear message, instead of an +// uncaught-exception stack trace or a silent empty credential surfacing as a confusing GitHub 401 later. +// Exits 2, not 1: docs/unattended-scheduling.md's contract only defines 0 (success) and 2 (failure -- "Alert +// on this"), so an operator alerting strictly on 2 would otherwise miss a broken secret mount entirely. +try { + loadMinerFileSecrets(); +} catch (error) { + console.error(error instanceof Error ? error.message : String(error)); + process.exit(2); +} + +// Opt-in Sentry (#6011): a complete no-op unless the operator sets LOOPOVER_MINER_SENTRY_DSN themselves. Must +// run AFTER loadMinerFileSecrets (so a `_FILE`-mounted DSN resolves first) and BEFORE installCliSignalHandlers +// (so a startup crash is still captured). +/* v8 ignore start -- process entry point (this bin's top level runs unconditionally, every invocation); + * exercised by the real --help/--version subprocess spawn in miner-package-skeleton.test.ts (a different + * Node process, invisible to this test run's own coverage instrumentation), not unit-coverable here. The + * functions themselves (initMinerSentry, installCliSignalHandlers) are fully unit-tested in isolation + * (miner-sentry.test.ts, miner-process-lifecycle.test.ts) -- this is only the top-level wiring that calls + * them, mirroring src/server.ts's identical, already-established exemption in codecov.yml. */ +await initMinerSentry(process.env); + +// Register signal + crash handlers once, before any command runs, so an interrupted run closes its open ledgers +// cleanly instead of dying mid-write (#4826). Covers every subcommand below, including the local ones. +installCliSignalHandlers({ captureError: captureMinerErrorAndFlush }); +/* v8 ignore stop */ + +// Peel the global logging flags (--quiet/--verbose/--log-level) off the front of argv and configure the +// process-wide logger once (#4835), so every command below shares one level-aware logger without re-parsing +// them; the stripped `cliArgs` is what the command dispatch sees. +const { options: logOptions, rest: cliArgs } = extractLogOptions(process.argv.slice(2)); +configureLogger({ ...logOptions, env: process.env }); + +// `status` and `doctor` are strictly local, offline commands — their contract is to make NO network calls. +// `init` stays local by default and only makes a network call when the operator explicitly passes +// `--verify-token`. +// Dispatch the local commands BEFORE the opportunistic npm-registry update check is even started, so they can +// never reach that network path (the update check runs for the remaining commands below). +if (cliArgs[0] === "init") { + if (cliArgs.includes("--interactive")) { + const wizardIo = createWizardIo(); + try { + process.exit(await runInteractiveInit(process.env, process.cwd(), wizardIo)); + } finally { + wizardIo.close(); + } + } + process.exit(await runInit(cliArgs.slice(1))); +} + +if (cliArgs[0] === "status") { + process.exit(runStatus(cliArgs.slice(1))); +} + +if (cliArgs[0] === "doctor") { + process.exit(runDoctor(cliArgs.slice(1))); +} + +// `migrate` is strictly local + offline like `status`/`doctor` (it only opens the local SQLite stores), so it is +// dispatched here too, before the opportunistic npm-registry update check is ever started. +if (cliArgs[0] === "migrate") { + process.exit(runMigrate(cliArgs.slice(1))); +} + +// `metrics` is strictly local + offline like `status`/`doctor` (it reads only the local prediction ledger), so it +// is dispatched here, before the opportunistic npm-registry update check is ever started. +if (cliArgs[0] === "metrics") { + process.exit(runMetrics(cliArgs.slice(1))); +} + +if (cliArgs[0] === "manage" && cliArgs[1] === "status") { + process.exit(runManageStatus(cliArgs.slice(2))); +} + +if (cliArgs[0] === "queue") { + process.exit(runQueueCli(cliArgs[1], cliArgs.slice(2))); +} + +if (cliArgs[0] === "orb" && cliArgs[1] === "export") { + process.exit(await runOrbExportCli(cliArgs.slice(2))); +} + +// `tenant` (#7275) talks to the hosting control-plane's provisioning API — a deliberate, Bearer-authed admin +// action that is inert unless LOOPOVER_MINER_CONTROL_PLANE is set. Grouped with `orb export` above (also a +// network command) rather than the strictly-local commands; it fails loud on any control-plane error. +if (cliArgs[0] === "tenant") { + process.exit(await runTenantCli(cliArgs[1], cliArgs.slice(2))); +} + +if (cliArgs[0] === "claim") { + process.exit(runClaimCli(cliArgs[1], cliArgs.slice(2))); +} + +if (cliArgs[0] === "ledger") { + process.exit(runLedgerCli(cliArgs[1], cliArgs.slice(2))); +} + +if (cliArgs[0] === "calibration") { + process.exit(runCalibrationCli(cliArgs.slice(1))); +} + +if (cliArgs[0] === "plan") { + process.exit(runPlanCli(cliArgs[1], cliArgs.slice(2))); +} + +if (cliArgs[0] === "governor") { + process.exit(await runGovernorCli(cliArgs[1], cliArgs.slice(2))); +} + +if (cliArgs[0] === "feasibility") { + process.exit(runFeasibilityCli(cliArgs.slice(1))); +} + +if (cliArgs[0] === "idea-feasibility") { + process.exit(runIdeaFeasibilityCli(cliArgs.slice(1))); +} + +// `purge` (#5564) is strictly local + offline like `queue`/`claim`/`governor` above -- it only opens the local +// SQLite stores, so it is dispatched here too, before the opportunistic npm-registry update check ever starts. +if (cliArgs[0] === "purge") { + process.exit(runPurge(cliArgs.slice(1))); +} + +const packageName = "@loopover/miner"; +const packageVersion = resolveMinerVersion(process.env); +const upgradeCommand = resolveUpgradeCommand(packageName); + +const updateCheck = startUpdateCheck(cliArgs, { + packageName, + packageVersion, + upgradeCommand, + env: process.env, +}); + +if ( + cliArgs.length === 0 || + cliArgs.includes("--help") || + cliArgs.includes("-h") || + cliArgs[0] === "help" +) { + printHelp({ packageName }); + await awaitOpportunisticUpdateCheck(updateCheck); + process.exit(0); +} + +if ( + cliArgs.includes("--version") || + cliArgs.includes("-v") || + cliArgs[0] === "version" +) { + printVersion({ packageName, packageVersion }); + await awaitOpportunisticUpdateCheck(updateCheck); + process.exit(0); +} + +if (cliArgs[0] === "hooks" && cliArgs[1] === "check") { + const exitCode = runDenyCheck(cliArgs.slice(2)); + await awaitOpportunisticUpdateCheck(updateCheck); + process.exit(exitCode); +} + +if (cliArgs[0] === "state") { + const exitCode = runStateCli(cliArgs[1], cliArgs.slice(2)); + await awaitOpportunisticUpdateCheck(updateCheck); + process.exit(exitCode); +} + +if (cliArgs[0] === "manage" && cliArgs[1] === "poll") { + const exitCode = await runManagePoll(cliArgs.slice(2)); + await awaitOpportunisticUpdateCheck(updateCheck); + process.exit(exitCode); +} + +if (cliArgs[0] === "discover") { + const exitCode = await runDiscover(cliArgs.slice(1)); + await awaitOpportunisticUpdateCheck(updateCheck); + process.exit(exitCode); +} + +if (cliArgs[0] === "attempt") { + const exitCode = await runAttempt(cliArgs.slice(1)); + await awaitOpportunisticUpdateCheck(updateCheck); + process.exit(exitCode); +} + +if (cliArgs[0] === "loop") { + const exitCode = await runLoop(cliArgs.slice(1)); + await awaitOpportunisticUpdateCheck(updateCheck); + process.exit(exitCode); +} + +const exitCode = runCli(cliArgs, { packageName }); +await awaitOpportunisticUpdateCheck(updateCheck); +process.exit(exitCode); diff --git a/scripts/check-miner-package.mjs b/scripts/check-miner-package.mjs index fcd1ee42f2..82e6e92e6c 100644 --- a/scripts/check-miner-package.mjs +++ b/scripts/check-miner-package.mjs @@ -6,7 +6,7 @@ import { fileURLToPath } from "node:url"; import { FORBIDDEN_CONTENT } from "./forbidden-content.mjs"; const ALLOWED = [ - /^bin\/loopover-miner\.js$/, + /^bin\/loopover-miner\.(js|d\.ts)$/, /^bin\/loopover-miner-mcp\.(js|d\.ts)$/, /^lib\/[a-z0-9-]+\.(js|d\.ts)$/, /^package\.json$/,