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
17 changes: 12 additions & 5 deletions packages/gittensory-engine/src/miner/iterate-loop.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,8 @@
// pretooluse-hook append-failure handling elsewhere in this package).

import type { CodingAgentDriver, CodingAgentDriverResult, CodingAgentDriverTask } from "./coding-agent-driver.js";
import type { CodingAgentExecutionMode } from "./coding-agent-mode.js";
import { codingAgentModeExecutes, type CodingAgentExecutionMode } from "./coding-agent-mode.js";
import { invokeCodingAgentDriver } from "./coding-agent-invoke.js";
import type { AttemptLogEvent, AttemptLogEventType } from "./attempt-log.js";
import { runSelfReview, type AttemptDiffState, type SelfReviewAdapterDeps, type SelfReviewContext, type SelfReviewVerdict } from "./self-review-adapter.js";
import { decideNextActionWithReason, deriveSelfReviewOutcome, type IterateLoopDecision, type HandoffPacket, type IterationState, type SelfReviewOutcome } from "./iterate-policy.js";
Expand Down Expand Up @@ -139,10 +140,16 @@ function evaluateSelfReviewOutcome(input: IterateLoopInput, driverResult: Coding
}

/** A thrown driver error is normalized into the same `{ ok: false }` shape a driver returning gracefully would
* produce, so {@link evaluateSelfReviewOutcome} has exactly one failure path to handle, not two. */
async function runDriverSafely(driver: CodingAgentDriver, task: CodingAgentDriverTask): Promise<CodingAgentDriverResult> {
* produce, so {@link evaluateSelfReviewOutcome} has exactly one failure path to handle, not two. Non-live modes
* are also resolved here, at the driver boundary, so paused/dry-run attempts never spawn the underlying agent. */
async function runDriverSafely(input: IterateLoopInput, deps: IterateLoopDeps, task: CodingAgentDriverTask): Promise<CodingAgentDriverResult> {
if (!codingAgentModeExecutes(input.mode)) {
return invokeCodingAgentDriver(deps.driver, input.mode, task, {
append: (event) => safeAppendAttemptLogEvent(deps, event),
});
}
try {
return await driver.run(task);
return await deps.driver.run(task);
} catch (error) {
return { ok: false, changedFiles: [], summary: "", error: `driver_threw: ${error instanceof Error ? error.message : String(error)}` };
}
Expand Down Expand Up @@ -257,7 +264,7 @@ export async function runIterateLoop(input: IterateLoopInput, deps: IterateLoopD
let totalTurnsUsed = 0;

for (let iterationNumber = 1; iterationNumber <= maxIterations; iterationNumber += 1) {
const driverResult = await runDriverSafely(deps.driver, {
const driverResult = await runDriverSafely(input, deps, {
attemptId: input.attemptId,
workingDirectory: input.workingDirectory,
acceptanceCriteriaPath: input.acceptanceCriteriaPath,
Expand Down
28 changes: 28 additions & 0 deletions packages/gittensory-engine/test/iterate-loop.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,34 @@ test("immediate abandon: maxIterations <= 0 abandons before ever invoking the dr
assert.equal(events[0]?.eventType, "attempt_aborted");
});

test("paused mode: does not invoke the coding-agent driver (regression for pause gate bypass)", async () => {
let driverCalled = false;
const { deps, events } = collectingDeps({ driver: { async run() { driverCalled = true; return okResult(); } } });
const result = await runIterateLoop(passingInput({ mode: "paused", maxIterations: 1 }), deps);

assert.equal(driverCalled, false, "paused is a kill switch and must not call driver.run");
assert.equal(result.outcome, "abandon");
assert.equal(result.finalDecision.abandonReason, "self_review_ambiguous");
assert.equal(result.iterationsUsed, 1);
assert.equal(result.totalTurnsUsed, 0);
assert.equal(result.iterations[0]?.driverResult.error, "coding_agent_paused");
assert.equal(events.some((event) => event.eventType === "attempt_aborted" && event.actionClass === "codegen" && event.reason === "coding_agent_paused"), true);
});

test("dry_run mode: records a shadow coding-agent invocation without calling the driver (regression for dry-run gate bypass)", async () => {
let driverCalled = false;
const { deps, events } = collectingDeps({ driver: { async run() { driverCalled = true; return okResult(); } } });
const result = await runIterateLoop(passingInput({ mode: "dry_run", maxIterations: 1 }), deps);

assert.equal(driverCalled, false, "dry-run must not spawn the underlying coding-agent session");
assert.equal(result.outcome, "handoff");
assert.equal(result.iterationsUsed, 1);
assert.equal(result.totalTurnsUsed, 0);
assert.equal(result.iterations[0]?.driverResult.ok, true);
assert.match(result.iterations[0]?.driverResult.summary ?? "", /dry-run: would invoke coding agent/);
assert.equal(events.some((event) => event.eventType === "attempt_shadow" && event.actionClass === "codegen" && event.mode === "dry_run"), true);
});

test("handoff: a clean predicted-gate pass on the first iteration hands off, with a full HandoffPacket", async () => {
const { deps, events } = collectingDeps({ driver: driverReturning(okResult(["src/upload.ts"], 5)) });
const result = await runIterateLoop(passingInput({ maxIterations: 3 }), deps);
Expand Down