Skip to content

Commit 17de87a

Browse files
jony376cursoragent
andcommitted
feat(miner): add manage-phase status CLI command (#2325)
Add gittensory-miner manage status to render local portfolio pr:* rows enriched with manage_pr_update ledger events. Dispatch before the npm update check so the command stays read-only/offline, and skip ledger rows for PRs no longer in the portfolio queue. Co-authored-by: Cursor <cursoragent@cursor.com>
1 parent 8e9ca63 commit 17de87a

8 files changed

Lines changed: 211 additions & 4 deletions

File tree

packages/gittensory-miner/README.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,10 @@ The package also includes an append-only governor decision ledger: `initGovernor
2828
persist structured allow/deny/throttle/kill-switch outcomes in local SQLite for contributor audit. Insert-only —
2929
no enforcement wiring yet. (#2328)
3030

31+
The package also includes a manage-phase status renderer: `gittensory-miner manage status` reads the local portfolio
32+
queue (`pr:<number>` identifiers) plus `manage_pr_update` rows from the event ledger and prints CI/gate/outcome
33+
columns. Pass `--json` for machine-readable output. Read-only — no network and no writes. (#2325)
34+
3135
## Install
3236

3337
From a local checkout:

packages/gittensory-miner/bin/gittensory-miner.js

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,9 @@ import {
1212

1313
const cliArgs = process.argv.slice(2);
1414

15-
// `status` and `doctor` are strictly local, offline commands — their contract is to make NO network calls. Dispatch
16-
// them BEFORE the opportunistic npm-registry update check is even started, so they can never reach that network
17-
// path (the update check runs for the remaining commands below).
15+
// `status`, `doctor`, and `manage status` are strictly local, offline commands — their contract is to make NO
16+
// network calls. Dispatch them BEFORE the opportunistic npm-registry update check is even started, so they can
17+
// never reach that network path (the update check runs for the remaining commands below).
1818
if (cliArgs[0] === "status") {
1919
process.exit(runStatus(cliArgs.slice(1)));
2020
}
@@ -23,6 +23,22 @@ if (cliArgs[0] === "doctor") {
2323
process.exit(runDoctor(cliArgs.slice(1)));
2424
}
2525

26+
if (cliArgs[0] === "manage" && cliArgs[1] === "status") {
27+
const { listQueue } = await import("../lib/portfolio-queue.js");
28+
const { readEvents } = await import("../lib/event-ledger.js");
29+
const { parseManageStatusArgs, runManageStatus } = await import("../lib/manage-status.js");
30+
try {
31+
const options = parseManageStatusArgs(cliArgs.slice(2));
32+
const result = runManageStatus({ listQueue, readEvents }, options);
33+
process.stdout.write(result.output);
34+
process.exit(result.exitCode);
35+
} catch (error) {
36+
const message = error instanceof Error ? error.message : "manage_status_failed";
37+
console.error(`manage status failed: ${message}`);
38+
process.exit(1);
39+
}
40+
}
41+
2642
const require = createRequire(import.meta.url);
2743
const packageName = "@jsonbored/gittensory-miner";
2844
const packageVersion = require("../package.json").version;

packages/gittensory-miner/lib/cli.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ export function printHelp(input) {
1616
" gittensory-miner version",
1717
" gittensory-miner status [--json] Show installed versions + local state paths",
1818
" gittensory-miner doctor [--json] Check this laptop is set up correctly",
19+
" gittensory-miner manage status [--json] Show managed PR portfolio + CI/gate status",
1920
" gittensory-miner hooks check --tool <name> --input <json> [--json]",
2021
" gittensory-miner state get <owner/repo> [--json]",
2122
" gittensory-miner state set <owner/repo> <idle|discovering|planning|preparing> [--json]",
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
export declare const MANAGE_STATUS_EVENT_TYPE: "manage_pr_update";
2+
3+
export type ManageStatusRow = {
4+
repoFullName: string;
5+
pullNumber: number;
6+
branch: string | null;
7+
ciState: string | null;
8+
gateVerdict: string | null;
9+
outcome: string | null;
10+
lastPolledAt: string | null;
11+
portfolioStatus: string | null;
12+
};
13+
14+
export type ManageStatusReaders = {
15+
listQueue(): ReadonlyArray<{
16+
repoFullName: string;
17+
identifier: string;
18+
status: string;
19+
}>;
20+
readEvents(): ReadonlyArray<{
21+
type: string;
22+
repoFullName: string | null;
23+
payload: Record<string, unknown>;
24+
createdAt: string;
25+
}>;
26+
};
27+
28+
export function buildManageStatusSnapshot(readers: ManageStatusReaders): ManageStatusRow[];
29+
export function formatManageStatusJson(rows: ManageStatusRow[]): string;
30+
export function formatManageStatusTable(rows: ManageStatusRow[]): string;
31+
export function parseManageStatusArgs(cliArgs: string[]): { json: boolean };
32+
export function runManageStatus(
33+
readers: ManageStatusReaders,
34+
options?: { json?: boolean },
35+
): { rows: ManageStatusRow[]; output: string; exitCode: number };
Lines changed: 150 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,150 @@
1+
/** Manage-phase event vocabulary — later phases append these to the local event ledger (#2325). */
2+
export const MANAGE_STATUS_EVENT_TYPE = "manage_pr_update";
3+
4+
const portfolioPrIdentifierPattern = /^pr:(\d+)$/;
5+
6+
function parsePortfolioPullNumber(identifier) {
7+
if (typeof identifier !== "string") return null;
8+
const match = portfolioPrIdentifierPattern.exec(identifier.trim());
9+
if (!match) return null;
10+
const pullNumber = Number(match[1]);
11+
return Number.isInteger(pullNumber) && pullNumber > 0 ? pullNumber : null;
12+
}
13+
14+
function rowKey(repoFullName, pullNumber) {
15+
return `${repoFullName}#${pullNumber}`;
16+
}
17+
18+
function normalizeOptionalString(value) {
19+
if (value === undefined || value === null) return null;
20+
if (typeof value !== "string") return null;
21+
const trimmed = value.trim();
22+
return trimmed ? trimmed : null;
23+
}
24+
25+
function normalizePullNumber(value) {
26+
if (!Number.isInteger(value) || value <= 0) return null;
27+
return value;
28+
}
29+
30+
function mergeManageFields(target, payload, eventCreatedAt) {
31+
const pullNumber = normalizePullNumber(payload?.pullNumber);
32+
if (pullNumber !== null) target.pullNumber = pullNumber;
33+
const branch = normalizeOptionalString(payload?.branch);
34+
if (branch !== null) target.branch = branch;
35+
const ciState = normalizeOptionalString(payload?.ciState);
36+
if (ciState !== null) target.ciState = ciState;
37+
const gateVerdict = normalizeOptionalString(payload?.gateVerdict);
38+
if (gateVerdict !== null) target.gateVerdict = gateVerdict;
39+
const outcome = normalizeOptionalString(payload?.outcome);
40+
if (outcome !== null) target.outcome = outcome;
41+
const lastPolledAt =
42+
normalizeOptionalString(payload?.lastPolledAt) ?? normalizeOptionalString(eventCreatedAt);
43+
if (lastPolledAt !== null) target.lastPolledAt = lastPolledAt;
44+
}
45+
46+
/**
47+
* Aggregate manage-phase rows from the portfolio queue and append-only event ledger. Pure read/render input —
48+
* no network calls and no writes (#2325).
49+
*/
50+
export function buildManageStatusSnapshot(readers) {
51+
const rows = new Map();
52+
53+
for (const item of readers.listQueue()) {
54+
if (item.status === "done") continue;
55+
const pullNumber = parsePortfolioPullNumber(item.identifier);
56+
if (pullNumber === null) continue;
57+
const key = rowKey(item.repoFullName, pullNumber);
58+
rows.set(key, {
59+
repoFullName: item.repoFullName,
60+
pullNumber,
61+
branch: null,
62+
ciState: item.status === "in_progress" ? "unknown" : "unknown",
63+
gateVerdict: null,
64+
outcome: null,
65+
lastPolledAt: null,
66+
portfolioStatus: item.status,
67+
});
68+
}
69+
70+
for (const event of readers.readEvents()) {
71+
if (event.type !== MANAGE_STATUS_EVENT_TYPE) continue;
72+
if (!event.repoFullName) continue;
73+
const payload = event.payload;
74+
if (!payload || typeof payload !== "object" || Array.isArray(payload)) continue;
75+
const pullNumber = normalizePullNumber(payload.pullNumber);
76+
if (pullNumber === null) continue;
77+
const key = rowKey(event.repoFullName, pullNumber);
78+
const existing = rows.get(key);
79+
if (!existing) continue;
80+
mergeManageFields(existing, payload, event.createdAt);
81+
}
82+
83+
return [...rows.values()].sort((left, right) => {
84+
const repoCompare = left.repoFullName
85+
.toLowerCase()
86+
.localeCompare(right.repoFullName.toLowerCase(), "en");
87+
if (repoCompare !== 0) return repoCompare;
88+
return left.pullNumber - right.pullNumber;
89+
});
90+
}
91+
92+
export function formatManageStatusJson(rows) {
93+
return `${JSON.stringify({ rows }, null, 2)}\n`;
94+
}
95+
96+
function pad(value, width) {
97+
const text = String(value ?? "");
98+
return text.length >= width ? text : `${text}${" ".repeat(width - text.length)}`;
99+
}
100+
101+
export function formatManageStatusTable(rows) {
102+
if (rows.length === 0) {
103+
return "No managed pull requests in the local portfolio.\n";
104+
}
105+
const headers = [
106+
"repo",
107+
"pr",
108+
"branch",
109+
"ci",
110+
"gate",
111+
"outcome",
112+
"last_polled_at",
113+
];
114+
const widths = [28, 6, 24, 10, 10, 10, 24];
115+
const lines = [
116+
headers.map((header, index) => pad(header, widths[index])).join(" "),
117+
widths.map((width) => "-".repeat(width)).join(" "),
118+
];
119+
for (const row of rows) {
120+
lines.push(
121+
[
122+
pad(row.repoFullName, widths[0]),
123+
pad(row.pullNumber, widths[1]),
124+
pad(row.branch ?? "-", widths[2]),
125+
pad(row.ciState ?? "unknown", widths[3]),
126+
pad(row.gateVerdict ?? "-", widths[4]),
127+
pad(row.outcome ?? "-", widths[5]),
128+
pad(row.lastPolledAt ?? "-", widths[6]),
129+
].join(" "),
130+
);
131+
}
132+
return `${lines.join("\n")}\n`;
133+
}
134+
135+
const globalCliFlags = new Set(["--json", "--no-update-check"]);
136+
137+
export function parseManageStatusArgs(cliArgs) {
138+
const json = cliArgs.includes("--json");
139+
const positional = cliArgs.filter((arg) => !globalCliFlags.has(arg));
140+
if (positional.length > 0) {
141+
throw new Error(`unexpected_arguments:${positional.join(",")}`);
142+
}
143+
return { json };
144+
}
145+
146+
export function runManageStatus(readers, options = {}) {
147+
const rows = buildManageStatusSnapshot(readers);
148+
const output = options.json ? formatManageStatusJson(rows) : formatManageStatusTable(rows);
149+
return { rows, output, exitCode: 0 };
150+
}

packages/gittensory-miner/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@
3131
"lib"
3232
],
3333
"scripts": {
34-
"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"
34+
"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"
3535
},
3636
"dependencies": {
3737
"@jsonbored/gittensory-engine": "0.1.0"

test/unit/miner-cli.test.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,7 @@ describe("gittensory-miner CLI helpers", () => {
6464
const text = log.mock.calls[0]?.[0];
6565
expect(text).toContain("gittensory-miner --help");
6666
expect(text).toContain("gittensory-miner version");
67+
expect(text).toContain("gittensory-miner status");
6768
expect(text).toContain("--no-update-check");
6869
});
6970

14.2 KB
Binary file not shown.

0 commit comments

Comments
 (0)