feat: committer distinguishes recoverable vs unrecoverable failures

CommitterMeta is now a 3-way discriminated union:
- committed: success with branch + commitSha
- recoverable: coder can fix (hook failures, lint, test, conflicts)
- unrecoverable: can't be fixed by code (auth, permissions, disk)

Moderator routes recoverable → coder for retry.
This commit is contained in:
2026-05-06 10:53:17 +00:00
parent 267ca73a1b
commit 196562c82a
3 changed files with 17 additions and 6 deletions
@@ -68,7 +68,7 @@ describe("createCommitterRole", () => {
test("returns failed meta when extraction reports failure", async () => { test("returns failed meta when extraction reports failure", async () => {
const failed = { const failed = {
status: "failed" as const, status: "recoverable" as const,
error: "working tree clean; nothing to commit", error: "working tree clean; nothing to commit",
logRef: null as string | null, logRef: null as string | null,
}; };
@@ -91,7 +91,7 @@ describe("createCommitterRole", () => {
test("returns failed meta with logRef when extraction includes it", async () => { test("returns failed meta with logRef when extraction includes it", async () => {
const failed = { const failed = {
status: "failed" as const, status: "recoverable" as const,
error: "push rejected", error: "push rejected",
logRef: "LOGREF01", logRef: "LOGREF01",
}; };
@@ -125,7 +125,7 @@ describe("createCommitterRole", () => {
const out = await role(makeCtx()); const out = await role(makeCtx());
expect(out.meta).toEqual({ expect(out.meta).toEqual({
status: "failed", status: "unrecoverable",
error: "committer role threw before structured result", error: "committer role threw before structured result",
logRef: null, logRef: null,
}); });
@@ -15,7 +15,12 @@ export const committerMetaSchema = z.discriminatedUnion("status", [
commitSha: z.string(), commitSha: z.string(),
}), }),
z.object({ z.object({
status: z.literal("failed"), status: z.literal("recoverable"),
error: z.string(),
logRef: z.string().nullable(),
}),
z.object({
status: z.literal("unrecoverable"),
error: z.string(), error: z.string(),
logRef: z.string().nullable(), logRef: z.string().nullable(),
}), }),
@@ -54,7 +59,10 @@ Create a branch, commit the changes, and push. Report whether the push succeeded
## On failure ## On failure
If any git operation fails (hook rejection, push denied, merge conflict, etc.), **do not attempt to fix it yourself**. Your job is to capture the key error output and report it back clearly so other roles in the workflow can address it.`; If any git operation fails, **do not attempt to fix it yourself**. Capture the key error output and classify it:
- **Recoverable**: failures that a coder can fix (lint/test hook rejection, merge conflict, commit validation errors)
- **Unrecoverable**: failures beyond code changes (no push permission, remote not found, authentication denied, disk full)`;
} }
/** /**
@@ -83,7 +91,7 @@ export function createCommitterRole(
onFail<CommitterMeta>({ onFail<CommitterMeta>({
label: "committer", label: "committer",
meta: { meta: {
status: "failed", status: "unrecoverable",
error: "committer role threw before structured result", error: "committer role threw before structured result",
logRef: null, logRef: null,
}, },
@@ -31,6 +31,9 @@ export const solveIssueModerator: Moderator<SolveIssueMeta> = (ctx) => {
} }
if (last.role === "committer") { if (last.role === "committer") {
if (last.meta.status === "recoverable" && ctx.steps.length < maxRounds - 1) {
return "coder";
}
return END; return END;
} }