Skip to content
21 changes: 12 additions & 9 deletions packages/gittensory-miner/lib/attempt-cli.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
// query (attempt-log.js's schema has no repo+issue index, and reenqueue counts aren't tracked anywhere yet).

import { resolveCodingAgentModeFromConfig } from "@jsonbored/gittensory-engine";
import { argsWantJson, describeCliError, reportCliFailure } from "./cli-error.js";
import { constructProductionCodingAgentDriver } from "./coding-agent-construction.js";
import { runSlopAssessment } from "./slop-assessment.js";
import { fetchLiveIssueSnapshot } from "./live-issue-snapshot.js";
Expand Down Expand Up @@ -139,8 +140,7 @@ export function buildAttemptDeps(env, ledgers) {
export async function runAttempt(args, options = {}) {
const parsed = parseAttemptArgs(args);
if ("error" in parsed) {
console.error(parsed.error);
return 2;
return reportCliFailure(argsWantJson(args), parsed.error);
}

const env = options.env ?? process.env;
Expand All @@ -149,10 +149,11 @@ export async function runAttempt(args, options = {}) {
const mode = resolveMode({ env, agentDryRun: !parsed.live });

if (mode === "paused") {
console.error(
return reportCliFailure(
parsed.json,
`Coding-agent execution is globally paused (MINER_CODING_AGENT_PAUSED). Not running attempt for ${parsed.repoFullName}#${parsed.issueNumber}.`,
3,
);
return 3;
}

const attemptId = options.attemptId ?? `${parsed.repoFullName.replace("/", "_")}-${parsed.issueNumber}-${nowMs}`;
Expand Down Expand Up @@ -222,9 +223,12 @@ export async function runAttempt(args, options = {}) {
const buildDeps = options.buildAttemptDeps ?? buildAttemptDeps;
deps = buildDeps(env, { claimLedger, eventLedger, attemptLog, governorLedger, nowMs });
} catch (error) {
const reason = error instanceof Error ? error.message : String(error);
console.error(`Attempt for ${parsed.repoFullName}#${parsed.issueNumber} is blocked: ${reason}`);
return 3;
const reason = describeCliError(error);
return reportCliFailure(
parsed.json,
`Attempt for ${parsed.repoFullName}#${parsed.issueNumber} is blocked: ${reason}`,
3,
);
}

// Real worktree preparation (repo-clone.js + attempt-worktree.js, #5237): the allocator above only
Expand Down Expand Up @@ -461,8 +465,7 @@ export async function runAttempt(args, options = {}) {
return 2;
}
} catch (error) {
console.error(error instanceof Error ? error.message : String(error));
return 2;
return reportCliFailure(parsed.json, describeCliError(error));
} finally {
// worktreeResult.attemptOk is set to the REAL runMinerAttempt outcome (submitted = true) once that call
// happens; every earlier blocked path (rejection/worktree-prep-failure/infeasible) never sets it, since
Expand Down
16 changes: 10 additions & 6 deletions packages/gittensory-miner/lib/calibration-cli.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { buildCalibrationReport } from "./calibration.js";
import { initEventLedger, resolveEventLedgerDbPath } from "./event-ledger.js";
import { MINER_PR_OUTCOME_EVENT } from "./pr-outcome.js";
import { initPredictionLedger, resolvePredictionLedgerDbPath } from "./prediction-ledger.js";
import { reportCliFailure, describeCliError } from "./cli-error.js";

const CALIBRATION_USAGE = "Usage: gittensory-miner calibration [--json]";

Expand Down Expand Up @@ -67,22 +68,25 @@ export function runCalibrationCli(args = [], env = process.env) {
const json = args.includes("--json");
const unknown = args.find((token) => token.startsWith("-") && token !== "--json");
if (unknown) {
console.error(`Unknown option: ${unknown}. ${CALIBRATION_USAGE}`);
return 1;
return reportCliFailure(json, `Unknown option: ${unknown}. ${CALIBRATION_USAGE}`, 1);
}

const predictionStore = initPredictionLedger(resolvePredictionLedgerDbPath(env));
const eventLedger = initEventLedger(resolveEventLedgerDbPath(env));
let predictionStore;
let eventLedger;
try {
predictionStore = initPredictionLedger(resolvePredictionLedgerDbPath(env));
eventLedger = initEventLedger(resolveEventLedgerDbPath(env));
const report = buildCalibrationReport(
toPredictionRecords(predictionStore.readPredictions()),
toOutcomeRecords(eventLedger.readEvents()),
);
if (json) console.log(JSON.stringify(report, null, 2));
else renderReportText(report);
return 0;
} catch (error) {
return reportCliFailure(json, describeCliError(error));
} finally {
predictionStore.close();
eventLedger.close();
predictionStore?.close();
eventLedger?.close();
}
}
25 changes: 9 additions & 16 deletions packages/gittensory-miner/lib/claim-ledger-cli.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { CLAIM_STATUSES, openClaimLedger } from "./claim-ledger.js";
import { argsWantJson, describeCliError, reportCliFailure } from "./cli-error.js";

const CLAIM_CLAIM_USAGE =
"Usage: gittensory-miner claim claim <owner/repo> <issue#> [--note <text>] [--json]";
Expand Down Expand Up @@ -183,8 +184,7 @@ function withClaimLedger(options, run) {
export function runClaimClaim(args, options = {}) {
const parsed = parseClaimClaimArgs(args);
if ("error" in parsed) {
console.error(parsed.error);
return 2;
return reportCliFailure(argsWantJson(args), parsed.error);
}

try {
Expand All @@ -202,24 +202,21 @@ export function runClaimClaim(args, options = {}) {
return 0;
});
} catch (error) {
console.error(error instanceof Error ? error.message : String(error));
return 2;
return reportCliFailure(parsed.json, describeCliError(error));
}
}

export function runClaimRelease(args, options = {}) {
const parsed = parseClaimReleaseArgs(args);
if ("error" in parsed) {
console.error(parsed.error);
return 2;
return reportCliFailure(argsWantJson(args), parsed.error);
}

try {
return withClaimLedger(options, (claimLedger) => {
const claim = claimLedger.releaseClaim(parsed.repoFullName, parsed.issueNumber);
if (!claim) {
console.error("claim_not_found");
return 2;
return reportCliFailure(parsed.json, "claim_not_found");
}
if (parsed.json) {
console.log(JSON.stringify({ claim }, null, 2));
Expand All @@ -229,16 +226,14 @@ export function runClaimRelease(args, options = {}) {
return 0;
});
} catch (error) {
console.error(error instanceof Error ? error.message : String(error));
return 2;
return reportCliFailure(parsed.json, describeCliError(error));
}
}

export function runClaimList(args, options = {}) {
const parsed = parseClaimListArgs(args);
if ("error" in parsed) {
console.error(parsed.error);
return 2;
return reportCliFailure(argsWantJson(args), parsed.error);
}

try {
Expand All @@ -255,15 +250,13 @@ export function runClaimList(args, options = {}) {
return 0;
});
} catch (error) {
console.error(error instanceof Error ? error.message : String(error));
return 2;
return reportCliFailure(parsed.json, describeCliError(error));
}
}

export function runClaimCli(subcommand, args, options = {}) {
if (subcommand === "claim") return runClaimClaim(args, options);
if (subcommand === "release") return runClaimRelease(args, options);
if (subcommand === "list") return runClaimList(args, options);
console.error(`Unknown claim subcommand: ${subcommand ?? ""}. ${CLAIM_LIST_USAGE}`);
return 2;
return reportCliFailure(argsWantJson(args), `Unknown claim subcommand: ${subcommand ?? ""}. ${CLAIM_LIST_USAGE}`);
}
3 changes: 3 additions & 0 deletions packages/gittensory-miner/lib/cli-error.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export function reportCliFailure(wantsJson: boolean, message: string, exitCode?: number): number;
export function argsWantJson(args: readonly string[]): boolean;
export function describeCliError(error: unknown): string;
27 changes: 27 additions & 0 deletions packages/gittensory-miner/lib/cli-error.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
/** Shared CLI failure output (#4836): when `--json` is set, emit a parseable `{ ok: false, error }` object on
* stdout (matching each command's success-path JSON stream); otherwise log plain text to stderr. */

/**
* @param {boolean} wantsJson
* @param {string} message
* @param {number} [exitCode]
* @returns {number}
*/
export function reportCliFailure(wantsJson, message, exitCode = 2) {
if (wantsJson) {
console.log(JSON.stringify({ ok: false, error: message }, null, 2));
} else {
console.error(message);
}
return exitCode;
}

/** True when argv includes `--json` (used on parse-error paths before a full parse result exists). */
export function argsWantJson(args) {
return args.includes("--json");
}

/** Normalize a thrown value to a safe error string for CLI output. */
export function describeCliError(error) {
return error instanceof Error ? error.message : String(error);
}
6 changes: 4 additions & 2 deletions packages/gittensory-miner/lib/cli.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { argsWantJson, reportCliFailure } from "./cli-error.js";

export function printVersion(input) {
console.log(`${input.packageName}/${input.packageVersion} (node ${process.version})`);
}
Expand Down Expand Up @@ -56,6 +58,6 @@ export function printHelp(input) {

export function runCli(cliArgs, input) {
const command = cliArgs[0] ?? "";
console.error(`Unknown command: ${command}. Run ${input.packageName} --help.`);
return 1;
const message = `Unknown command: ${command}. Run ${input.packageName} --help.`;
return reportCliFailure(argsWantJson(cliArgs), message, 1);
}
4 changes: 2 additions & 2 deletions packages/gittensory-miner/lib/deny-check.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { evaluateDenyHooks } from "./deny-hooks.js";
import { argsWantJson, reportCliFailure } from "./cli-error.js";

const DENY_CHECK_USAGE =
"Usage: gittensory-miner hooks check --tool <name> --input <json> [--json]";
Expand Down Expand Up @@ -59,8 +60,7 @@ export function parseDenyCheckArgs(args) {
export function runDenyCheck(args) {
const parsed = parseDenyCheckArgs(args);
if ("error" in parsed) {
console.error(parsed.error);
return 2;
return reportCliFailure(argsWantJson(args), parsed.error);
}

const verdict = evaluateDenyHooks({ name: parsed.tool, input: parsed.input });
Expand Down
16 changes: 10 additions & 6 deletions packages/gittensory-miner/lib/discover-cli.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { initPolicyDocCacheStore } from "./policy-doc-cache.js";
import { initPolicyVerdictCacheStore } from "./policy-verdict-cache.js";
import { enqueueRankedDiscovery } from "./portfolio-discovery.js";
import { initPortfolioQueueStore } from "./portfolio-queue.js";
import { argsWantJson, describeCliError, reportCliFailure } from "./cli-error.js";

const DISCOVER_USAGE =
"Usage: gittensory-miner discover <owner/repo> [<owner/repo>...] | --search <query> [--json] [--api-base-url <url>] [--token-env <VAR>]";
Expand Down Expand Up @@ -138,8 +139,7 @@ export function renderDiscoverSummary(result) {
export async function runDiscover(args, options = {}) {
const parsed = parseDiscoverArgs(args);
if ("error" in parsed) {
console.error(parsed.error);
return 2;
return reportCliFailure(argsWantJson(args), parsed.error);
}

// Credential env var is per-tenant (#4784): a `--token-env FORGE_PAT` flag (or `options.tokenEnv`) reads a
Expand All @@ -157,7 +157,12 @@ export async function runDiscover(args, options = {}) {
const enqueue = options.enqueueRankedDiscovery ?? enqueueRankedDiscovery;

const ownsPortfolioQueue = options.initPortfolioQueue === undefined;
const portfolioQueue = (options.initPortfolioQueue ?? initPortfolioQueueStore)();
let portfolioQueue;
try {
portfolioQueue = (options.initPortfolioQueue ?? initPortfolioQueueStore)();
} catch (error) {
return reportCliFailure(parsed.json, describeCliError(error));
}

// Local ETag cache so a repeated discover revalidates each repo's policy docs with a conditional GET instead of
// re-downloading them (#4842). Opened inside its OWN try/catch, separate from the portfolio queue above: the
Expand Down Expand Up @@ -222,10 +227,9 @@ export async function runDiscover(args, options = {}) {
}
return 0;
} catch (error) {
console.error(error instanceof Error ? error.message : String(error));
return 2;
return reportCliFailure(parsed.json, describeCliError(error));
} finally {
if (ownsPortfolioQueue) portfolioQueue.close();
if (ownsPortfolioQueue && portfolioQueue) portfolioQueue.close();
if (ownsPolicyDocCache && policyDocCache) policyDocCache.close();
if (ownsPolicyVerdictCache && policyVerdictCache) policyVerdictCache.close();
}
Expand Down
16 changes: 6 additions & 10 deletions packages/gittensory-miner/lib/event-ledger-cli.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { initEventLedger } from "./event-ledger.js";
import { argsWantJson, describeCliError, reportCliFailure } from "./cli-error.js";

const LEDGER_LIST_USAGE =
"Usage: gittensory-miner ledger list [--repo <owner/repo>] [--since <seq>] [--type <eventType>] [--json]";
Expand Down Expand Up @@ -217,8 +218,7 @@ function withEventLedger(options, run) {
export function runLedgerList(args, options = {}) {
const parsed = parseLedgerListArgs(args);
if ("error" in parsed) {
console.error(parsed.error);
return 2;
return reportCliFailure(argsWantJson(args), parsed.error);
}

try {
Expand All @@ -238,15 +238,13 @@ export function runLedgerList(args, options = {}) {
return 0;
});
} catch (error) {
console.error(error instanceof Error ? error.message : String(error));
return 2;
return reportCliFailure(parsed.json, describeCliError(error));
}
}

export function runLedgerMetrics(args, options = {}) {
if (args.length > 0) {
console.error(EVENT_LEDGER_METRICS_USAGE);
return 2;
return reportCliFailure(argsWantJson(args), EVENT_LEDGER_METRICS_USAGE);
}

try {
Expand All @@ -257,14 +255,12 @@ export function runLedgerMetrics(args, options = {}) {
return 0;
});
} catch (error) {
console.error(error instanceof Error ? error.message : String(error));
return 2;
return reportCliFailure(argsWantJson(args), describeCliError(error));
}
}

export function runLedgerCli(subcommand, args, options = {}) {
if (subcommand === "list") return runLedgerList(args, options);
if (subcommand === "metrics") return runLedgerMetrics(args, options);
console.error(`Unknown ledger subcommand: ${subcommand ?? ""}. ${LEDGER_LIST_USAGE}`);
return 2;
return reportCliFailure(argsWantJson(args), `Unknown ledger subcommand: ${subcommand ?? ""}. ${LEDGER_LIST_USAGE}`);
}
4 changes: 2 additions & 2 deletions packages/gittensory-miner/lib/feasibility-cli.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
* `buildFeasibilityVerdict` composer. Purely local — no network, no filesystem — so it never needs the
* npm-registry update check other subcommands opt into. */
import { buildFeasibilityVerdict } from "@jsonbored/gittensory-engine";
import { argsWantJson, reportCliFailure } from "./cli-error.js";

const CLAIM_STATUSES = ["unclaimed", "claimed", "solved", "unknown"];
const DUPLICATE_CLUSTER_RISKS = ["none", "low", "medium", "high"];
Expand Down Expand Up @@ -59,8 +60,7 @@ export function parseFeasibilityArgs(args) {
export function runFeasibilityCli(args, options = {}) {
const parsed = parseFeasibilityArgs(args);
if ("error" in parsed) {
console.error(parsed.error);
return 2;
return reportCliFailure(argsWantJson(args), parsed.error);
}

const buildVerdict = options.buildFeasibilityVerdict ?? buildFeasibilityVerdict;
Expand Down
11 changes: 5 additions & 6 deletions packages/gittensory-miner/lib/governor-ledger-cli.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
/** Must match `GOVERNOR_LEDGER_EVENT_TYPES` in `@jsonbored/gittensory-engine`. */
import { argsWantJson, describeCliError, reportCliFailure } from "./cli-error.js";

const GOVERNOR_LEDGER_EVENT_TYPES = Object.freeze([
"allowed",
"denied",
Expand Down Expand Up @@ -109,8 +111,7 @@ async function withGovernorLedger(options, run) {
export async function runGovernorList(args, options = {}) {
const parsed = parseGovernorListArgs(args);
if ("error" in parsed) {
console.error(parsed.error);
return 2;
return reportCliFailure(argsWantJson(args), parsed.error);
}

try {
Expand All @@ -129,13 +130,11 @@ export async function runGovernorList(args, options = {}) {
return 0;
});
} catch (error) {
console.error(error instanceof Error ? error.message : String(error));
return 2;
return reportCliFailure(parsed.json, describeCliError(error));
}
}

export async function runGovernorCli(subcommand, args, options = {}) {
if (subcommand === "list") return runGovernorList(args, options);
console.error(`Unknown governor subcommand: ${subcommand ?? ""}. ${GOVERNOR_LIST_USAGE}`);
return 2;
return reportCliFailure(argsWantJson(args), `Unknown governor subcommand: ${subcommand ?? ""}. ${GOVERNOR_LIST_USAGE}`);
}
Loading