Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions packages/gittensory-miner/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 manage status` reads the local portfolio
queue (`pr:<number>` 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:
Expand Down
18 changes: 18 additions & 0 deletions packages/gittensory-miner/bin/gittensory-miner.js
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,24 @@ if (cliArgs[0] === "state") {
process.exit(exitCode);
}

if (cliArgs[0] === "manage" && cliArgs[1] === "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(2));
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(`manage status failed: ${message}`);
await awaitOpportunisticUpdateCheck(updateCheck);
process.exit(1);
}
}

const exitCode = runCli(cliArgs, { packageName });
await awaitOpportunisticUpdateCheck(updateCheck);
process.exit(exitCode);
1 change: 1 addition & 0 deletions packages/gittensory-miner/lib/cli.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ export function printHelp(input) {
" gittensory-miner version",
" gittensory-miner status [--json] Show installed versions + local state paths",
" gittensory-miner doctor [--json] Check this laptop is set up correctly",
" gittensory-miner manage status [--json] Show managed PR portfolio + CI/gate status",
" gittensory-miner hooks check --tool <name> --input <json> [--json]",
" gittensory-miner state get <owner/repo> [--json]",
" gittensory-miner state set <owner/repo> <idle|discovering|planning|preparing> [--json]",
Expand Down
35 changes: 35 additions & 0 deletions packages/gittensory-miner/lib/manage-status.d.ts
Original file line number Diff line number Diff line change
@@ -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<string, unknown>;
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 };
150 changes: 150 additions & 0 deletions packages/gittensory-miner/lib/manage-status.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
/** 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);
if (!existing) continue;
mergeManageFields(existing, payload, event.createdAt);
}

return [...rows.values()].sort((left, right) => {
const repoCompare = left.repoFullName
.toLowerCase()
.localeCompare(right.repoFullName.toLowerCase(), "en");
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`;
}

const globalCliFlags = new Set(["--json", "--no-update-check"]);

export function parseManageStatusArgs(cliArgs) {
const json = cliArgs.includes("--json");
const positional = cliArgs.filter((arg) => !globalCliFlags.has(arg));
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 };
}
2 changes: 1 addition & 1 deletion packages/gittensory-miner/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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/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"
"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"
Expand Down
1 change: 1 addition & 0 deletions test/unit/miner-cli.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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");
});

Expand Down
Loading
Loading