Skip to content
Merged
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
7 changes: 7 additions & 0 deletions packages/gittensory-miner/bin/gittensory-miner.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { runDiscover } from "../lib/discover-cli.js";
import { runFeasibilityCli } from "../lib/feasibility-cli.js";
import { runGovernorCli } from "../lib/governor-ledger-cli.js";
import { runLedgerCli } from "../lib/event-ledger-cli.js";
import { runLoop } from "../lib/loop-cli.js";
import { runManagePoll } from "../lib/manage-poll.js";
import { runManageStatus } from "../lib/manage-status.js";
import { runPlanCli } from "../lib/plan-store-cli.js";
Expand Down Expand Up @@ -133,6 +134,12 @@ if (cliArgs[0] === "attempt") {
process.exit(exitCode);
}

if (cliArgs[0] === "loop") {
const exitCode = await runLoop(cliArgs.slice(1));
await awaitOpportunisticUpdateCheck(updateCheck);
process.exit(exitCode);
}

const exitCode = runCli(cliArgs, { packageName });
await awaitOpportunisticUpdateCheck(updateCheck);
process.exit(exitCode);
40 changes: 38 additions & 2 deletions packages/gittensory-miner/lib/attempt-cli.d.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import type { CodingAgentExecutionMode } from "@jsonbored/gittensory-engine";
import type { AttemptDeps, runMinerAttempt } from "./attempt-runner.js";
import type { CodingAgentExecutionMode, FeasibilityVerdict, LocalWriteActionSpec } from "@jsonbored/gittensory-engine";
import type { AttemptDeps, AttemptResult as RunMinerAttemptResult, runMinerAttempt } from "./attempt-runner.js";
import type { ClaimLedger } from "./claim-ledger.js";
import type { EventLedger } from "./event-ledger.js";
import type { AttemptLog } from "./attempt-log.js";
Expand All @@ -12,6 +12,39 @@ import type { buildCodingTaskSpec } from "./coding-task-spec.js";
import type { resolveAmsPolicy } from "./ams-policy.js";
import type { checkMinerKillSwitch } from "./governor-kill-switch.js";

type CommonAttemptResultFields = {
repoFullName: string;
issueNumber: number;
minerLogin: string;
base: string;
mode: CodingAgentExecutionMode;
attemptId: string;
};

/** The result runAttempt reports at every real return point, threaded to `options.onResult` (in addition to
* the plain exit-code return runAttempt itself still returns, unchanged, so bin/gittensory-miner.js's own
* `process.exit(exitCode)` usage never breaks) -- the loop orchestrator's real caller for this data. */
export type AttemptCliResult =
| (CommonAttemptResultFields & { outcome: "blocked_rejection_signaled"; reason: string })
| (CommonAttemptResultFields & { outcome: "blocked_worktree_preparation_failed"; reason: string })
| (CommonAttemptResultFields & {
outcome: "blocked_infeasible";
reason: string;
verdict: FeasibilityVerdict;
avoidReasons: string[];
raiseReasons: string[];
})
| (CommonAttemptResultFields & {
outcome: `attempt_${RunMinerAttemptResult["outcome"]}`;
submissionMode: "observe" | "enforce";
totalTurnsUsed: number;
iterationsUsed: number;
reason?: string;
decision?: unknown;
spec?: LocalWriteActionSpec;
execResult?: unknown;
});

export type ParsedAttemptArgs =
| { error: string }
| { repoFullName: string; issueNumber: number; minerLogin: string; base: string; live: boolean; json: boolean };
Expand Down Expand Up @@ -43,6 +76,9 @@ export type RunAttemptOptions = {
resolveAmsPolicy?: typeof resolveAmsPolicy;
checkMinerKillSwitch?: typeof checkMinerKillSwitch;
runMinerAttempt?: typeof runMinerAttempt;
/** Invoked with the real structured result at every return point, in addition to (never instead of) the
* plain exit-code return -- the loop orchestrator's real hook into what actually happened. */
onResult?: (result: AttemptCliResult) => void;
};

export function runAttempt(args: string[], options?: RunAttemptOptions): Promise<number>;
10 changes: 10 additions & 0 deletions packages/gittensory-miner/lib/attempt-cli.js
Original file line number Diff line number Diff line change
Expand Up @@ -207,6 +207,7 @@ export async function runAttempt(args, options = {}) {
`Attempt for ${parsed.repoFullName}#${parsed.issueNumber} is blocked: this repo's AI-usage policy bans automated/AI-authored contributions.`,
);
}
options.onResult?.(rejectedResult);
return 5;
}

Expand Down Expand Up @@ -259,6 +260,7 @@ export async function runAttempt(args, options = {}) {
} else {
console.error(`Attempt for ${parsed.repoFullName}#${parsed.issueNumber} is blocked: real worktree preparation failed: ${reason}`);
}
options.onResult?.(worktreeFailureResult);
return 6;
}

Expand Down Expand Up @@ -325,6 +327,7 @@ export async function runAttempt(args, options = {}) {
`Attempt for ${parsed.repoFullName}#${parsed.issueNumber} is blocked: feasibility verdict "${codingTaskSpec.verdict}" (${[...codingTaskSpec.feasibility.avoidReasons, ...codingTaskSpec.feasibility.raiseReasons].join(", ")}).`,
);
}
options.onResult?.(infeasibleResult);
return 4;
}

Expand Down Expand Up @@ -371,16 +374,23 @@ export async function runAttempt(args, options = {}) {
mode,
attemptId,
submissionMode: amsPolicy.spec.submissionMode,
// Every runMinerAttempt outcome carries a real loopResult (#5135's loop needs its genuine turn-usage to
// save real GovernorCapUsage via governor-state.js's saveCapUsage -- nothing else in the codebase calls
// it yet). Surfaced flat rather than the whole loopResult object, matching this result's own shallow shape.
totalTurnsUsed: result.loopResult.totalTurnsUsed,
iterationsUsed: result.loopResult.iterationsUsed,
...("reason" in result ? { reason: result.reason } : {}),
...("decision" in result ? { decision: result.decision } : {}),
...("spec" in result ? { spec: result.spec } : {}),
...("execResult" in result ? { execResult: result.execResult } : {}),
};

if (parsed.json) {
console.log(JSON.stringify(finalResult, null, 2));
} else {
console.log(`Attempt for ${parsed.repoFullName}#${parsed.issueNumber} finished with outcome: ${result.outcome}.`);
}
options.onResult?.(finalResult);

switch (result.outcome) {
case "submitted":
Expand Down
3 changes: 3 additions & 0 deletions packages/gittensory-miner/lib/cli.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,9 @@ export function printHelp(input) {
" gittensory-miner discover <owner/repo> [<owner/repo>...] [--json]",
" gittensory-miner discover --search <query> [--json] Fan out, rank, and enqueue candidates",
" gittensory-miner attempt <owner/repo> <issue#> --miner-login <login> [--base <branch>] [--live] [--json]",
" gittensory-miner loop <owner/repo> [<owner/repo>...] --miner-login <login> [--base <branch>] [--live]",
" gittensory-miner loop --search <query> --miner-login <login> [--max-cycles <n>] [--cycle-delay-ms <ms>] [--json]",
" Autonomous discover->claim->attempt->reenter loop",
" gittensory-miner queue list [--repo <owner/repo>] [--json] List portfolio backlog rows",
" gittensory-miner queue next [--json] Claim the highest-priority queued item",
" gittensory-miner queue claim-batch [--global-wip <n>] [--per-repo-wip <n>] [--json]",
Expand Down
61 changes: 61 additions & 0 deletions packages/gittensory-miner/lib/loop-cli.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import type { AttemptCliResult } from "./attempt-cli.js";
import type { PortfolioQueueStore } from "./portfolio-queue.js";
import type { GovernorState } from "./governor-state.js";
import type { EventLedger } from "./event-ledger.js";
import type { GovernorLedger } from "./governor-ledger.js";
import type { RunStateStore } from "./run-state.js";
import type { PollPrDispositionOptions } from "./pr-disposition-poller.js";

export type ParsedLoopArgs =
| { error: string }
| {
targets: string[];
search: string | null;
minerLogin: string;
base: string;
live: boolean;
maxCycles: number | undefined;
cycleDelayMs: number;
json: boolean;
};

export function parseLoopArgs(args: string[]): ParsedLoopArgs;

export type LoopCycleSummary = {
cycle: number;
outcome: "idle_queue_empty" | "halted" | "attempted" | "skipped_malformed_identifier";
reason?: string;
repoFullName?: string;
identifier?: string;
attemptOutcome?: AttemptCliResult["outcome"] | "attempt_error";
reentryOutcome?: "merged" | "disengaged" | "other";
prNumber?: number | null;
reentered?: boolean;
reasons?: string[];
};

export type RunLoopOptions = {
env?: Record<string, string | undefined>;
nowMs?: number;
githubToken?: string;
apiBaseUrl?: string;
sleepFn?: (delayMs: number) => Promise<void>;
openGovernorState?: () => GovernorState;
initEventLedger?: () => EventLedger;
initGovernorLedger?: () => GovernorLedger;
initPortfolioQueue?: () => PortfolioQueueStore;
initRunStateStore?: () => RunStateStore;
runDiscover?: (args: string[], options?: Record<string, unknown>) => Promise<number>;
runAttempt?: (args: string[], options?: Record<string, unknown>) => Promise<number>;
resolveAmsPolicy?: (repoFullName: string, options?: Record<string, unknown>) => Promise<{ spec: Record<string, unknown>; source: string; warnings: string[] }>;
checkMinerKillSwitch?: (input?: { env?: Record<string, string | undefined>; repoPaused?: boolean }) => { scope: "global" | "repo" | "none"; active: boolean };
evaluateRunLoopBoundaryGate?: (input: unknown, options?: unknown) => { verdict: { reason: string }; canClaimNext: boolean };
pollPrDisposition?: (repoFullName: string, prNumber: number, options?: PollPrDispositionOptions) => Promise<{ state: "open" | "closed"; merged: boolean; closedAt: string | null; attempts: number }>;
recordPrOutcomeSnapshot?: (input: unknown, options?: unknown) => unknown;
buildLoopClosureSummary?: (sources: unknown, options?: unknown) => { sinceSeq: number | null; lastSeq: number };
attemptLoopReentry?: (candidate: unknown, deps: unknown) => { decision: { reenter: boolean; reasons: string[] }; dequeued: { repoFullName: string; identifier: string; priority: number; status: string; enqueuedAt: string } | null };
attemptOptions?: Record<string, unknown>;
prDispositionOptions?: PollPrDispositionOptions;
};

export function runLoop(args: string[], options?: RunLoopOptions): Promise<number>;
Loading