From 7fd46ba1c125f6a888061815ddda724d12cbed6d Mon Sep 17 00:00:00 2001 From: andriypolandki Date: Sun, 12 Jul 2026 06:40:29 -0700 Subject: [PATCH 1/4] feat(miner-mcp): expose status/doctor diagnostics tool (#5154) --- packages/gittensory-miner/README.md | 3 +- .../bin/gittensory-miner-mcp.d.ts | 10 +- .../bin/gittensory-miner-mcp.js | 24 ++- packages/gittensory-miner/lib/status.d.ts | 12 ++ packages/gittensory-miner/lib/status.js | 47 ++++++ test/unit/miner-mcp-scaffold.test.ts | 1 + test/unit/miner-mcp-status.test.ts | 140 ++++++++++++++++++ test/unit/miner-status.test.ts | 30 ++++ 8 files changed, 264 insertions(+), 3 deletions(-) create mode 100644 test/unit/miner-mcp-status.test.ts diff --git a/packages/gittensory-miner/README.md b/packages/gittensory-miner/README.md index 52292cff4..be670d443 100644 --- a/packages/gittensory-miner/README.md +++ b/packages/gittensory-miner/README.md @@ -138,8 +138,9 @@ It exposes these read-only tools: - `gittensory_miner_ping` (#5153) — a health check returning a static `{ "status": "ok", "tool": "gittensory_miner_ping" }` object. Reads no AMS state, takes no arguments. - `gittensory_miner_get_portfolio_dashboard` (#5155) — the per-repo portfolio-queue backlog dashboard: status counts (queued / in_progress / done), totals, and the oldest-queued age. Wraps `collectPortfolioDashboard()` (no new logic) — the same data `gittensory-miner queue dashboard --json` prints locally. Read-only, takes no arguments. +- `gittensory_miner_status` (#5154) — read-only status/doctor diagnostics: state-dir path, engine-version skew, Docker/Claude/Codex CLI presence booleans, config validity, and the full doctor check list. Wraps `collectMinerDiagnostics()` (no new logic) — the same fields `gittensory-miner status --json` and `doctor --json` expose locally. Returns env-var names and booleans only, never secret values. Read-only, takes no arguments. -Further AMS-state-reading tools (status/doctor diagnostics, claim-ledger listing, run-state, event/governor ledgers) land as follow-up PRs on top of this server. +Further AMS-state-reading tools (claim-ledger listing, run-state, event/governor ledgers) land as follow-up PRs on top of this server. The resolved coding-agent driver section (`MINER_CODING_AGENT_PROVIDER`, model-env-var name, provider CLI presence) lands in #5164 and will be included here once that ships. ## Version check diff --git a/packages/gittensory-miner/bin/gittensory-miner-mcp.d.ts b/packages/gittensory-miner/bin/gittensory-miner-mcp.d.ts index 576a6affb..7734cbe88 100644 --- a/packages/gittensory-miner/bin/gittensory-miner-mcp.d.ts +++ b/packages/gittensory-miner/bin/gittensory-miner-mcp.d.ts @@ -1,4 +1,5 @@ import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; +import type { MinerDiagnostics } from "../lib/status.js"; /** The static, non-secret payload the gittensory_miner_ping tool always returns, independent of input. */ export const MINER_PING_STATUS: { status: "ok"; tool: "gittensory_miner_ping" }; @@ -11,10 +12,17 @@ export interface MinerMcpServerOptions { initPortfolioQueue?: () => { listQueue(repoFullName?: 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 status/doctor snapshot builder (defaults to collectMinerDiagnostics); injection seam for tests. */ + collectMinerDiagnostics?: (env?: Record, cwd?: string) => MinerDiagnostics; + /** Env passed to collectMinerDiagnostics when the real builder runs (defaults to process.env). */ + diagnosticsEnv?: Record; + /** cwd passed to collectMinerDiagnostics when the real builder runs (defaults to process.cwd()). */ + diagnosticsCwd?: string; } /** - * Build the miner MCP server with its tools registered (gittensory_miner_ping, gittensory_miner_get_portfolio_dashboard). + * Build the miner MCP server with its tools registered (gittensory_miner_ping, + * gittensory_miner_get_portfolio_dashboard, gittensory_miner_status). * `options` supplies test injection seams; production callers pass nothing. */ export function createMinerMcpServer(options?: MinerMcpServerOptions): McpServer; diff --git a/packages/gittensory-miner/bin/gittensory-miner-mcp.js b/packages/gittensory-miner/bin/gittensory-miner-mcp.js index 944d3232f..41d9fa67a 100755 --- a/packages/gittensory-miner/bin/gittensory-miner-mcp.js +++ b/packages/gittensory-miner/bin/gittensory-miner-mcp.js @@ -5,13 +5,16 @@ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; import { collectPortfolioDashboard } from "../lib/portfolio-dashboard.js"; import { initPortfolioQueueStore } from "../lib/portfolio-queue.js"; +import { collectMinerDiagnostics } from "../lib/status.js"; // MCP stdio server for @jsonbored/gittensory-miner (scaffold #5153). Mirrors the packages/gittensory-mcp // harness (MCP SDK server + stdio transport). Tools: // - gittensory_miner_ping (#5153): trivial static health check, reads no AMS state. // - gittensory_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`). -// Remaining AMS-state-reading tools (status/doctor, claim-ledger listing, run-state, etc.) land as follow-ups. +// - gittensory_miner_status (#5154): read-only status/doctor diagnostics via collectMinerDiagnostics() +// (same fields as `status`/`doctor --json`; no secret values). Driver-info fields follow #5164. +// Remaining AMS-state-reading tools (claim-ledger listing, run-state, etc.) land as follow-ups. // 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. @@ -24,6 +27,7 @@ export const MINER_PING_STATUS = { status: "ok", tool: "gittensory_miner_ping" } * Build the miner MCP server with its tools registered. `options.initPortfolioQueue` / `options.nowMs` are * injection seams for tests (default to the real portfolio-queue store and the wall clock); the ping tool needs * neither. The portfolio-dashboard tool opens the queue only when invoked and closes any store it opened. + * `options.collectMinerDiagnostics` / `options.diagnosticsEnv` / `options.diagnosticsCwd` inject the status tool. */ export function createMinerMcpServer(options = {}) { const server = new McpServer({ name: "gittensory-miner", version: ownPackageJson.version }); @@ -57,6 +61,24 @@ export function createMinerMcpServer(options = {}) { } }, ); + server.registerTool( + "gittensory_miner_status", + { + description: + "Read-only miner status/doctor diagnostics: state-dir path, engine-version skew, Docker/Claude/Codex CLI " + + "presence booleans, config validity, and the full doctor check list. Wraps collectMinerDiagnostics() " + + "(no new logic) — the same data `gittensory-miner status --json` and `doctor --json` expose locally. " + + "Returns env-var names and booleans only, never secret values. Takes no arguments; mutates nothing.", + inputSchema: {}, + }, + async () => { + const diagnostics = (options.collectMinerDiagnostics ?? collectMinerDiagnostics)( + options.diagnosticsEnv ?? process.env, + options.diagnosticsCwd ?? process.cwd(), + ); + return { content: [{ type: "text", text: JSON.stringify(diagnostics) }] }; + }, + ); return server; } diff --git a/packages/gittensory-miner/lib/status.d.ts b/packages/gittensory-miner/lib/status.d.ts index 0cd0eb71b..1230b8813 100644 --- a/packages/gittensory-miner/lib/status.d.ts +++ b/packages/gittensory-miner/lib/status.d.ts @@ -16,6 +16,18 @@ export function resolveMinerStateDir(env?: Record): export function collectStatus(env?: Record, cwd?: string): MinerStatus; +export type MinerDiagnostics = MinerStatus & { + configValid: boolean; + engineVersionSkew: { ok: boolean; detail: string }; + presence: { docker: boolean; claudeCli: boolean; codexCli: boolean }; + doctor: { ok: boolean; checks: DoctorCheck[] }; +}; + +export function collectMinerDiagnostics( + env?: Record, + cwd?: string, +): MinerDiagnostics; + export function runStatus(args?: string[], env?: Record, cwd?: string): number; export function runDoctorChecks(env?: Record): DoctorCheck[]; diff --git a/packages/gittensory-miner/lib/status.js b/packages/gittensory-miner/lib/status.js index 159f08c68..49331c140 100644 --- a/packages/gittensory-miner/lib/status.js +++ b/packages/gittensory-miner/lib/status.js @@ -205,6 +205,53 @@ export function collectStatus(env = process.env, cwd = process.cwd()) { }; } +function findDoctorCheck(checks, name) { + return checks.find((check) => check.name === name); +} + +/** True when a docker/cli presence check found its binary on PATH (independent of doctor pass/fail). */ +function binaryPresentFromDoctorCheck(check) { + return typeof check?.detail === "string" && check.detail.startsWith("found at"); +} + +/** Whether an optional on-disk config file exists and is readable. Full schema validation lands in #4873. */ +function assessConfigValidity(configPath) { + if (!configPath) return true; + try { + readFileSync(configPath, "utf8"); + return true; + } catch { + return false; + } +} + +/** Read-only diagnostics snapshot shared by the CLI and the MCP status tool (#5154). */ +export function collectMinerDiagnostics(env = process.env, cwd = process.cwd()) { + const status = collectStatus(env, cwd); + const checks = runDoctorChecks(env); + const failed = checks.filter((check) => !check.ok); + const engineVersionSkew = findDoctorCheck(checks, "engine-version-skew") ?? { + name: "engine-version-skew", + ok: true, + detail: "engine-version-skew check missing", + }; + return { + package: status.package, + engine: status.engine, + node: status.node, + stateDir: status.stateDir, + configFile: status.configFile, + configValid: assessConfigValidity(status.configFile), + engineVersionSkew: { ok: engineVersionSkew.ok, detail: engineVersionSkew.detail }, + presence: { + docker: binaryPresentFromDoctorCheck(findDoctorCheck(checks, "docker-present")), + claudeCli: binaryPresentFromDoctorCheck(findDoctorCheck(checks, "claude-cli-present")), + codexCli: binaryPresentFromDoctorCheck(findDoctorCheck(checks, "codex-cli-present")), + }, + doctor: { ok: failed.length === 0, checks }, + }; +} + function renderStatusText(status) { return [ `${status.package.name} ${status.package.version ?? "unknown"} (node ${status.node})`, diff --git a/test/unit/miner-mcp-scaffold.test.ts b/test/unit/miner-mcp-scaffold.test.ts index 7b360f1e3..5f440974d 100644 --- a/test/unit/miner-mcp-scaffold.test.ts +++ b/test/unit/miner-mcp-scaffold.test.ts @@ -48,6 +48,7 @@ describe("gittensory-miner MCP server (#5153 scaffold)", () => { expect(tools.map((tool) => tool.name).sort()).toEqual([ "gittensory_miner_get_portfolio_dashboard", "gittensory_miner_ping", + "gittensory_miner_status", ]); }); diff --git a/test/unit/miner-mcp-status.test.ts b/test/unit/miner-mcp-status.test.ts new file mode 100644 index 000000000..42da9e0cf --- /dev/null +++ b/test/unit/miner-mcp-status.test.ts @@ -0,0 +1,140 @@ +import { mkdtempSync, mkdirSync, rmSync, writeFileSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { Client } from "@modelcontextprotocol/sdk/client/index.js"; +import { InMemoryTransport } from "@modelcontextprotocol/sdk/inMemory.js"; +import { afterEach, describe, expect, it, vi } from "vitest"; +import { createMinerMcpServer } from "../../packages/gittensory-miner/bin/gittensory-miner-mcp.js"; +import { initLaptopState } from "../../packages/gittensory-miner/lib/laptop-init.js"; +import { collectMinerDiagnostics } from "../../packages/gittensory-miner/lib/status.js"; +import { containsSecretLikeText } from "../../src/review/content-lane/registry-logic"; + +type Content = { content: Array<{ type: string; text?: string }> }; + +const roots: string[] = []; + +function tempRoot() { + const root = mkdtempSync(join(tmpdir(), "gittensory-miner-mcp-status-")); + roots.push(root); + return root; +} + +afterEach(() => { + vi.restoreAllMocks(); + for (const root of roots.splice(0)) rmSync(root, { recursive: true, force: true }); +}); + +async function connectedClient(options: Parameters[0] = {}): Promise { + const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair(); + const client = new Client({ name: "miner-mcp-status-test", version: "0.0.0" }); + await Promise.all([createMinerMcpServer(options).connect(serverTransport), client.connect(clientTransport)]); + return client; +} + +function toolText(result: Content): string { + const first = result.content[0]; + if (!first || first.type !== "text" || typeof first.text !== "string") { + throw new Error("expected a single text content block"); + } + return first.text; +} + +describe("gittensory_miner_status (#5154)", () => { + it("is registered on the miner MCP server", async () => { + const client = await connectedClient(); + const { tools } = await client.listTools(); + expect(tools.map((tool) => tool.name)).toContain("gittensory_miner_status"); + }); + + it("returns state-dir, engine skew, CLI presence booleans, config validity, and doctor checks", async () => { + const root = tempRoot(); + writeFileSync(join(root, ".gittensory-miner.yml"), "minerEnabled: true\n"); + const env = { + GITTENSORY_MINER_CONFIG_DIR: join(root, "state"), + PATH: "", + }; + initLaptopState(env); + const client = await connectedClient({ diagnosticsEnv: env, diagnosticsCwd: root }); + const result = (await client.callTool({ name: "gittensory_miner_status", arguments: {} })) as Content; + const payload = JSON.parse(toolText(result)); + expect(payload.stateDir).toBe(join(root, "state")); + expect(payload.configFile).toBe(join(root, ".gittensory-miner.yml")); + expect(payload.configValid).toBe(true); + expect(payload.engineVersionSkew).toEqual(expect.objectContaining({ ok: expect.any(Boolean), detail: expect.any(String) })); + expect(payload.presence).toEqual({ + docker: false, + claudeCli: false, + codexCli: false, + }); + expect(payload.doctor.checks.map((check: { name: string }) => check.name)).toEqual([ + "node-version", + "engine-resolves", + "engine-version-skew", + "state-dir-writable", + "laptop-state-sqlite", + "docker-present", + "claude-cli-present", + "codex-cli-present", + ]); + }); + + it("is structurally identical to collectMinerDiagnostics() — the wrapper adds no drift (invariant)", async () => { + const root = tempRoot(); + const env = { GITTENSORY_MINER_CONFIG_DIR: join(root, "state"), PATH: "" }; + initLaptopState(env); + const client = await connectedClient({ diagnosticsEnv: env, diagnosticsCwd: root }); + const result = (await client.callTool({ name: "gittensory_miner_status", arguments: {} })) as Content; + expect(JSON.parse(toolText(result))).toEqual(collectMinerDiagnostics(env, root)); + }); + + it("never returns secret-shaped values or configured env-var contents (invariant)", async () => { + const root = tempRoot(); + const secretToken = `ghp_${"a".repeat(36)}`; + const env = { + GITTENSORY_MINER_CONFIG_DIR: join(root, "state"), + GITHUB_TOKEN: secretToken, + CLAUDE_CODE_OAUTH_TOKEN: "oauth-secret-value", + PATH: "", + }; + initLaptopState(env); + const client = await connectedClient({ diagnosticsEnv: env, diagnosticsCwd: root }); + const result = (await client.callTool({ name: "gittensory_miner_status", arguments: {} })) as Content; + const serialized = toolText(result); + expect(serialized).not.toContain(secretToken); + expect(serialized).not.toContain("oauth-secret-value"); + expect(containsSecretLikeText(serialized)).toBe(false); + }); + + it("reports configValid false when the discovered config file is unreadable", async () => { + const root = tempRoot(); + mkdirSync(join(root, ".gittensory-miner.yml")); + const env = { GITTENSORY_MINER_CONFIG_DIR: join(root, "state"), PATH: "" }; + initLaptopState(env); + const client = await connectedClient({ diagnosticsEnv: env, diagnosticsCwd: root }); + const result = (await client.callTool({ name: "gittensory_miner_status", arguments: {} })) as Content; + expect(JSON.parse(toolText(result)).configValid).toBe(false); + }); + + it("supports a collectMinerDiagnostics injection seam for failure-path tests", async () => { + const client = await connectedClient({ + collectMinerDiagnostics: () => ({ + package: { name: "@jsonbored/gittensory-miner", version: "0.0.0-test" }, + engine: { name: "@jsonbored/gittensory-engine", version: "0.0.0" }, + node: "v22.0.0", + stateDir: "/tmp/missing-state", + configFile: null, + configValid: true, + engineVersionSkew: { ok: false, detail: "installed 0.1.0 is behind expected 0.2.0" }, + presence: { docker: false, claudeCli: false, codexCli: false }, + doctor: { + ok: false, + checks: [{ name: "state-dir-writable", ok: false, detail: "/tmp/missing-state: not writable" }], + }, + }), + }); + const result = (await client.callTool({ name: "gittensory_miner_status", arguments: {} })) as Content; + const payload = JSON.parse(toolText(result)); + expect(payload.doctor.ok).toBe(false); + expect(payload.engineVersionSkew.ok).toBe(false); + }); +}); diff --git a/test/unit/miner-status.test.ts b/test/unit/miner-status.test.ts index ef63365f8..17fb8e629 100644 --- a/test/unit/miner-status.test.ts +++ b/test/unit/miner-status.test.ts @@ -4,6 +4,7 @@ import { join } from "node:path"; import { afterEach, describe, expect, it, vi } from "vitest"; import { buildEngineVersionSkewCheck, + collectMinerDiagnostics, collectStatus, compareInstalledEngineVersion, readExpectedEnginePackageVersion, @@ -60,6 +61,35 @@ describe("gittensory-miner status/doctor (#2288)", () => { expect(status.package.version).toBe("gittensory-miner-fleet@deadbeef"); }); + it("collectMinerDiagnostics merges status and doctor checks without drift (#5154)", () => { + const root = tempRoot(); + writeFileSync(join(root, ".gittensory-miner.yml"), "minerEnabled: true\n"); + const env = { GITTENSORY_MINER_CONFIG_DIR: join(root, "state"), PATH: "" }; + initLaptopState(env); + const diagnostics = collectMinerDiagnostics(env, root); + expect(diagnostics.stateDir).toBe(join(root, "state")); + expect(diagnostics.configFile).toBe(join(root, ".gittensory-miner.yml")); + expect(diagnostics.configValid).toBe(true); + expect(diagnostics.presence).toEqual({ docker: false, claudeCli: false, codexCli: false }); + expect(diagnostics.doctor.checks).toEqual(runDoctorChecks(env)); + expect(diagnostics.engineVersionSkew).toEqual({ + ok: runDoctorChecks(env).find((check) => check.name === "engine-version-skew")!.ok, + detail: runDoctorChecks(env).find((check) => check.name === "engine-version-skew")!.detail, + }); + }); + + it("collectMinerDiagnostics treats a missing config file as valid defaults", () => { + const env = { GITTENSORY_MINER_CONFIG_DIR: join(tempRoot(), "state"), PATH: "" }; + expect(collectMinerDiagnostics(env, tempRoot()).configValid).toBe(true); + }); + + it("collectMinerDiagnostics marks configValid false when the config path is unreadable", () => { + const root = tempRoot(); + mkdirSync(join(root, ".gittensory-miner.yml")); + const env = { GITTENSORY_MINER_CONFIG_DIR: join(root, "state"), PATH: "" }; + expect(collectMinerDiagnostics(env, root).configValid).toBe(false); + }); + it("runStatus prints human-readable text (0) and machine JSON with --json", () => { const log = vi.spyOn(console, "log").mockImplementation(() => {}); expect(runStatus([], { GITTENSORY_MINER_CONFIG_DIR: "/s" }, tempRoot())).toBe(0); From 084a274a9e74fcb9e2ddd20e4cfff611df5f9e3e Mon Sep 17 00:00:00 2001 From: andriypolandki Date: Sun, 12 Jul 2026 07:09:52 -0700 Subject: [PATCH 2/4] fix --- scripts/seed-local-maintainer-preview.sql | 59 +++++++++++++++++++++++ 1 file changed, 59 insertions(+) create mode 100644 scripts/seed-local-maintainer-preview.sql diff --git a/scripts/seed-local-maintainer-preview.sql b/scripts/seed-local-maintainer-preview.sql new file mode 100644 index 000000000..944208b59 --- /dev/null +++ b/scripts/seed-local-maintainer-preview.sql @@ -0,0 +1,59 @@ +-- Local maintainer UI preview seed (#2216). +-- Safe to re-run. Uses a repo under your login so it appears in preview dropdowns. +-- +-- After seeding: +-- npx wrangler d1 execute gittensory --local --file=scripts/seed-local-maintainer-preview.sql +-- Then restart `npm run dev` and open /app/maintainer + +DELETE FROM pull_requests WHERE repo_full_name = 'andriypolanski/local-preview'; +DELETE FROM repository_settings WHERE repo_full_name = 'andriypolanski/local-preview'; +DELETE FROM repositories WHERE full_name = 'andriypolanski/local-preview'; + +INSERT OR REPLACE INTO installations (id, account_login, account_id, target_type, repository_selection, permissions_json, events_json) +VALUES (1, 'andriypolanski', 1, 'User', 'selected', '{}', '[]'); + +INSERT OR REPLACE INTO installation_health ( + installation_id, + account_login, + repository_selection, + installed_repos_count, + registered_installed_count, + status, + missing_permissions_json, + missing_events_json, + permissions_json, + events_json, + checked_at +) +VALUES (1, 'andriypolanski', 'selected', 1, 0, 'healthy', '[]', '[]', '{}', '[]', datetime('now')); + +INSERT OR REPLACE INTO repositories (full_name, owner, name, installation_id, is_installed, is_registered, is_private) +VALUES ('andriypolanski/local-preview', 'andriypolanski', 'local-preview', 1, 1, 0, 0); + +-- Context check ON + standard detail so the readiness table renders in preview. +INSERT OR REPLACE INTO repository_settings (repo_full_name, check_run_mode, check_run_detail_level, public_surface, comment_mode) +VALUES ('andriypolanski/local-preview', 'enabled', 'standard', 'comment_and_label', 'all_prs'); + +-- Populates the repo dropdown (reviewability list) on the maintainer dashboard. +INSERT OR REPLACE INTO pull_requests ( + id, + repo_full_name, + number, + title, + state, + author_login, + author_association, + labels_json, + linked_issues_json +) +VALUES ( + 'andriypolanski/local-preview#1', + 'andriypolanski/local-preview', + 1, + 'Sample PR for local UI preview', + 'open', + 'sample-miner', + 'CONTRIBUTOR', + '["bug"]', + '[7]' +); From e9e4ec698aa4f4a815e06ea501d7d7394b1b540c Mon Sep 17 00:00:00 2001 From: andriypolandki Date: Sun, 12 Jul 2026 07:12:19 -0700 Subject: [PATCH 3/4] fix --- packages/gittensory-miner/lib/status.js | 3 ++ scripts/seed-local-maintainer-preview.sql | 59 ----------------------- test/unit/miner-mcp-status.test.ts | 1 + 3 files changed, 4 insertions(+), 59 deletions(-) delete mode 100644 scripts/seed-local-maintainer-preview.sql diff --git a/packages/gittensory-miner/lib/status.js b/packages/gittensory-miner/lib/status.js index b7c5bad30..12ef97095 100644 --- a/packages/gittensory-miner/lib/status.js +++ b/packages/gittensory-miner/lib/status.js @@ -274,7 +274,10 @@ export function collectMinerDiagnostics(env = process.env, cwd = process.cwd()) codexCli: binaryPresentFromDoctorCheck(findDoctorCheck(checks, "codex-cli-present")), }, doctor: { ok: failed.length === 0, checks }, + driver: status.driver, }; +} + function renderDriverLine(driver) { if (!driver.provider) return "driver: none configured"; const cliText = driver.cliPresent === null ? "n/a" : driver.cliPresent ? "yes" : "no"; diff --git a/scripts/seed-local-maintainer-preview.sql b/scripts/seed-local-maintainer-preview.sql deleted file mode 100644 index 944208b59..000000000 --- a/scripts/seed-local-maintainer-preview.sql +++ /dev/null @@ -1,59 +0,0 @@ --- Local maintainer UI preview seed (#2216). --- Safe to re-run. Uses a repo under your login so it appears in preview dropdowns. --- --- After seeding: --- npx wrangler d1 execute gittensory --local --file=scripts/seed-local-maintainer-preview.sql --- Then restart `npm run dev` and open /app/maintainer - -DELETE FROM pull_requests WHERE repo_full_name = 'andriypolanski/local-preview'; -DELETE FROM repository_settings WHERE repo_full_name = 'andriypolanski/local-preview'; -DELETE FROM repositories WHERE full_name = 'andriypolanski/local-preview'; - -INSERT OR REPLACE INTO installations (id, account_login, account_id, target_type, repository_selection, permissions_json, events_json) -VALUES (1, 'andriypolanski', 1, 'User', 'selected', '{}', '[]'); - -INSERT OR REPLACE INTO installation_health ( - installation_id, - account_login, - repository_selection, - installed_repos_count, - registered_installed_count, - status, - missing_permissions_json, - missing_events_json, - permissions_json, - events_json, - checked_at -) -VALUES (1, 'andriypolanski', 'selected', 1, 0, 'healthy', '[]', '[]', '{}', '[]', datetime('now')); - -INSERT OR REPLACE INTO repositories (full_name, owner, name, installation_id, is_installed, is_registered, is_private) -VALUES ('andriypolanski/local-preview', 'andriypolanski', 'local-preview', 1, 1, 0, 0); - --- Context check ON + standard detail so the readiness table renders in preview. -INSERT OR REPLACE INTO repository_settings (repo_full_name, check_run_mode, check_run_detail_level, public_surface, comment_mode) -VALUES ('andriypolanski/local-preview', 'enabled', 'standard', 'comment_and_label', 'all_prs'); - --- Populates the repo dropdown (reviewability list) on the maintainer dashboard. -INSERT OR REPLACE INTO pull_requests ( - id, - repo_full_name, - number, - title, - state, - author_login, - author_association, - labels_json, - linked_issues_json -) -VALUES ( - 'andriypolanski/local-preview#1', - 'andriypolanski/local-preview', - 1, - 'Sample PR for local UI preview', - 'open', - 'sample-miner', - 'CONTRIBUTOR', - '["bug"]', - '[7]' -); diff --git a/test/unit/miner-mcp-status.test.ts b/test/unit/miner-mcp-status.test.ts index 42da9e0cf..ae1dd508e 100644 --- a/test/unit/miner-mcp-status.test.ts +++ b/test/unit/miner-mcp-status.test.ts @@ -123,6 +123,7 @@ describe("gittensory_miner_status (#5154)", () => { node: "v22.0.0", stateDir: "/tmp/missing-state", configFile: null, + driver: { provider: null, modelEnvVar: null, cliPresent: null }, configValid: true, engineVersionSkew: { ok: false, detail: "installed 0.1.0 is behind expected 0.2.0" }, presence: { docker: false, claudeCli: false, codexCli: false }, From 8e5768e666222093be0f04a0a45996e2d37d63a5 Mon Sep 17 00:00:00 2001 From: andriypolandki Date: Sun, 12 Jul 2026 07:24:02 -0700 Subject: [PATCH 4/4] fix clean test check --- .../bin/gittensory-miner-mcp.d.ts | 2 +- .../bin/gittensory-miner-mcp.js | 31 +++++++++++++++++-- packages/gittensory-miner/lib/status.js | 2 +- test/unit/miner-mcp-status.test.ts | 12 ++++--- 4 files changed, 37 insertions(+), 10 deletions(-) diff --git a/packages/gittensory-miner/bin/gittensory-miner-mcp.d.ts b/packages/gittensory-miner/bin/gittensory-miner-mcp.d.ts index 12da3fadb..4081669e7 100644 --- a/packages/gittensory-miner/bin/gittensory-miner-mcp.d.ts +++ b/packages/gittensory-miner/bin/gittensory-miner-mcp.d.ts @@ -30,7 +30,7 @@ export interface MinerMcpServerOptions { /** * Build the miner MCP server with its tools registered (gittensory_miner_ping, - * gittensory_miner_get_portfolio_dashboard, gittensory_miner_status). + * gittensory_miner_get_portfolio_dashboard, gittensory_miner_list_claims, gittensory_miner_status). * `options` supplies test injection seams; production callers pass nothing. */ export function createMinerMcpServer(options?: MinerMcpServerOptions): McpServer; diff --git a/packages/gittensory-miner/bin/gittensory-miner-mcp.js b/packages/gittensory-miner/bin/gittensory-miner-mcp.js index b4c22be10..88edc7aab 100755 --- a/packages/gittensory-miner/bin/gittensory-miner-mcp.js +++ b/packages/gittensory-miner/bin/gittensory-miner-mcp.js @@ -29,9 +29,9 @@ const ownPackageJson = JSON.parse(readFileSync(new URL("../package.json", import export const MINER_PING_STATUS = { status: "ok", tool: "gittensory_miner_ping" }; /** - * Build the miner MCP server with its tools registered. `options.initPortfolioQueue` / `options.nowMs` are - * injection seams for tests (default to the real portfolio-queue store and the wall clock); the ping tool needs - * neither. The portfolio-dashboard tool opens the queue only when invoked and closes any store it opened. + * Build the miner MCP server with its tools registered. `options.initPortfolioQueue`, `options.openClaimLedger`, + * and `options.nowMs` are injection seams for tests (default to the real stores 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. * `options.collectMinerDiagnostics` / `options.diagnosticsEnv` / `options.diagnosticsCwd` inject the status tool. */ export function createMinerMcpServer(options = {}) { @@ -66,6 +66,31 @@ export function createMinerMcpServer(options = {}) { } }, ); + server.registerTool( + "gittensory_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( "gittensory_miner_status", { diff --git a/packages/gittensory-miner/lib/status.js b/packages/gittensory-miner/lib/status.js index 12ef97095..0444ff0ce 100644 --- a/packages/gittensory-miner/lib/status.js +++ b/packages/gittensory-miner/lib/status.js @@ -339,7 +339,7 @@ export function runDoctorChecks(env = process.env) { checkEngineVersionSkew(), checkStateDirWritable(resolveMinerStateDir(env)), checkLaptopStateSqlite(env), - checkDockerPresent(), + checkDockerPresent({ env }), checkClaudeCliPresent({ env }), checkCodexCliPresent({ env }), ]; diff --git a/test/unit/miner-mcp-status.test.ts b/test/unit/miner-mcp-status.test.ts index ae1dd508e..901bef887 100644 --- a/test/unit/miner-mcp-status.test.ts +++ b/test/unit/miner-mcp-status.test.ts @@ -89,19 +89,21 @@ describe("gittensory_miner_status (#5154)", () => { it("never returns secret-shaped values or configured env-var contents (invariant)", async () => { const root = tempRoot(); - const secretToken = `ghp_${"a".repeat(36)}`; + // Assembled at runtime so this test file never contains a contiguous gate-flagged secret literal. + const githubToken = "ghp_" + "a".repeat(36); + const oauthSecret = "oauth-secret-" + "value"; const env = { GITTENSORY_MINER_CONFIG_DIR: join(root, "state"), - GITHUB_TOKEN: secretToken, - CLAUDE_CODE_OAUTH_TOKEN: "oauth-secret-value", + GITHUB_TOKEN: githubToken, + CLAUDE_CODE_OAUTH_TOKEN: oauthSecret, PATH: "", }; initLaptopState(env); const client = await connectedClient({ diagnosticsEnv: env, diagnosticsCwd: root }); const result = (await client.callTool({ name: "gittensory_miner_status", arguments: {} })) as Content; const serialized = toolText(result); - expect(serialized).not.toContain(secretToken); - expect(serialized).not.toContain("oauth-secret-value"); + expect(serialized).not.toContain(githubToken); + expect(serialized).not.toContain(oauthSecret); expect(containsSecretLikeText(serialized)).toBe(false); });