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
51 changes: 51 additions & 0 deletions packages/gittensory-miner/lib/attempt-runner.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import type {
CodingAgentDriver,
GovernorChokepointInput,
GovernorDecision,
IterateLoopInput,
IterateLoopResult,
LocalWriteActionSpec,
} from "@jsonbored/gittensory-engine";
import type { HarnessSubmissionDecision, HarnessSubmissionEventLedger } from "./harness-submission-trigger.js";
import type { SubmissionFreshnessClaimLedger, LiveIssueSnapshot, FreshnessAbortReason } from "./submission-freshness-check.js";

export const ATTEMPT_OUTCOMES: readonly ["abandon", "stale", "blocked", "governed", "submitted"];

export type AttemptGovernorContext = Omit<GovernorChokepointInput, "actionClass" | "repoFullName" | "nowMs" | "wouldBeAction">;

export type AttemptInput = {
loopInput: IterateLoopInput;
issueNumber: number;
minerLogin: string;
base: string;
killSwitchScope: "global" | "repo" | "none";
slopThreshold: "clean" | "low" | "elevated" | "high";
submissionMode: "observe" | "enforce";
maxConsecutiveGateBlocks?: number;
draft?: boolean;
governor: AttemptGovernorContext;
};

export type AttemptDeps = {
driver: CodingAgentDriver;
runSlopAssessment: (input: unknown) => unknown;
appendAttemptLogEvent: (event: unknown) => void;
claimLedger: SubmissionFreshnessClaimLedger;
fetchLiveIssueSnapshot: (repoFullName: string, issueNumber: number) => Promise<LiveIssueSnapshot | null>;
eventLedger: HarnessSubmissionEventLedger;
/** Injected governor-ledger append (mirrors evaluateGovernorChokepointGate's own `options.append`); omitted
* falls back to that function's own default (the real default governor ledger). */
governorLedgerAppend?: (event: unknown) => unknown;
sessionStartMs?: number;
nowMs: number;
executeLocalWrite: (spec: LocalWriteActionSpec) => Promise<unknown>;
};

export type AttemptResult =
| { outcome: "abandon"; loopResult: IterateLoopResult }
| { outcome: "stale"; reason: FreshnessAbortReason; loopResult: IterateLoopResult }
| { outcome: "blocked"; decision: HarnessSubmissionDecision; loopResult: IterateLoopResult }
| { outcome: "governed"; decision: GovernorDecision; loopResult: IterateLoopResult }
| { outcome: "submitted"; spec: LocalWriteActionSpec; execResult: unknown; loopResult: IterateLoopResult };

export function runMinerAttempt(input: AttemptInput, deps: AttemptDeps): Promise<AttemptResult>;
156 changes: 156 additions & 0 deletions packages/gittensory-miner/lib/attempt-runner.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
import { buildOpenPrSpec } from "@jsonbored/gittensory-engine";
import { runIterateLoop } from "@jsonbored/gittensory-engine";
import { checkSubmissionFreshness } from "./submission-freshness-check.js";
import { evaluateGovernorChokepointGate } from "./governor-chokepoint.js";
import { prepareOpenPrSubmission } from "./harness-submission-trigger.js";

// The real driving-loop entrypoint (#2337): the missing link between #2333's iterate-loop orchestrator and an
// actual, executed open_pr write. Composes, in order: runIterateLoop (create -> score -> self-review -> decide,
// #2333) -> on handoff, checkSubmissionFreshness (#3007) -> prepareOpenPrSubmission (#2336/#2337) -> the
// Governor chokepoint (#2340, which itself composes kill-switch, dry-run, rate-limit, budget caps, non-
// convergence, self-reputation-throttle, and self-plagiarism -- see chokepoint.ts's own module doc comment for
// the exact precedence ladder) -> on allowed:true, builds the REAL open_pr command via the now-shared
// buildOpenPrSpec (@jsonbored/gittensory-engine, moved from root src/mcp/local-write-tools.ts) and executes it.
//
// WORKTREE LIFECYCLE IS NOT THIS MODULE'S JOB: runIterateLoop already takes a plain `workingDirectory` string
// (packages/gittensory-engine/src/miner/iterate-loop.ts's own IterateLoopInput), deliberately agnostic about
// where it came from. Allocating one is the caller's job, via the already-built slot allocator
// (worktree-allocator.js, #4297) -- this module composes the create/review/gate/submit sequence #2337 is
// actually about, not worktree allocation policy, which is a separate, already-solved concern.
//
// KNOWN, DELIBERATE GAPS (not silently papered over -- both were injected-but-unwired seams before this module
// existed, and remain so here):
// - `deps.runSlopAssessment` has no production implementation anywhere in this package. The real slop scorer
// (src/signals/slop.ts, 518 lines, 5 sibling src/signals/** dependencies) is far larger and more
// interconnected than local-write-tools.ts was, so extracting it is separate, substantial scope -- this
// function requires a real one be injected rather than silently stubbing a result that would either always
// pass (unsafe) or always fail (useless).
// - `input.governor`'s cross-attempt state (rate-limit buckets, budget-cap usage, convergence input,
// reputation history, self-plagiarism recent-submissions) has no persistence wiring anywhere in this
// package either -- every existing governor-*.js wrapper is a pure in/out transform over caller-supplied
// state (confirmed by reading governor-write-rate-limit.js: it returns an updated bucket store, but nothing
// persists it). A caller with no prior history should pass honest empty/zero defaults, not fabricated ones;
// durable cross-attempt tracking is portfolio-loop-level scope (the outer "process the queue" loop this
// issue does not build), not a single attempt's.

/** True once the loop reaches handoff AND every downstream gate (freshness, submission, governor) allows. */
export const ATTEMPT_OUTCOMES = Object.freeze(["abandon", "stale", "blocked", "governed", "submitted"]);

function assertFn(value, name) {
if (typeof value !== "function") throw new Error(`invalid_${name}`);
}

function assertDeps(deps) {
if (!deps || typeof deps !== "object") throw new Error("invalid_attempt_deps");
assertFn(deps.runSlopAssessment, "run_slop_assessment");
assertFn(deps.appendAttemptLogEvent, "append_attempt_log_event");
assertFn(deps.fetchLiveIssueSnapshot, "fetch_live_issue_snapshot");
assertFn(deps.executeLocalWrite, "execute_local_write");
if (!deps.driver || typeof deps.driver.run !== "function") throw new Error("invalid_driver");
if (!deps.claimLedger || typeof deps.claimLedger.listClaims !== "function") throw new Error("invalid_claim_ledger");
if (!deps.eventLedger || typeof deps.eventLedger.appendEvent !== "function") throw new Error("invalid_event_ledger");
if (typeof deps.nowMs !== "number" || !Number.isFinite(deps.nowMs)) throw new Error("invalid_now_ms");
}

function assertInput(input) {
if (!input || typeof input !== "object") throw new Error("invalid_attempt_input");
if (!input.loopInput || typeof input.loopInput !== "object") throw new Error("invalid_loop_input");
if (!Number.isInteger(input.issueNumber) || input.issueNumber < 1) throw new Error("invalid_issue_number");
if (typeof input.minerLogin !== "string" || !input.minerLogin.trim()) throw new Error("invalid_miner_login");
if (typeof input.base !== "string" || !input.base.trim()) throw new Error("invalid_base");
if (!["global", "repo", "none"].includes(input.killSwitchScope)) throw new Error("invalid_kill_switch_scope");
if (!["clean", "low", "elevated", "high"].includes(input.slopThreshold)) throw new Error("invalid_slop_threshold");
if (!["observe", "enforce"].includes(input.submissionMode)) throw new Error("invalid_submission_mode");
if (!input.governor || typeof input.governor !== "object") throw new Error("invalid_governor_context");
}

/**
* Run one full attempt end to end: iterate-loop -> (on handoff) freshness -> submission-gate -> Governor
* chokepoint -> (on allowed:true) build + execute the real open_pr command. Fails closed (throws) on malformed
* input/deps, mirroring every sibling module in this pipeline.
*
* @param {{
* loopInput: import("@jsonbored/gittensory-engine").IterateLoopInput,
* issueNumber: number,
* minerLogin: string,
* base: string,
* killSwitchScope: "global"|"repo"|"none",
* slopThreshold: "clean"|"low"|"elevated"|"high",
* submissionMode: "observe"|"enforce",
* maxConsecutiveGateBlocks?: number,
* draft?: boolean,
* governor: Omit<import("@jsonbored/gittensory-engine").GovernorChokepointInput, "actionClass"|"repoFullName"|"nowMs"|"wouldBeAction">,
* }} input
* @param {{
* driver: import("@jsonbored/gittensory-engine").CodingAgentDriver,
* runSlopAssessment: Function,
* appendAttemptLogEvent: Function,
* claimLedger: object,
* fetchLiveIssueSnapshot: Function,
* eventLedger: object,
* sessionStartMs?: number,
* nowMs: number,
* executeLocalWrite: (spec: import("@jsonbored/gittensory-engine").LocalWriteActionSpec) => Promise<unknown>,
* }} deps
*/
export async function runMinerAttempt(input, deps) {
assertInput(input);
assertDeps(deps);

const loopResult = await runIterateLoop(input.loopInput, {
driver: deps.driver,
runSlopAssessment: deps.runSlopAssessment,
appendAttemptLogEvent: deps.appendAttemptLogEvent,
});

if (loopResult.outcome === "abandon") {
return { outcome: "abandon", loopResult };
}

const handoffPacket = loopResult.handoffPacket;

const freshness = await checkSubmissionFreshness(
{ repoFullName: input.loopInput.repoFullName, issueNumber: input.issueNumber, minerLogin: input.minerLogin },
{ claimLedger: deps.claimLedger, fetchLiveIssueSnapshot: deps.fetchLiveIssueSnapshot, eventLedger: deps.eventLedger },
);
if (!freshness.fresh) {
return { outcome: "stale", reason: freshness.reason, loopResult };
}

const submission = await prepareOpenPrSubmission(
{
killSwitchScope: input.killSwitchScope,
repoFullName: input.loopInput.repoFullName,
handoffPacket,
slopThreshold: input.slopThreshold,
mode: input.submissionMode,
maxConsecutiveGateBlocks: input.maxConsecutiveGateBlocks,
base: input.base,
title: input.loopInput.title,
body: input.loopInput.body ?? "",
draft: input.draft,
},
{ eventLedger: deps.eventLedger, sessionStartMs: deps.sessionStartMs },
);
if (!submission.ready) {
return { outcome: "blocked", decision: submission.decision, loopResult };
}

const governed = evaluateGovernorChokepointGate(
{
actionClass: "open_pr",
repoFullName: input.loopInput.repoFullName,
nowMs: deps.nowMs,
wouldBeAction: submission.openPrInput,
...input.governor,
},
deps.governorLedgerAppend ? { append: deps.governorLedgerAppend } : {},
);
if (!governed.decision.allowed) {
return { outcome: "governed", decision: governed.decision, loopResult };
}

const spec = buildOpenPrSpec(submission.openPrInput);
const execResult = await deps.executeLocalWrite(spec);
return { outcome: "submitted", spec, execResult, loopResult };
}
2 changes: 1 addition & 1 deletion packages/gittensory-miner/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@
"expected-engine.version"
],
"scripts": {
"build": "node --check bin/gittensory-miner.js && node --check lib/attempt-log.js && node --check lib/calibration-types.js && node --check lib/calibration.js && node --check lib/ci-poller.js && node --check lib/claim-adjudication.js && node --check lib/claim-ledger-cli.js && node --check lib/claim-ledger-expiry.js && node --check lib/claim-ledger.js && node --check lib/cli.js && node --check lib/coding-agent-house-rules.js && node --check lib/deny-check.js && node --check lib/deny-hook-synthesis.js && node --check lib/deny-hooks.js && node --check lib/discover-cli.js && node --check lib/event-ledger-cli.js && node --check lib/event-ledger.js && node --check lib/feasibility-cli.js && node --check lib/gate-verdict-poller.js && node --check lib/governor-action-mode.js && node --check lib/governor-chokepoint.js && node --check lib/governor-kill-switch.js && node --check lib/governor-ledger-cli.js && node --check lib/governor-ledger.js && node --check lib/governor-open-pr.js && node --check lib/governor-run-halt.js && node --check lib/governor-write-rate-limit.js && node --check lib/harness-submission-trigger.js && node --check lib/laptop-init.js && node --check lib/local-store.js && node --check lib/loop-closure.js && node --check lib/loop-reentry.js && node --check lib/manage-poll.js && node --check lib/manage-status.js && node --check lib/opportunity-fanout.js && node --check lib/opportunity-ranker.js && node --check lib/orb-export.js && node --check lib/plan-store-cli.js && node --check lib/plan-store.js && node --check lib/portfolio-dashboard.js && node --check lib/portfolio-discovery.js && node --check lib/portfolio-queue-cli.js && node --check lib/portfolio-queue-manager.js && node --check lib/portfolio-queue.js && node --check lib/pr-outcome.js && node --check lib/prediction-ledger.js && node --check lib/pretooluse-hook.js && node --check lib/rejection-state-machine.js && node --check lib/rejection-templates.js && node --check lib/replay-objective-anchor.js && node --check lib/replay-task-generation.js && node --check lib/run-state-cli.js && node --check lib/run-state.js && node --check lib/status.js && node --check lib/submission-freshness-check.js && node --check lib/update-check.js && node --check lib/version.js && node --check lib/worktree-allocator.js"
"build": "node --check bin/gittensory-miner.js && node --check lib/attempt-log.js && node --check lib/attempt-runner.js && node --check lib/calibration-types.js && node --check lib/calibration.js && node --check lib/ci-poller.js && node --check lib/claim-adjudication.js && node --check lib/claim-ledger-cli.js && node --check lib/claim-ledger-expiry.js && node --check lib/claim-ledger.js && node --check lib/cli.js && node --check lib/coding-agent-house-rules.js && node --check lib/deny-check.js && node --check lib/deny-hook-synthesis.js && node --check lib/deny-hooks.js && node --check lib/discover-cli.js && node --check lib/event-ledger-cli.js && node --check lib/event-ledger.js && node --check lib/feasibility-cli.js && node --check lib/gate-verdict-poller.js && node --check lib/governor-action-mode.js && node --check lib/governor-chokepoint.js && node --check lib/governor-kill-switch.js && node --check lib/governor-ledger-cli.js && node --check lib/governor-ledger.js && node --check lib/governor-open-pr.js && node --check lib/governor-run-halt.js && node --check lib/governor-write-rate-limit.js && node --check lib/harness-submission-trigger.js && node --check lib/laptop-init.js && node --check lib/local-store.js && node --check lib/loop-closure.js && node --check lib/loop-reentry.js && node --check lib/manage-poll.js && node --check lib/manage-status.js && node --check lib/opportunity-fanout.js && node --check lib/opportunity-ranker.js && node --check lib/orb-export.js && node --check lib/plan-store-cli.js && node --check lib/plan-store.js && node --check lib/portfolio-dashboard.js && node --check lib/portfolio-discovery.js && node --check lib/portfolio-queue-cli.js && node --check lib/portfolio-queue-manager.js && node --check lib/portfolio-queue.js && node --check lib/pr-outcome.js && node --check lib/prediction-ledger.js && node --check lib/pretooluse-hook.js && node --check lib/rejection-state-machine.js && node --check lib/rejection-templates.js && node --check lib/replay-objective-anchor.js && node --check lib/replay-task-generation.js && node --check lib/run-state-cli.js && node --check lib/run-state.js && node --check lib/status.js && node --check lib/submission-freshness-check.js && node --check lib/update-check.js && node --check lib/version.js && node --check lib/worktree-allocator.js"
},
"dependencies": {
"@jsonbored/gittensory-engine": "*"
Expand Down
Loading