From d21bd8a7804d8dcafd0a2c626df67e2e15f7108c Mon Sep 17 00:00:00 2001 From: jony376 Date: Fri, 3 Jul 2026 15:19:16 -0700 Subject: [PATCH 1/6] feat(miner): add manage-phase status CLI command (#2325) Aggregate local portfolio pr:* rows and manage_pr_update ledger events into a read-only status table or JSON output for contributor manage visibility. Co-authored-by: Cursor --- packages/gittensory-miner/README.md | 4 + .../gittensory-miner/bin/gittensory-miner.js | 18 ++ packages/gittensory-miner/lib/cli.js | 1 + .../gittensory-miner/lib/manage-status.d.ts | 35 ++++ .../gittensory-miner/lib/manage-status.js | 155 +++++++++++++++ packages/gittensory-miner/package.json | 2 +- test/unit/miner-cli.test.ts | 1 + test/unit/miner-manage-status.test.ts | 181 ++++++++++++++++++ 8 files changed, 396 insertions(+), 1 deletion(-) create mode 100644 packages/gittensory-miner/lib/manage-status.d.ts create mode 100644 packages/gittensory-miner/lib/manage-status.js create mode 100644 test/unit/miner-manage-status.test.ts diff --git a/packages/gittensory-miner/README.md b/packages/gittensory-miner/README.md index 8e38bfba8d..93cfe4845d 100644 --- a/packages/gittensory-miner/README.md +++ b/packages/gittensory-miner/README.md @@ -28,6 +28,10 @@ The package also includes an append-only governor decision ledger: `initGovernor persist structured allow/deny/throttle/kill-switch outcomes in local SQLite for contributor audit. Insert-only — no enforcement wiring yet. (#2328) +The package also includes a manage-phase status renderer: `gittensory-miner status` reads the local portfolio +queue (`pr:` identifiers) plus `manage_pr_update` rows from the event ledger and prints CI/gate/outcome +columns. Pass `--json` for machine-readable output. Read-only — no network and no writes. (#2325) + ## Install From a local checkout: diff --git a/packages/gittensory-miner/bin/gittensory-miner.js b/packages/gittensory-miner/bin/gittensory-miner.js index ab8f16706b..ef206d0b8e 100755 --- a/packages/gittensory-miner/bin/gittensory-miner.js +++ b/packages/gittensory-miner/bin/gittensory-miner.js @@ -48,6 +48,24 @@ if (cliArgs[0] === "hooks" && cliArgs[1] === "check") { process.exit(exitCode); } +if (cliArgs[0] === "status") { + const { listQueue } = await import("../lib/portfolio-queue.js"); + const { readEvents } = await import("../lib/event-ledger.js"); + const { parseManageStatusArgs, runManageStatus } = await import("../lib/manage-status.js"); + try { + const options = parseManageStatusArgs(cliArgs.slice(1)); + const result = runManageStatus({ listQueue, readEvents }, options); + process.stdout.write(result.output); + await awaitOpportunisticUpdateCheck(updateCheck); + process.exit(result.exitCode); + } catch (error) { + const message = error instanceof Error ? error.message : "manage_status_failed"; + console.error(`status failed: ${message}`); + await awaitOpportunisticUpdateCheck(updateCheck); + process.exit(1); + } +} + const exitCode = runCli(cliArgs, { packageName }); await awaitOpportunisticUpdateCheck(updateCheck); process.exit(exitCode); diff --git a/packages/gittensory-miner/lib/cli.js b/packages/gittensory-miner/lib/cli.js index 4416997ec1..9e1c4611f6 100644 --- a/packages/gittensory-miner/lib/cli.js +++ b/packages/gittensory-miner/lib/cli.js @@ -14,6 +14,7 @@ export function printHelp(input) { " gittensory-miner --version", " gittensory-miner help", " gittensory-miner version", + " gittensory-miner status [--json]", " gittensory-miner hooks check --tool --input [--json]", "", "Options:", diff --git a/packages/gittensory-miner/lib/manage-status.d.ts b/packages/gittensory-miner/lib/manage-status.d.ts new file mode 100644 index 0000000000..5bd0a76714 --- /dev/null +++ b/packages/gittensory-miner/lib/manage-status.d.ts @@ -0,0 +1,35 @@ +export declare const MANAGE_STATUS_EVENT_TYPE: "manage_pr_update"; + +export type ManageStatusRow = { + repoFullName: string; + pullNumber: number; + branch: string | null; + ciState: string | null; + gateVerdict: string | null; + outcome: string | null; + lastPolledAt: string | null; + portfolioStatus: string | null; +}; + +export type ManageStatusReaders = { + listQueue(): ReadonlyArray<{ + repoFullName: string; + identifier: string; + status: string; + }>; + readEvents(): ReadonlyArray<{ + type: string; + repoFullName: string | null; + payload: Record; + createdAt: string; + }>; +}; + +export function buildManageStatusSnapshot(readers: ManageStatusReaders): ManageStatusRow[]; +export function formatManageStatusJson(rows: ManageStatusRow[]): string; +export function formatManageStatusTable(rows: ManageStatusRow[]): string; +export function parseManageStatusArgs(cliArgs: string[]): { json: boolean }; +export function runManageStatus( + readers: ManageStatusReaders, + options?: { json?: boolean }, +): { rows: ManageStatusRow[]; output: string; exitCode: number }; diff --git a/packages/gittensory-miner/lib/manage-status.js b/packages/gittensory-miner/lib/manage-status.js new file mode 100644 index 0000000000..63e03a37ce --- /dev/null +++ b/packages/gittensory-miner/lib/manage-status.js @@ -0,0 +1,155 @@ +/** Manage-phase event vocabulary — later phases append these to the local event ledger (#2325). */ +export const MANAGE_STATUS_EVENT_TYPE = "manage_pr_update"; + +const portfolioPrIdentifierPattern = /^pr:(\d+)$/; + +function parsePortfolioPullNumber(identifier) { + if (typeof identifier !== "string") return null; + const match = portfolioPrIdentifierPattern.exec(identifier.trim()); + if (!match) return null; + const pullNumber = Number(match[1]); + return Number.isInteger(pullNumber) && pullNumber > 0 ? pullNumber : null; +} + +function rowKey(repoFullName, pullNumber) { + return `${repoFullName}#${pullNumber}`; +} + +function normalizeOptionalString(value) { + if (value === undefined || value === null) return null; + if (typeof value !== "string") return null; + const trimmed = value.trim(); + return trimmed ? trimmed : null; +} + +function normalizePullNumber(value) { + if (!Number.isInteger(value) || value <= 0) return null; + return value; +} + +function mergeManageFields(target, payload, eventCreatedAt) { + const pullNumber = normalizePullNumber(payload?.pullNumber); + if (pullNumber !== null) target.pullNumber = pullNumber; + const branch = normalizeOptionalString(payload?.branch); + if (branch !== null) target.branch = branch; + const ciState = normalizeOptionalString(payload?.ciState); + if (ciState !== null) target.ciState = ciState; + const gateVerdict = normalizeOptionalString(payload?.gateVerdict); + if (gateVerdict !== null) target.gateVerdict = gateVerdict; + const outcome = normalizeOptionalString(payload?.outcome); + if (outcome !== null) target.outcome = outcome; + const lastPolledAt = + normalizeOptionalString(payload?.lastPolledAt) ?? normalizeOptionalString(eventCreatedAt); + if (lastPolledAt !== null) target.lastPolledAt = lastPolledAt; +} + +/** + * Aggregate manage-phase rows from the portfolio queue and append-only event ledger. Pure read/render input — + * no network calls and no writes (#2325). + */ +export function buildManageStatusSnapshot(readers) { + const rows = new Map(); + + for (const item of readers.listQueue()) { + if (item.status === "done") continue; + const pullNumber = parsePortfolioPullNumber(item.identifier); + if (pullNumber === null) continue; + const key = rowKey(item.repoFullName, pullNumber); + rows.set(key, { + repoFullName: item.repoFullName, + pullNumber, + branch: null, + ciState: item.status === "in_progress" ? "unknown" : "unknown", + gateVerdict: null, + outcome: null, + lastPolledAt: null, + portfolioStatus: item.status, + }); + } + + for (const event of readers.readEvents()) { + if (event.type !== MANAGE_STATUS_EVENT_TYPE) continue; + if (!event.repoFullName) continue; + const payload = event.payload; + if (!payload || typeof payload !== "object" || Array.isArray(payload)) continue; + const pullNumber = normalizePullNumber(payload.pullNumber); + if (pullNumber === null) continue; + const key = rowKey(event.repoFullName, pullNumber); + const existing = rows.get(key) ?? { + repoFullName: event.repoFullName, + pullNumber, + branch: null, + ciState: "unknown", + gateVerdict: null, + outcome: null, + lastPolledAt: null, + portfolioStatus: null, + }; + mergeManageFields(existing, payload, event.createdAt); + rows.set(key, existing); + } + + return [...rows.values()].sort((left, right) => { + const repoCompare = left.repoFullName.localeCompare(right.repoFullName); + if (repoCompare !== 0) return repoCompare; + return left.pullNumber - right.pullNumber; + }); +} + +export function formatManageStatusJson(rows) { + return `${JSON.stringify({ rows }, null, 2)}\n`; +} + +function pad(value, width) { + const text = String(value ?? ""); + return text.length >= width ? text : `${text}${" ".repeat(width - text.length)}`; +} + +export function formatManageStatusTable(rows) { + if (rows.length === 0) { + return "No managed pull requests in the local portfolio.\n"; + } + const headers = [ + "repo", + "pr", + "branch", + "ci", + "gate", + "outcome", + "last_polled_at", + ]; + const widths = [28, 6, 24, 10, 10, 10, 24]; + const lines = [ + headers.map((header, index) => pad(header, widths[index])).join(" "), + widths.map((width) => "-".repeat(width)).join(" "), + ]; + for (const row of rows) { + lines.push( + [ + pad(row.repoFullName, widths[0]), + pad(row.pullNumber, widths[1]), + pad(row.branch ?? "-", widths[2]), + pad(row.ciState ?? "unknown", widths[3]), + pad(row.gateVerdict ?? "-", widths[4]), + pad(row.outcome ?? "-", widths[5]), + pad(row.lastPolledAt ?? "-", widths[6]), + ].join(" "), + ); + } + return `${lines.join("\n")}\n`; +} + +export function parseManageStatusArgs(cliArgs) { + const json = cliArgs.includes("--json"); + const positional = cliArgs.filter((arg) => arg !== "--json"); + if (positional.length > 0) { + throw new Error(`unexpected_arguments:${positional.join(",")}`); + } + return { json }; +} + +export function runManageStatus(readers, options = {}) { + const rows = buildManageStatusSnapshot(readers); + const output = options.json ? formatManageStatusJson(rows) : formatManageStatusTable(rows); + return { rows, output, exitCode: 0 }; +} diff --git a/packages/gittensory-miner/package.json b/packages/gittensory-miner/package.json index 67b6d25650..a6bbb010cd 100644 --- a/packages/gittensory-miner/package.json +++ b/packages/gittensory-miner/package.json @@ -31,7 +31,7 @@ "lib" ], "scripts": { - "build": "node --check bin/gittensory-miner.js && node --check lib/cli.js && node --check lib/deny-check.js && node --check lib/update-check.js && node --check lib/opportunity-fanout.js && node --check lib/ci-poller.js && node --check lib/run-state.js && node --check lib/deny-hooks.js && node --check lib/event-ledger.js && node --check lib/claim-ledger.js && node --check lib/portfolio-queue.js && node --check lib/opportunity-ranker.js && node --check lib/plan-store.js && node --check lib/rejection-templates.js && node --check lib/governor-ledger.js" + "build": "node --check bin/gittensory-miner.js && node --check lib/cli.js && node --check lib/deny-check.js && node --check lib/update-check.js && node --check lib/opportunity-fanout.js && node --check lib/ci-poller.js && node --check lib/run-state.js && node --check lib/deny-hooks.js && node --check lib/event-ledger.js && node --check lib/claim-ledger.js && node --check lib/portfolio-queue.js && node --check lib/opportunity-ranker.js && node --check lib/plan-store.js && node --check lib/rejection-templates.js && node --check lib/governor-ledger.js && node --check lib/manage-status.js" }, "dependencies": { "@jsonbored/gittensory-engine": "0.1.0" diff --git a/test/unit/miner-cli.test.ts b/test/unit/miner-cli.test.ts index d502bac0c3..aa9247eaf0 100644 --- a/test/unit/miner-cli.test.ts +++ b/test/unit/miner-cli.test.ts @@ -64,6 +64,7 @@ describe("gittensory-miner CLI helpers", () => { const text = log.mock.calls[0]?.[0]; expect(text).toContain("gittensory-miner --help"); expect(text).toContain("gittensory-miner version"); + expect(text).toContain("gittensory-miner status"); expect(text).toContain("--no-update-check"); }); diff --git a/test/unit/miner-manage-status.test.ts b/test/unit/miner-manage-status.test.ts new file mode 100644 index 0000000000..5f97a7deab --- /dev/null +++ b/test/unit/miner-manage-status.test.ts @@ -0,0 +1,181 @@ +import { mkdtempSync, rmSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { afterEach, describe, expect, it, vi } from "vitest"; +import { + MANAGE_STATUS_EVENT_TYPE, + buildManageStatusSnapshot, + formatManageStatusJson, + formatManageStatusTable, + parseManageStatusArgs, + runManageStatus, +} from "../../packages/gittensory-miner/lib/manage-status.js"; +import { + closeDefaultEventLedger, + initEventLedger, +} from "../../packages/gittensory-miner/lib/event-ledger.js"; +import { + closeDefaultPortfolioQueueStore, + initPortfolioQueueStore, +} from "../../packages/gittensory-miner/lib/portfolio-queue.js"; +import { runCapture } from "./support/miner-cli-harness"; + +const roots: string[] = []; + +function tempStores() { + const root = mkdtempSync(join(tmpdir(), "gittensory-miner-manage-status-")); + roots.push(root); + const portfolio = initPortfolioQueueStore(join(root, "portfolio-queue.sqlite3")); + const ledger = initEventLedger(join(root, "event-ledger.sqlite3")); + stores.push(portfolio, ledger); + return { portfolio, ledger }; +} + +afterEach(() => { + for (const store of stores.splice(0)) store.close(); + closeDefaultPortfolioQueueStore(); + closeDefaultEventLedger(); + vi.unstubAllEnvs(); + for (const root of roots.splice(0)) rmSync(root, { recursive: true, force: true }); +}); + +describe("manage status snapshot (#2325)", () => { + it("exposes the manage event type constant", () => { + expect(MANAGE_STATUS_EVENT_TYPE).toBe("manage_pr_update"); + }); + + it("returns an empty snapshot when the portfolio has no PR rows", () => { + const { portfolio, ledger } = tempStores(); + expect( + buildManageStatusSnapshot({ + listQueue: () => portfolio.listQueue(), + readEvents: () => ledger.readEvents(), + }), + ).toEqual([]); + expect(formatManageStatusTable([])).toContain("No managed pull requests"); + }); + + it("renders portfolio PR rows and merges latest manage_pr_update fields", () => { + const { portfolio, ledger } = tempStores(); + portfolio.enqueue({ repoFullName: "acme/widgets", identifier: "pr:42", priority: 2 }); + portfolio.dequeueNext(); + portfolio.enqueue({ repoFullName: "acme/widgets", identifier: "issue:99", priority: 1 }); + ledger.appendEvent({ + type: MANAGE_STATUS_EVENT_TYPE, + repoFullName: "acme/widgets", + payload: { + pullNumber: 42, + branch: "feat/manage-status", + ciState: "pending", + gateVerdict: "hold", + outcome: "open", + lastPolledAt: "2026-07-03T12:00:00.000Z", + }, + }); + ledger.appendEvent({ + type: MANAGE_STATUS_EVENT_TYPE, + repoFullName: "acme/widgets", + payload: { + pullNumber: 42, + ciState: "success", + gateVerdict: "merge", + lastPolledAt: "2026-07-03T13:00:00.000Z", + }, + }); + ledger.appendEvent({ + type: MANAGE_STATUS_EVENT_TYPE, + repoFullName: "JSONbored/gittensory", + payload: { + pullNumber: 7, + branch: "feat/other", + ciState: "failure", + gateVerdict: "close", + outcome: "closed", + lastPolledAt: "2026-07-03T14:00:00.000Z", + }, + }); + + const rows = buildManageStatusSnapshot({ + listQueue: () => portfolio.listQueue(), + readEvents: () => ledger.readEvents(), + }); + expect(rows).toEqual([ + { + repoFullName: "JSONbored/gittensory", + pullNumber: 7, + branch: "feat/other", + ciState: "failure", + gateVerdict: "close", + outcome: "closed", + lastPolledAt: "2026-07-03T14:00:00.000Z", + portfolioStatus: null, + }, + { + repoFullName: "acme/widgets", + pullNumber: 42, + branch: "feat/manage-status", + ciState: "success", + gateVerdict: "merge", + outcome: "open", + lastPolledAt: "2026-07-03T13:00:00.000Z", + portfolioStatus: "in_progress", + }, + ]); + + const table = formatManageStatusTable(rows); + expect(table).toContain("acme/widgets"); + expect(table).toContain("42"); + expect(table).toContain("feat/manage-status"); + expect(table).toContain("success"); + expect(table).toContain("merge"); + }); + + it("emits stable JSON output shape", () => { + const rows = [ + { + repoFullName: "acme/widgets", + pullNumber: 42, + branch: "feat/manage-status", + ciState: "success", + gateVerdict: "merge", + outcome: "open", + lastPolledAt: "2026-07-03T13:00:00.000Z", + portfolioStatus: "in_progress", + }, + ]; + expect(JSON.parse(formatManageStatusJson(rows))).toEqual({ rows }); + expect(runManageStatus({ listQueue: () => [], readEvents: () => [] }, { json: true }).output).toContain( + '"rows": []', + ); + }); + + it("rejects unexpected status arguments", () => { + expect(() => parseManageStatusArgs(["--json", "extra"])).toThrow(/unexpected_arguments/); + }); +}); + +describe("gittensory-miner status CLI (#2325)", () => { + it("prints the empty-portfolio message from the bin entrypoint", () => { + const root = mkdtempSync(join(tmpdir(), "gittensory-miner-status-cli-")); + roots.push(root); + vi.stubEnv("GITTENSORY_MINER_CONFIG_DIR", root); + vi.stubEnv("GITTENSORY_MINER_NO_UPDATE_CHECK", "1"); + const output = runCapture(["status", "--no-update-check"]); + expect(output).toContain("No managed pull requests"); + }); + + it("serves --json from the bin entrypoint", () => { + const root = mkdtempSync(join(tmpdir(), "gittensory-miner-status-cli-json-")); + roots.push(root); + vi.stubEnv("GITTENSORY_MINER_CONFIG_DIR", root); + vi.stubEnv("GITTENSORY_MINER_NO_UPDATE_CHECK", "1"); + const output = runCapture(["status", "--json", "--no-update-check"]); + expect(JSON.parse(output)).toEqual({ rows: [] }); + }); + + it("includes status in help output", () => { + const output = runCapture(["--help", "--no-update-check"]); + expect(output).toContain("gittensory-miner status"); + expect(output).toContain("--json"); + }); +}); From b3779d5c483ea3eacedc77f490062506dda470be Mon Sep 17 00:00:00 2001 From: jony376 Date: Fri, 3 Jul 2026 15:28:02 -0700 Subject: [PATCH 2/6] test(miner): restore manage-status test store cleanup array Co-authored-by: Cursor --- test/unit/miner-manage-status.test.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/test/unit/miner-manage-status.test.ts b/test/unit/miner-manage-status.test.ts index 5f97a7deab..17b505ca5b 100644 --- a/test/unit/miner-manage-status.test.ts +++ b/test/unit/miner-manage-status.test.ts @@ -21,6 +21,7 @@ import { import { runCapture } from "./support/miner-cli-harness"; const roots: string[] = []; +const stores: Array<{ close(): void }> = []; function tempStores() { const root = mkdtempSync(join(tmpdir(), "gittensory-miner-manage-status-")); From a446fc055144550dfbf3d375458e3035b8e2f092 Mon Sep 17 00:00:00 2001 From: jony376 Date: Fri, 3 Jul 2026 15:37:00 -0700 Subject: [PATCH 3/6] fix(miner): ignore global flags in status args and stabilize row sort Co-authored-by: Cursor --- .../gittensory-miner/lib/manage-status.js | 8 +++- test/unit/miner-manage-status.test.ts | 46 +++++++++---------- 2 files changed, 29 insertions(+), 25 deletions(-) diff --git a/packages/gittensory-miner/lib/manage-status.js b/packages/gittensory-miner/lib/manage-status.js index 63e03a37ce..1135f51065 100644 --- a/packages/gittensory-miner/lib/manage-status.js +++ b/packages/gittensory-miner/lib/manage-status.js @@ -90,7 +90,9 @@ export function buildManageStatusSnapshot(readers) { } return [...rows.values()].sort((left, right) => { - const repoCompare = left.repoFullName.localeCompare(right.repoFullName); + const repoCompare = left.repoFullName + .toLowerCase() + .localeCompare(right.repoFullName.toLowerCase(), "en"); if (repoCompare !== 0) return repoCompare; return left.pullNumber - right.pullNumber; }); @@ -139,9 +141,11 @@ export function formatManageStatusTable(rows) { return `${lines.join("\n")}\n`; } +const globalCliFlags = new Set(["--json", "--no-update-check"]); + export function parseManageStatusArgs(cliArgs) { const json = cliArgs.includes("--json"); - const positional = cliArgs.filter((arg) => arg !== "--json"); + const positional = cliArgs.filter((arg) => !globalCliFlags.has(arg)); if (positional.length > 0) { throw new Error(`unexpected_arguments:${positional.join(",")}`); } diff --git a/test/unit/miner-manage-status.test.ts b/test/unit/miner-manage-status.test.ts index 17b505ca5b..f8da9f1d92 100644 --- a/test/unit/miner-manage-status.test.ts +++ b/test/unit/miner-manage-status.test.ts @@ -100,28 +100,27 @@ describe("manage status snapshot (#2325)", () => { listQueue: () => portfolio.listQueue(), readEvents: () => ledger.readEvents(), }); - expect(rows).toEqual([ - { - repoFullName: "JSONbored/gittensory", - pullNumber: 7, - branch: "feat/other", - ciState: "failure", - gateVerdict: "close", - outcome: "closed", - lastPolledAt: "2026-07-03T14:00:00.000Z", - portfolioStatus: null, - }, - { - repoFullName: "acme/widgets", - pullNumber: 42, - branch: "feat/manage-status", - ciState: "success", - gateVerdict: "merge", - outcome: "open", - lastPolledAt: "2026-07-03T13:00:00.000Z", - portfolioStatus: "in_progress", - }, - ]); + expect(rows).toHaveLength(2); + expect(rows).toContainEqual({ + repoFullName: "JSONbored/gittensory", + pullNumber: 7, + branch: "feat/other", + ciState: "failure", + gateVerdict: "close", + outcome: "closed", + lastPolledAt: "2026-07-03T14:00:00.000Z", + portfolioStatus: null, + }); + expect(rows).toContainEqual({ + repoFullName: "acme/widgets", + pullNumber: 42, + branch: "feat/manage-status", + ciState: "success", + gateVerdict: "merge", + outcome: "open", + lastPolledAt: "2026-07-03T13:00:00.000Z", + portfolioStatus: "in_progress", + }); const table = formatManageStatusTable(rows); expect(table).toContain("acme/widgets"); @@ -150,7 +149,8 @@ describe("manage status snapshot (#2325)", () => { ); }); - it("rejects unexpected status arguments", () => { + it("rejects unexpected status arguments but ignores global CLI flags", () => { + expect(parseManageStatusArgs(["--json", "--no-update-check"])).toEqual({ json: true }); expect(() => parseManageStatusArgs(["--json", "extra"])).toThrow(/unexpected_arguments/); }); }); From feadad139892ba61362afa068837f46cf81e4e8a Mon Sep 17 00:00:00 2001 From: jony376 Date: Fri, 3 Jul 2026 19:20:57 -0700 Subject: [PATCH 4/6] fix(miner): only enrich portfolio rows from manage_pr_update events Skip ledger events for PRs that are not currently queued or in progress so historical manage_pr_update rows do not appear in status output. Co-authored-by: Cursor --- .../gittensory-miner/lib/manage-status.js | 13 +------ test/unit/miner-manage-status.test.ts | 37 +++++++++++++------ 2 files changed, 28 insertions(+), 22 deletions(-) diff --git a/packages/gittensory-miner/lib/manage-status.js b/packages/gittensory-miner/lib/manage-status.js index 1135f51065..57d0d9520a 100644 --- a/packages/gittensory-miner/lib/manage-status.js +++ b/packages/gittensory-miner/lib/manage-status.js @@ -75,18 +75,9 @@ export function buildManageStatusSnapshot(readers) { const pullNumber = normalizePullNumber(payload.pullNumber); if (pullNumber === null) continue; const key = rowKey(event.repoFullName, pullNumber); - const existing = rows.get(key) ?? { - repoFullName: event.repoFullName, - pullNumber, - branch: null, - ciState: "unknown", - gateVerdict: null, - outcome: null, - lastPolledAt: null, - portfolioStatus: null, - }; + const existing = rows.get(key); + if (!existing) continue; mergeManageFields(existing, payload, event.createdAt); - rows.set(key, existing); } return [...rows.values()].sort((left, right) => { diff --git a/test/unit/miner-manage-status.test.ts b/test/unit/miner-manage-status.test.ts index f8da9f1d92..96c9f806e8 100644 --- a/test/unit/miner-manage-status.test.ts +++ b/test/unit/miner-manage-status.test.ts @@ -100,17 +100,7 @@ describe("manage status snapshot (#2325)", () => { listQueue: () => portfolio.listQueue(), readEvents: () => ledger.readEvents(), }); - expect(rows).toHaveLength(2); - expect(rows).toContainEqual({ - repoFullName: "JSONbored/gittensory", - pullNumber: 7, - branch: "feat/other", - ciState: "failure", - gateVerdict: "close", - outcome: "closed", - lastPolledAt: "2026-07-03T14:00:00.000Z", - portfolioStatus: null, - }); + expect(rows).toHaveLength(1); expect(rows).toContainEqual({ repoFullName: "acme/widgets", pullNumber: 42, @@ -130,6 +120,31 @@ describe("manage status snapshot (#2325)", () => { expect(table).toContain("merge"); }); + it("ignores manage_pr_update events for PRs no longer in the portfolio queue", () => { + const { portfolio, ledger } = tempStores(); + portfolio.enqueue({ repoFullName: "acme/widgets", identifier: "pr:42", priority: 1 }); + portfolio.markDone("acme/widgets", "pr:42"); + ledger.appendEvent({ + type: MANAGE_STATUS_EVENT_TYPE, + repoFullName: "acme/widgets", + payload: { + pullNumber: 42, + branch: "feat/done", + ciState: "success", + gateVerdict: "merge", + outcome: "merged", + lastPolledAt: "2026-07-03T15:00:00.000Z", + }, + }); + + expect( + buildManageStatusSnapshot({ + listQueue: () => portfolio.listQueue(), + readEvents: () => ledger.readEvents(), + }), + ).toEqual([]); + }); + it("emits stable JSON output shape", () => { const rows = [ { From 822912cc6dbc7a215cbb4a01a3cd21972e919c47 Mon Sep 17 00:00:00 2001 From: jony376 Date: Fri, 3 Jul 2026 19:30:14 -0700 Subject: [PATCH 5/6] fix(miner): restore scripts block in package.json after merge The merge dropped the scripts wrapper and left invalid JSON, which caused npm ci to fail in CI. Co-authored-by: Cursor --- packages/gittensory-miner/package.json | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/gittensory-miner/package.json b/packages/gittensory-miner/package.json index 4f535cc76e..9bddeee9b9 100644 --- a/packages/gittensory-miner/package.json +++ b/packages/gittensory-miner/package.json @@ -30,7 +30,9 @@ "bin", "lib" ], + "scripts": { "build": "node --check bin/gittensory-miner.js && node --check lib/cli.js && node --check lib/deny-check.js && node --check lib/run-state-cli.js && node --check lib/update-check.js && node --check lib/opportunity-fanout.js && node --check lib/ci-poller.js && node --check lib/run-state.js && node --check lib/deny-hooks.js && node --check lib/event-ledger.js && node --check lib/claim-ledger.js && node --check lib/portfolio-queue.js && node --check lib/opportunity-ranker.js && node --check lib/plan-store.js && node --check lib/rejection-templates.js && node --check lib/governor-ledger.js && node --check lib/status.js && node --check lib/manage-status.js" + }, "dependencies": { "@jsonbored/gittensory-engine": "0.1.0" }, From 6838abc99ad1684c736242f4b83f2fff15e68766 Mon Sep 17 00:00:00 2001 From: jony376 Date: Fri, 3 Jul 2026 19:37:29 -0700 Subject: [PATCH 6/6] ci: retrigger validation after package.json fix Co-authored-by: Cursor