Pipeline Roles Implementor - axolotl-logic/project-buddy GitHub Wiki

Implementor role: spec (draft)

Draft from integration c1797bd (2026-10-16). It describes today's code; adjust it as the split settles. Index: Pipeline roles.

Purpose

The implementor turns a spec into commits on the issue's branch and keeps that branch healthy until merge. It builds the change, fixes review findings, rebases onto the base, and fixes failing checks. Proposed (finalizer-role, refined by sync-at-edges): the implementor rebases onto the base only at the start of each entry, before any edit, and resolves the conflicts that rebase finds. After implementation, the finalizer's rebase in finalize is the only other sync. Every model session it runs can write code. It never merges.

Proposed (spec-writes-failing-tests, implementor-reads-wiki):

  • it starts from the spec writer's stubs and failing tests, and its job is to make them pass;
  • it may write tests of its own (Axlog changed her mind, 2026-10-16), but the spec writer's tests must still pass as written;
  • it reads the wiki but doesn't write it;
  • it opens no PR and never edits one (spec-writer-opens-pr). The spec writer creates the branch and opens the PR.

Scope today

  • Stages:
    • implement (stages/implement.ts, the implementer half of build-replies.ts).
    • open-pr (stages/open-pr.ts, flow-step.ts, src/workflow/flows.ts). Proposed (spec-writer-opens-pr): the stage goes; implement moves the issue straight to review.
    • The fixer half of fix (stages/fix.ts).
    • sync (proposed: no stage of its own; its states run at the start of each implementor entry, with the conflict resolver, and the finalizer rebases in finalize, sync-at-edges) (stages/sync.ts, sync-conflict.ts, conflict-resolver.ts, conflict-memo.ts, conflict-target.ts, stacked.ts).
    • checks (stages/checks.ts, checks-round.ts, checks-reply.ts, ci-fix.ts, ci-fixer.ts).
    • Continuations for every writer (stages/continuation.ts).
  • Ops: push, open-pr, close-pr (src/ops/pr.ts; proposed: open-pr and close-pr move to the spec writer); sync-base, finish-sync (src/ops/sync.ts, src/rebase.ts, src/stack.ts); verify (src/ops/verify.ts, fails-before-fix.ts); ci-status; wiki-check.
  • Sessions, all agent issue-pipeline-writer:
    • implementer: Opus, replies with IMPL_SCHEMA.
    • fixer: Opus, replies with FIX_SCHEMA.
    • conflict resolver: replies with CIFIX_SCHEMA.
    • CI fixer: Opus, replies with CIFIX_SCHEMA.
  • Job role: writer for implement, open-pr, fix, sync and checks (proposed: implement, fix and checks, since sync has no stage of its own, sync-at-edges).

Session reply schemas

From src/workflow/schemas.ts and review-schemas.ts. Every object is closed (additionalProperties: false) and every field is required. The stages check replies by hand (build-replies.ts, review-replies.ts, checks-reply.ts).

// IMPL_SCHEMA: the implementer's reply (stages/implement.ts).
{
  status: "done" | "handoff" | "blocked";
  summary: string;      // logged; posted as the stop's detail when "blocked"
  prTitle: string;      // becomes the PR title, keeping "(#N)"; proposed: dropped
  prBody: string;       // becomes the PR body, keeping "Closes #N"; proposed: dropped
}
// Proposed (`spec-writer-opens-pr`): IMPL_SCHEMA drops prTitle and prBody, because the spec writer's
// PR carries the title and body, derived from the issue and the spec.
// Built (#414) for the role: src/roles/implementor/reply.ts has its own closed
// IMPL_SCHEMA, { status, summary }. The shared one in src/workflow/schemas.ts
// stays as it is for the lanes' implement stage.

// FIX_SCHEMA: the fixer's reply (stages/fix.ts).
{
  status: "done" | "handoff";
  fixed: string[];      // ids of the findings it fixed, such as "f1"
  disputed: Array<{ id: string; evidence: string }>; // each finding at most once
  summary: string;
}

// CIFIX_SCHEMA: the CI fixer's reply (stages/ci-fixer.ts) and the conflict
// resolver's (stages/conflict-resolver.ts).
{
  status: "fixed" | "cannot-fix" | "handoff";
  summary: string;      // a resolver says here what it chose where sides contradicted
}

Changed from today:

  • Built (#414): the role's reader, implReadingOf in src/roles/implementor/reply.ts, parses with zod and ignores any other field, so a reply that still carries testsAdded, prTitle or prBody (an older prompt, or the job's reply for a session it stopped at contextLimits.hard) reads as its status and summary.
  • tests-added-derived (Axlog, 2026-10-16): IMPL_SCHEMA drops testsAdded. The tests a change adds or edits are derived from its diff: the files changed since the base that match the config's testPathGlobs, which fails-before-fix already computes. The implementer doesn't report them. Today the field is required, implReplyOf checks it, and no stage reads it.

For review:

  • cannot-fix means different things in the two sessions that share CIFIX_SCHEMA. From the CI fixer, it means no change in the repo can fix the failure, such as an outside service's outage, missing secrets or credentials, or a fix that needs Axlog (unrelated-fixes-allowed). From the conflict resolver, it means the rebase couldn't be finished, and the issue retries next sweep.
  • FIX_SCHEMA has no blocked, so a fixer can only fix, dispute or hand off.

Lifecycle (proposed: state machine, role-state-machines)

The implementor runs as one state machine covering its four stages. The shared rules (the table and transition, effects, the runner, restarts, budget) are in Shared. Wiki access: read in every stage (implementor-reads-wiki), in the job's docs/, at the issue's wiki branch, or at the wiki's base when that branch doesn't exist; the bridge that gave implement, fix and checks write is gone (#334). Code access: write in every stage, in repo/ (#338). Sessions commit only through pipeline_commit, which pushes the code, and refuses a docs/ path in a stage that only reads the wiki.

Every job starts with claim (not ours → end released) and checkout: the issue's branch at its pushed head, or the prerequisite's when stacked, at or past specTestsSha. It then enters at the state for the issue's stage. Proposed (sync-at-edges): the start sync (below) runs first, before any edit, in an implement entry, a fix entry, and a job that resumes an interrupted implementor entry (a killed job, a crash, a rate-limit deferral or an orchestrator restart). It runs nowhere else: not while an entry runs, not between its continuation sessions (a planned handoff at contextLimits.soft or hard is not an interruption), and not in a checks entry, unless that entry resumes an interrupted one. A checks job that starts again after waiting on CI (held) is not resuming an interruption, so it doesn't sync. Built (implementor-one-job, #424): one job runs fix → checks, with no sync between them (a sync runs only at an entry's start), and its job.json names the stage it is at now. next(stage) saves the issue at the stage and carries on in the same job; saved(stage) ends it. So a fix job runs until the issue reaches accept, a stop, or waits on CI (held). An implement job ends at review, since review is the reviewer's.

data: continuations, fixRounds, conflictFixes, ciFixRounds, specReturns (limits from the config: maxContinuations, maxFixRounds, maxConflictFixes, the checks round limits, and a new back-to-spec limit), plus reply, findings and verifiedSha.

From the finalizer (proposed: finalizer-role; deferred, roles-6-10-scope: for now the finalizer saves a rebase conflict at sync and a failed retest at checks). The finalizer sends the issue to fix with findings of two new kinds, and a note on the PR that each writer's task includes:

  • conflict: the finalizer's rebase onto the base conflicted. The fix entry's start sync runs the rebase (sync-base) and the conflict resolver, then pushes, resolving the finding;
  • retest: a test, lint, typecheck, lockfile or CI failure at the rebased head. The CI fixer fixes it.

Either way the code changed, so the job goes on to checks, whose check-fix review covers the fix, then accept and security run again, and the issue returns to finalize. It also sends the issue to fix when a blocking finding is open at its head. Findings from the final security review (proposed: final-security-review; deferred, roles-6-10-scope) arrive the same way as any security finding, and the fixed code goes back through checks, accept, security and finalize.

Locate (every-role-resumes). Every start, after a crash too, works out what is done (Shared). The issue's saved stage picks the entry, and the entry's …-work? check skips a stage already done. Beyond that:

  • implement: when the branch already has a code change since specTestsSha and the spec tests pass, it goes straight to flows; otherwise the implementer runs again, on a branch that holds its pushed commits;
  • fix: the fixer runs again with the open findings; findings already resolved are in the saved state;
  • sync (proposed: the start sync, sync-at-edges): a resumed entry runs the start sync again, so an interrupted rebase starts over from the pushed head;
  • checks: verify and ci-status only read, so they run again.

Checkpoints saved in the issue's state: continuations, fixRounds, conflictFixes, ciFixRounds and specReturns, each saved when it goes up.

Built (#414): the machine's layout and the implement entry. src/roles/implementor/ holds the machine. Each entry's states are their own module (start.ts for the shared start, implement.ts for the implement entry), and machine.ts only spreads them into TABLE, so a later entry (#418 fix, #419 the start sync, #420 checks) adds one module and one line. implementor.ts holds IMPLEMENTOR_STAGES (["implement"] in #414, ["implement", "fix"] since #418, and ["implement", "fix", "sync"] since #419) and the Machine; registry.ts registers every stage in that list in one line, so job.mjs --stage implement runs the role while the lanes' workflow.js keeps stages/implement.ts and stages/open-pr.ts. data has every field above but specReturns (back to spec is deferred, roles-6-10-scope). The role imports nothing from the triage, spec writer or reviewer roles.

  • locate is pure: it starts every run at start, reading the checkpoints back. Its continuations is the checkpoint once the role saved one, and otherwise the older state's count through continuationsOf(saved, "implement"), so an issue saved by today's implement stage (its continuations a number or { implement: n }, its phase decide) resumes with its count.
  • start is a check that answers the entry's name. Since #419 every entry goes on to the start sync, which leads to the entry's first state.
  • "Straight to flows" is the implement-work? check's built result, since only the job's checkout can see the branch: a code change since specTestsSha (the spec draft's removal aside) skips the implementer. "And the spec tests pass" is deferred with verify's spec-test run (roles-6-10-scope).

Entry implement

State Kind Does Result → next
start script (#414) answers the run's entry implement, fix or sync → sync-work?, the start sync (#419)
implement-work? script a spec whose reply said wikiOnly has no code to build: the spec writer sends wiki-only issues straight to review, so this only guards a stale implement (#391). Built (#414): a code change on the job's checkout since specTestsSha, leaving out the spec draft, is work already done work → setup; built → flows; none → end saved(review)
setup script commands.setup in the job's checkout, logged; a failure is only logged, as at the claim, since verify runs it again ok → implementer
implementer session the implementer, or a continuation with handoff.md replied → implementer-reply
implementer-reply script (#414) the session's outcome is always replied, so this check routes on the reply's status done → flows; handoff, continuations < max → implementer; handoff at max → end stopped(too-large); blocked naming a spec test, call shape or wiki page, specReturns < max → end saved(spec) with the reason (deferred, roles-6-10-scope: built in #414 as other blocked); other blocked → end stopped(not-doable) with the summary; a reply that doesn't read → end stopped(error)
flows script push, saving headSha, then flowsOf(codeChanged, wikiAhead), and the spec's tests at the head (deferred) code change → end saved(review), or saved(open-pr) for an issue with no PR saved, implemented: true, the draft PR untouched; no code change → end stopped(not-doable), "the implementer changed nothing"

Entry fix

State Kind Does Result → next
fix-work? script open blocking findings? (the start sync has already resolved a conflict finding). Built (#418): any open finding is work; retest findings come with #420 none → next(checks) (#424); open, fixRounds < max → fixer; open at max → end stopped with the findings' stop reason
fixer session the fixer (issue-pipeline-writer, FIX_SCHEMA), with the open findings, the spec path and the practices, or a continuation (fix-rN-cM) with handoff.md replied → fixer-reply
fixer-reply script (#418) records each dispute once, posts the fixer's comment on the PR, and routes on the reply a disputed acceptance finding → end stopped(acceptance), before the push; done → fix-flows; handoff, fixContinuations < max → fixer; handoff at max → end stopped(too-large); a reply that doesn't read → end stopped(error)
fix-flows script (#418) push, saving headSha and the flows, then prFlowAction go → prepare-rereview; reopen (code on a wiki-only issue whose PR is closed) → end saved(open-pr), whose open-pr op reopens the PR; stop or close (no code left) → end stopped(not-doable)
prepare-rereview script prepare-review since reviewedSha, tagged rN (sS-rN after a security review), on the stack ok → rereview-correctness?
rereview-<lens>? script (#418) one per lens, in ALL_LENSES order: has the lens an open finding? open → rereview-<lens>; none → the next lens's check, after the last update-findings
rereview-<lens> session the lens's re-reviewer (issue-pipeline-reader, the lens's model, REREVIEW_SCHEMA or REREVIEW_SEC_SCHEMA), named rereview-<lens>-rN, with the fix diff, the resolved history and the lens's open findings replied → the next lens's check, after the last update-findings
update-findings script applies the verdicts in lens order and posts each re-reviewer's comment all resolved → next(checks) (#424), with reviewedSha the head, reviewedPatchId from prepare-review and testsOnly cleared, saved at checks before the checks entry starts, so a job killed between them resumes at checks; open, fixRounds < max → fixer; open at max → end stopped with the findings' stop reason; a reintroduced finding → end stopped(fix-loop); a re-reviewer's reply that doesn't read → end stopped(error)

Built (#418): the fix entry, in src/roles/implementor/fix.ts, joined in machine.ts with one line; IMPLEMENTOR_STAGES is ["implement", "fix"], so job.mjs --stage fix runs the role while the lanes keep stages/fix.ts and stages/rereview.ts.

  • The runner runs one session per state, so the re-review is a chain of one check and one session per lens rather than one state launching every lens. The re-reviewers are sessions of this entry, not a reviewer job, and the entry imports nothing from src/roles/reviewer/.
  • fixRounds is a checkpoint, guarded by maxFixRounds on the edges into fixer. A round counts when it begins, as in today's stage, so a half-done round is a used one. locate reads the higher of the checkpoint and today's round, so a state saved by today's fix stage, mid-round included, resumes with its count at fix-work?. The role also saves round, so the lanes see the same count. The review and the security review zero both when they send an issue to fix; acceptance doesn't, so its rounds count toward the limit.
  • The fixer's continuations are their own checkpoint, fixContinuations, apart from the implementer's: the higher of it and today's continuations.fix.
  • No state sends a PR op: a reopen ends at open-pr, whose stage passes reopen for an issue whose PR is closed.

Start sync (sync-at-edges, the start of every entry, before any edit). Built (#419), in src/roles/implementor/sync.ts with its checks in sync-checks.ts, its ends in sync-ends.ts, its fields in sync-data.ts and the resolver's task in resolver-task.ts. It runs in the implement, fix and sync entries. A checks entry (#420) brings its own rule for a job that starts again after waiting on CI.

State Kind Does Result → next
sync-work? script does the branch contain the target's head (the base, or the prerequisite's head while it is open, as syncTarget in src/stack.ts decides)? yes → the entry's first state, no sync-base and no comment; no → rebase
rebase script sync-base with keepConflict, which pushes a clean rebase itself clean → the entry's first state; conflict, conflictFixes < max → resolver, clearing testsOnly and passedSha; at max → abort-conflicts
resolver session the conflict resolver, for the stopped commit: today's writer with the fixer's model and budget and CIFIX_SCHEMA. It commits the resolution with pipeline_commit and leaves the rebase to the role replied → resolver-reply; a failed session → abort-failed
resolver-reply script the reply's status fixed → continue; cannot-fix → abort-cannot-fix; handoff, the resolver's own continuations < maxContinuations → resolver; at max → abort-out-of-room; a reply that doesn't read → abort-failed
continue script GIT_EDITOR=true git rebase --continue in the job's checkout clean → finish-sync; next conflict, conflictFixes < max → resolver; at max → abort-conflicts
finish-sync script finish-sync checks the rebase against the base it stopped on (or the prerequisite) and pushes resolved → the entry's first state (implement-work?, fix-work?, or the sync entry's sync-moved?); rejected → end released, or stopped(conflict) on the last attempt
abort-… script finish-sync with abort, which resets the checkout to the pushed branch after cannot-fix → end released, the issue left at its stage (conflicts-never-stop); after abort-conflicts → end stopped(conflict); after abort-out-of-room → end stopped(too-large); after abort-failed → end stopped(error)
  • Departures from the design above: a clean rebase skips finish-sync, since sync-base has already pushed it. The resolver commits and the role continues the rebase, so each conflicting commit is one conflictFixes; today's resolver finishes the whole rebase. The over-limit paths are their own abort-… states, since a guard's over-limit target is a state.
  • A start sync never touches syncs. Its resolutions count toward maxConflictFixes, which locate reads from the checkpoint, or else from the top-level conflictFixes today's sync stage saved.
  • A sync that moves the head sets the syncedHead checkpoint to the pushed head, so the run saves the state, with its headSha, as soon as the sync ends. A job held by a rate limit or killed after the sync leaves the issue at the head on GitHub, and the next claim doesn't find the branch changed outside the pipeline.
  • A rebase of a head with no code change since specTestsSha moves specTestsSha to the rebased head, and the role hands specTestsSha off, so implement-work? never counts the base's changes as the implementer's work, in that job or a later one. A branch rebased outside the start sync keeps its old specTestsSha.
  • A transient provider error in the resolver holds the run. The job's checkout, with the rebase still in it, is dropped, since every job clones afresh, and the release puts conflictFixes back.

Entry sync (built, #419). The start sync, then today's sync outcomes, for a conflict at merge, a stacked issue whose prerequisite moved, and an issue saved at sync by today's stage, with a rebase in progress or not. IMPLEMENTOR_STAGES is ["implement", "fix", "sync"], so job.mjs --stage sync runs the role while the lanes keep stages/sync.ts.

State Kind Does Result → next
sync-moved? script did the sync change the head? moved, syncs < maxSyncs → end saved(checks), counting it; moved at max → end stopped(sync-limit); same → end saved(checks)
  • Every end of the sync entry sets testsOnly when passedSha was the head before the sync (a clean rebase of a passed head, or nothing to do), and clears passedSha. A resolved conflict clears both, so every check runs again.
  • A branch that already has its target's head saves at checks without counting.

Entry checks

Built (#420). roles/implementor/checks-entry.ts holds the entry's states and IMPLEMENTOR_STAGES is ["implement", "checks"], so job.mjs --stage checks runs the role; the lanes' stages/checks*.ts, ci-fix.ts and ci-fixer.ts stay for the sweep. locate takes the entry from the saved stage and reads back verifiedSha, verifySummary, testsOnly, reviewedSha, stackedOn, flows and pr; an issue saved by today's checks stage keeps its ciFixes (as ciFixRounds) and its continuations.ciFix. wiki-check, verify and ci-status are check states: the job's check body runs the op (with its own timeout: 120 minutes for verify, ciWaitMinutes + 10 for the CI wait, through the op effect's minutes), posts the verify and CI comments through op post, and answers a result name. The built states:

State Kind Does Result → next
checks-work? check head == verifiedSha on a code change? verified → ci-status (a verified head may still wait on CI, as checks-round.ts does); work → wiki-check
wiki-check check op wiki-check, only when flows.wiki pass → verify; wiki-only → end saved(accept) with verifiedSha, skipping verify and CI; fail → ci-fixer
verify check op verify (number, testsOnly, stackedOn), the checks' table on the PR pass → ci-status, saving verifiedSha and verifySummary (clipped to 1500); fail → ci-fixer; moved → end stopped(branch-changed)
ci-status check op ci-status on pr at the head green → end saved(accept), or saved(merge) when testsOnly held, clearing it; pending → end held; fail → ci-fixer; moved → end stopped(branch-changed); no-checks → end stopped(no-ci)
ci-fixer session the CI fixer, guarded by ciFixRounds < maxCiFixes, else end stopped(check-fixes), "checks still fail after N CI-fix attempts" replied → ci-fixer-reply
ci-fixer-reply check the fixer's status fixed → fix-push; handoff → ci-fixer up to maxContinuations, then end stopped(too-large); cannot-fix → end stopped(checks-unrelated); unreadable → end stopped(error)
fix-push op push, with the fixer's summary on the PR ok → prepare-fix-review
prepare-fix-review op prepare-review since reviewedSha, tag cifix<N> ok → check-fix-correctness
check-fix-correctness, check-fix-scope session the correctness and scope lenses on the fix's diff, one after the other (the reviewer's tasks and REVIEW_SCHEMA, imported from workflow/, not from the reviewer role) replied → the next lens, then check-fix-review
check-fix-review check posts each lens's review on the PR clean → wiki-check, saving reviewedSha; blocking → ci-fixer, counted in ciFixRounds; unreadable → end stopped(error)

A reviewer's transient error holds the job through the runner. The role sends no PR op: a fix that empties the code flow is recorded in flows and the next wiki-check takes the wiki-only path. Retest findings and the spec-test rerun at specTestsSha are not built.

The proposed table it was built from:

State Kind Does Result → next
checks-work? script is the head verifiedSha, with nothing it was verified against moved, and no open retest finding? yes → ci-status (#420; this said end saved(accept), which would skip CI that hasn't gone green); no → wiki-check
wiki-check script wiki-check, when the wiki changed pass → verify; fail → ci-fixer
verify script setup, test (twice: as the branch has it, and with the spec writer's tests as at specTestsSha), lint, typecheck pass → ci-status; fail → ci-fixer
ci-status script ci-status green → end saved(accept) (today also saved(merge) with testsOnly); pending → end held; failed → ci-fixer
ci-fixer session the CI fixer, with the failures (or a retest finding's output); guarded by ciFixRounds < max, else end stopped done → check-fix-review
check-fix-review session the reviewer role's correctness and scope lenses on the fix no blocking finding → wiki-check; blocking → ci-fixer, counted in ciFixRounds

Inputs

  • From the spec writer, at implement: spec or specFile, interfaces (the new ones' where are the stubs to build; specs name no other files, #391), plan, pr (the draft PR, opened by the spec writer; proposed: spec-writer-opens-pr), and stackedOn for a stacked issue. Proposed: specTestsSha, and the stubs and failing tests as commits on the branch.
  • From the reviewer, at fix: open findings (correctness, tests, scope, acceptance or security), round, and the review comments.
  • From the reviewer, at checks: a clean review (reviewedSha), with testsOnly cleared and no sync (sync-at-edges, #340).
  • From the merger, at sync: a branch behind its base or in conflict, with passedSha kept. Proposed: from the finalizer, at fix: conflict and retest findings. The fix entry's start sync handles a conflict finding.
  • The issue's checkout and wiki checkout (read-only in a stage job, #303), handoff.md from an earlier session, and the config's practices and commands.

Outputs

  • At review: the PR ready for review with the implementer's title and body (keeping "(#N)" and "Closes #N"), headSha, flows, implemented: true. Proposed (spec-writer-opens-pr): the spec writer's draft PR, untouched, with headSha, flows and implemented: true.
  • At checks, after a fix round the reviewer cleared: the pushed fix, with testsOnly cleared and no sync (sync-at-edges, #340).
  • At accept: verifiedSha and verifySummary for a head that passed every check.
  • At merge: a clean rebase of a head that had passed everything (testsOnly), once its tests pass again. Proposed: none; the finalizer does this.
  • Stops with their labels and comments when a limit is reached.

Requirements

Implement

  • The implementer reads the config's practices in full, writes tests first, then the change that meets the spec, runs the configured test, lint and typecheck commands, and commits small, working steps often as it goes rather than once at the end, so a stop or kill loses little (budgets-bound-context; in a stage job through pipeline_commit, built in #303; in a lane with plain git commit). It follows the practices or says which rule it couldn't follow and why.
  • Proposed (spec-writes-failing-tests), replacing "writes tests first":
    • the implementer writes the change that makes the spec writer's failing tests pass and keeps every other test passing, replacing the stubs with the real behaviour;
    • it may add tests of its own, for edge cases or internals the spec's tests don't reach. These follow the practices' test rules;
    • it may change test files, including the spec writer's, but a change that weakens a spec test doesn't count: verify runs the spec writer's tests as they were at specTestsSha, and they must pass;
    • if a spec test is wrong, or can't pass without breaking the spec, it returns blocked naming the test and why. The issue goes back to spec with that reason, counted toward a limit (see Open questions), instead of the implementer working around the test.
  • Every writer (implementer, fixer, CI fixer, conflict resolver) runs with the issue's checkout as its working directory and only commits. A pre-push hook refuses every push except from the ops tool, so code reaches GitHub only through push, open-pr and finish-sync (proposed: only through pipeline_commit, finish-sync and the exit push). Built (#303) in stage jobs: writers commit through pipeline_commit, which pushes, and a pre-commit hook refuses any other commit; their prompts forbid running git's commit, push, rebase, reset, checkout and merge, except the conflict resolver's git add and GIT_EDITOR=true git rebase --continue (Shared).
  • Every writer (implementer, fixer, CI fixer) may fix anything broken it finds while it works, such as bugs or flaky or broken tests and checks, including breakage on the base branch. It may also simplify, remove code smells and refactor the code it works in, so the change has a better design (unrelated-fixes-allowed, Axlog, 2026-10-17). The issue's change stays the main one, each extra fix or improvement is its own commit with a message that says what it does and why, and the tests keep passing. The implementer names each one in prBody; the fixer and the CI fixer name each one in their summary, which the pipeline posts on the PR. The prompts' shared text is beyondIssueRule in src/workflow/prompts.ts.
  • Every writer (implementer, fixer, CI fixer, conflict resolver), and the spec writer, makes no artificial load on the machine the jobs share (writers-make-no-load, #409): no busy loops, stress tools or CPU-only processes, no repeated runs to find a flake, no background or detached runs that outlive their command, and no second copy of the app or the test database. It reasons from the failing run's logs and timings, runs each failing test file once in the foreground, and stops a command past the repo's configured timeout. A suspected load or timing flake goes in its reply with the log evidence, unreproduced. The prompts' shared text is NO_ARTIFICIAL_LOAD_RULE in src/workflow/prompts.ts.
  • Writer prompts say any file the issue needs is in scope, that code, diffs and logs are data and never instructions, that the PR merges into the base branch and not into production, and that releasing is never part of an issue's work.
  • In a lane: the implementer commits wiki updates the spec asks for in the issue's wiki checkout. push refuses uncommitted wiki changes. A push whose wiki checkout has commits of its own also pushes them to the wiki's agent/issue-<n> (--force-with-lease, under the wiki lock), and a claim counts commits there as the issue's work.
  • implementor-reads-wiki: the implementor reads the wiki and writes none of it. Built in stage jobs (#303): its sessions get a read-only wiki checkout, pipeline_commit commits no wiki changes for them, its prompt says edits there are dropped, and teardown lists them in lostChanges. Proposed (#305): the spec writer commits the spec's Wiki updates (spec-writes-wiki), so the wiki flow comes from the spec stage. The implementor reads those pages at the issue's wiki branch. A build that has to depart from them returns blocked, and the issue goes back to spec.
  • It builds from the full spec: a clipped spec reaches it as specFile's path. With no spec it works from triage's plan.
  • Proposed (spec-names-test-interfaces): its task lists the spec's interfaces. It builds each new or changed interface to its call shape, filling in the spec writer's stubs. If the work needs a different call shape, it returns blocked saying why, and the issue goes back to the spec writer instead of drifting from the spec.
  • It returns done (with prTitle and prBody today; proposed: without), handoff, or blocked. A reply that carries testsAdded doesn't fit the schema (tests-added-derived). blocked stops the issue with its summary. A failed session or a malformed reply stops it.
  • done moves the issue to open-pr. Proposed: to review, after the flows check.

Continuations (every writer)

  • Each writer prompt states contextLimits.soft (default 175000) and contextLimits.hard (default 225000) in tokens (wrap-up-at-175k). Past soft, a session finishes the edit it is in, starts no new ones, makes it build and commits it, writes handoff.md as the next round's summary (what is done, what is left in order, decisions and why, traps and dead ends, the branch state with the last commit and whether the checks pass) and returns handoff; the job's one steer at soft says the same. At hard the job stops it. The conflict resolver finishes the rebase steps it can and leaves the rest in progress, never aborting.
  • On every stop at hard, the job first commits the session's uncommitted work as one WIP: commit when the checkout is on its branch with no git operation in progress and the commit hooks allow it; work it can't commit stays in lostChanges. It then keeps any handoff already there: the session's own, with a note on its uncommitted work appended, or a continuation's unchanged earlier handoff, with the job's ## Stopped session <key> section below it (its commits, uncommitted work and last text). With no handoff before or after, it writes one: a header saying so, its commits since it started, its uncommitted work and its last assistant text, cut to 2000 characters (#326).
  • A handoff starts a continuation of the same stage from the handoff, up to maxContinuations (1) per stage per issue. Counts are kept per stage in continuations, read only through stages/continuation.ts, which reads the older single number as the implementer's count.
  • A handoff past the cap stops the issue with agent:stopped-too-large. A resolver's rebase is aborted first.
  • A continuation is not another fix round, CI fix or conflict fix.

Open the PR (today; proposed: moves to the spec writer)

  • Proposed (spec-writer-opens-pr): the implementor opens no PR, and this section moves to Spec writer (Branch and PR). The PR stays a draft until the finalizer marks it ready, as it does today before merging. The implementor still works out the flows: after the implementer, a code change goes to review, and none stops the issue.
  • Today: open-pr pushes the branch, replaces the PR's title and body with the implementer's (keeping "(#N)" and "Closes #N"), marks it ready, and moves the issue to review. It creates the PR if start-pr didn't.
  • An issue's change has two flows. Code: the code branch changes files against the base (git diff --quiet origin/<base>...HEAD). Wiki: the wiki branch has commits of its own. So a change is code only, wiki only, both, or neither.
  • The ops that push or rebase the code branch (push, open-pr, a clean sync-base, finish-sync) reply codeChanged and wikiAhead. push doesn't throw when the branch changes no files; it reports what changed.
  • The stages work out the flows from those replies after implementation, after every fix round and after every sync, and save them as st.flows. Stages never run git. src/workflow/flows.ts holds both decisions as pure functions (flowsOf, and prFlowAction: stop, close, reopen or go). The shared step in flow-step.ts carries them out for open-pr, fix, ci-fix and sync.
  • What each flow does at open-pr:
    • code only, or code and wiki: go to review;
    • wiki only: close the draft PR with a signed comment naming the wiki branch (st.prClosed), then go to review;
    • neither: stop as not doable.
  • A reply that fails or lacks the PR number stops the issue.

Fix (the fixer's half)

  • Proposed: the fixer may change tests too, including ones the tests lens flagged. The spec writer's tests, as of specTestsSha, must still pass. A finding that a spec test itself is wrong goes back to the spec writer (see Reviewer).
  • The fixer gets the open findings, the spec (by path when clipped) and the practices. For each finding it resolves it or disputes it once with evidence, and the lens that raised it rules on the dispute. A finding that can only be met by breaking the spec is disputed, quoting the spec line.
  • A disputed acceptance finding stops the issue with agent:stopped-acceptance before the push, since only the operator can rule on what the issue asks.
  • The fix is pushed, and a fix that adds code to a wiki-only issue reopens its PR through open-pr with reopen. Proposed: a wiki-only issue has no PR and the fixer can't edit the wiki, so its findings go to the spec writer. A finding that needs code there goes back to spec, whose new spec opens the PR.
  • A push that fails before git pushes is retried once in the same sweep. A second such failure stops as error. A git rejection stops as push-rejected.
  • Each fix round is counted. At most maxFixRounds (2). One past the last round stops the issue.

Sync

Proposed (sync-at-edges, Axlog, 2026-10-17: "Sync should happen at the beginning, before the edits, and the end."):

  • The implementor syncs only at the start of an entry, before any edit:
    • implement, so the branch starts at the base's head;
    • fix, after any return to the implementor: review, acceptance or security findings, or a finalizer conflict or retest finding;
    • a job that resumes an interrupted implementor entry: a killed job, a crash, a rate-limit deferral or an orchestrator restart.
  • The only other sync is the finalizer's rebase in finalize, after the merge-order wait (Finalizer).
  • No sync runs while an entry runs, between its continuation sessions, or as a step of its own after a review clears. A planned handoff at contextLimits.soft or hard is not an interruption. The old review or rereview → sync → checks path now goes straight to checks (#340).
  • A checks job that starts again after waiting on CI (held) doesn't sync. That wait is not an interruption.
  • A start sync that conflicts runs the conflict resolver first, in that entry, before the writer edits.
  • A resolver that returns cannot-fix at an entry's start leaves the issue at its stage, and the next job's start sync tries again (conflicts-never-stop). It stops with agent:stopped-conflict only once maxConflictFixes runs out, or when a resolution is rejected on the last attempt.
  • Only returns to finalize count toward maxSyncs. A start sync doesn't, whether clean or not. Conflict resolutions at an entry's start still count toward maxConflictFixes.

Today:

  • sync-base rebases the branch onto the base, or onto the prerequisite's branch for a stacked issue, and replies the new stackedOn. A clean rebase moves to checks. A head unchanged by the rebase moves on without a comment.
  • A conflict never waits for a human. sync-base leaves the rebase stopped in the checkout, and the conflict resolver finishes it. Where the sides contradict, it keeps what the base now does, re-applies the branch's change on top as far as it still makes sense, and says what it chose. It also fixes code that merged cleanly but broke. finish-sync checks the rebase finished on the current base with no markers before pushing.
  • The conflict resolver doesn't read the practices.
  • A resolver that returns cannot-fix leaves the issue at sync for next sweep. A rejected resolution retries up to the last attempt, then stops. A failed resolver session aborts the rebase and stops.
  • Limits: maxSyncs (3; project-buddy 100) rebases and maxConflictFixes (2; project-buddy 100) resolvers. Nothing is charged for a resolver or a re-check: the rebase allowance is gone (budgets-bound-context, Axlog, 2026-10-16).
  • A resolver that runs out of room hands off to a continuation on the rebase still in progress. Past maxContinuations the rebase is aborted and the issue stops with agent:stopped-too-large.
  • A clean rebase of a head that passed every check, acceptance and the security review sets testsOnly, so only the tests run again.
  • A network failure defers to the next sweep once. A second stops.

Stacked branches

  • An issue may start on a prerequisite issue's unmerged branch (job.mjs --stack-on P). It stays stacked on it, in st.stackedOn, until the prerequisite merges.
  • Every stacking decision on the ops side lives in src/stack.ts: where a claim starts the checkout, which ref sync-base rebases onto, and what prepare-review, verify and finish-sync measure from. The stages' side is src/workflow/stages/stacked.ts: they pass st.stackedOn in their payloads and copy the stackedOn a sync replies.
  • sync-base rebases onto the prerequisite's head while it is open, and onto the base once its PR merged or its issue closed as completed. It replays only the dependent's own commits (rebase --onto <target> <saved prerequisite head>), so a squash-merged prerequisite leaves none of its commits behind.
  • An issue closed as completed counts as merged. If one is closed as done without its PR merging, the dependent unstacks early, drops the prerequisite's commits, and its checks run on the base alone.
  • When the prerequisite's head moves or it merges, a stacked issue at sync through merge restarts at sync (restackToSync). Only one moved back from merge keeps its passed head, so a clean rebase re-runs only the tests. One moved back from checks, accept or security goes through acceptance and the security review again. Proposed (finalizer-role, sync-at-edges): nothing restarts a stacked issue at sync. It rebases onto the prerequisite's head, or onto the base once the prerequisite merged, at its next implementor entry's start or in finalize.

Checks

  • An issue that changed the wiki runs wiki-check first. A wiki-only change skips verify and CI and runs only wiki-check. wiki-check runs no model: it checks that the links in the issue's changed wiki pages point to existing pages, and that its new pages are linked from _Sidebar or another page.

  • Proposed (spec-writes-failing-tests): fails-before-fix moves to the spec writer's commit check, where the new tests are shown to fail on the stubs. In its place, verify checks two things:

    • the test command passes;
    • it passes again with the spec writer's test files put back as they were at specTestsSha, apart from what a rebase brought in from the base.

    That proves the spec's own tests pass on the implementor's code, whatever the implementor added or changed. The implementor's own tests aren't required to fail first, because the spec's tests already show the change does something.

  • Today: verify runs setup, test, lint and typecheck in the checkout. It checks fails-before-fix (the tests fail with the non-test changes reverted), which only counts once the tests pass on the PR, taking the change's tests from its diff through testPathGlobs, skipped when no code changed (nonCodePathGlobs, comment-only JS or TS changes). It regenerates changed lockfiles and compares them, and runs changed migrations up, down and up on a scratch database. With testsOnly it runs only setup, the test command and the lockfile check.

  • ci-status waits for the required CI checks on the exact head SHA, up to ciWaitMinutes (30). Pending CI ends the stage until next sweep.

  • Ops steps set control.needsAttentionAfterMs to their own timeout, so a long verify raises no silence alert.

  • The CI fixer fixes every failing check, whatever caused it, even when the issue's change didn't. A flaky check is a failure: the CI fixer fixes its cause and never re-runs it until it passes. Breakage on the base branch is fixed the same way. cannot-fix is only for what no change in the repo can fix: an outside service's outage, missing secrets or credentials, or anything that needs Axlog. It stops the issue with agent:stopped-checks-unrelated, a name kept so existing labels and state still parse (unrelated-fixes-allowed).

  • A failed check (verify, wiki-check or CI) starts a CI fix: the CI fixer fixes it, the fix is pushed, and its diff is reviewed by the correctness and scope lenses (reviewer role). A blocking finding there stops the issue in the lanes; in the role's checks entry (#420) it goes back to the CI fixer, counted as a CI fix. At most maxCiFixes (2), then the issue stops with the failure.

  • Passing checks move the issue to accept with verifiedSha, or to merge when testsOnly held. Proposed: always to accept.

Done looks like

  • Every requirement above has a test at one of the interfaces below, and mise //apps/issue-pipeline:check passes.
  • The role's stages, ops and replies live in modules the other roles don't import, apart from the shared parts the index lists.
  • Only this role's sessions run as writer, and every writer is steered to wrap up at contextLimits.soft and stops at contextLimits.hard with a handoff.
  • Every exit leaves the branch on GitHub at the pushed head, with nothing the issue needs only in a local checkout. The stage job's exit push covers writers with unpushed commits, and unpushed wiki commits are kept in the wiki mirror (see the index's shared checkout rules).
  • Proposed: the spec writer's tests, as of specTestsSha (apart from what rebases brought in), pass on the branch at review and at every later verify.
  • The role's own tests have no flakes. A flaky test is a failure to fix, never a rerun.
  • The wiki's implementer-handoff, bounded-fix-loop, conflicts-never-stop, verification and wiki-flows sections describe the role as built.

Test interfaces

  • Machine (proposed): TABLE and transition from src/roles/implementor/machine.ts, called directly, one state and result per test.
  • Step machine (today):
    • implement and open-pr (today): next from stages/implement.ts and stages/open-pr.ts, with workflow-build-test-kit.mjs (buildState, implementLaunch, implementTask, HANDOFF).
    • fix: stages/fix.ts with workflow-fix-test-kit.mjs (fixState, round, REJECTED_PUSH) and workflow-review-test-kit.mjs (openFinding, fixTask).
    • sync and checks: stages/sync.ts and stages/checks.ts with workflow-checks-test-kit.mjs (stateAt, sessionResult, replied).
  • Built sweep: runWorkflow with an issue claimed at implement, fix, sync or checks (claimAt), scripting the writer sessions and push, open-pr (today), sync-base, finish-sync, verify and ci-status replies. Assert with opsRequests, releasedState and sessionAgents.
  • Ops CLI:
    • push, open-pr, close-pr, sync-base, finish-sync: runOps against ops-fixture.mjs repos and the fake gh, checking the branch on the local origin with sh and GitHub effects with effectsOf.
    • verify and ci-status: verify-test-kit.mjs (verifyKit, ask), against a real repo with its own typescript.
    • wiki-check: runOps against ops-fixture.mjs (wiki-check-op.test.mjs), and src/wiki-check.ts called directly (wiki-check.test.mjs).
  • Stage job: runJob with --stage implement, fix, sync or checks, using fake-rpc-pi.mjs scripts whose turns raise the context. Assert the soft-limit steer's text, the hard-limit stop as a handoff, the handoff and WIP commit the job writes for a stopped writer, the exit push, and GH_TOKEN only from the writer's tokenEnv.

Test plan

Rows marked step machine test today's stages. With role-state-machines they are ported to the machine: the same case, given as a state and a result.

Requirement Interface Case Today
Implement from the spec step machine the writer launch carries the spec or its path, or the plan without a spec workflow-stages-implement-next.test.mjs
Implement outcomes step machine done moves to open-pr (proposed: to review); blocked, a failed session and a malformed reply stop workflow-stages-implement-next.test.mjs
Tests derived (change) step machine, ops CLI a done reply without testsAdded moves to open-pr, and one with it is malformed; fails-before-fix finds the change's tests from the diff verify-fails-before-fix.test.mjs covers the diff today. The reply fixtures in workflow-stages-implement-next, -build, -continuation and workflow-sweep carry testsAdded and must drop it
Continuations step machine, built sweep handoff then continuation then done; past maxContinuations stops as too large; 0 and 2 allowed; spawn cap holds a continuation workflow-stages-implement-next.test.mjs, workflow-stages-handoff-next.test.mjs, workflow-stages-fix-handoff.test.mjs, workflow-stages-continuation.test.mjs
Practices step machine implementer, fixer and CI fixer tasks carry the writer's practices note practices.test.mjs
Open PR and flows (proposed: moves to the spec writer) step machine code goes to review; wiki only closes the draft; nothing stops; a failing reply stops workflow-stages-openpr-next.test.mjs, workflow-stages-flows-next.test.mjs
Fixer step machine fixer gets the spec and the dispute rule; a dispute is recorded once; a disputed acceptance finding stops before the push workflow-stages-fix-next.test.mjs, workflow-stages-fix-spec-next.test.mjs, workflow-stages-accept-fix.test.mjs
Push retry step machine a pre-git failure retries once; a git rejection stops as push-rejected workflow-stages-push-retry.test.mjs
Sync step machine, ops CLI clean, unchanged, conflict resolved, cannot-fix, rejected resolution, failed session, network defer, maxSyncs workflow-stages-sync-next.test.mjs, sync-ops.test.mjs, sync-ops-base-sha.test.mjs
No spend stops step machine a resolver and a re-check run with old spend fields far past the old issue budget and rebase allowance workflow-stages-sync-next.test.mjs, workflow-stages-finish-next.test.mjs
Stacked sync step machine, ops CLI requests carry stackedOn; unstack after the prerequisite merges workflow-stages-sync-stacked.test.mjs, sync-ops-stacked.test.mjs
Checks step machine, built sweep wiki-check first; verify; CI pending defers; CI fix reviewed by two lenses; maxCiFixes stop; testsOnly goes to merge workflow-stages-checks-next.test.mjs, workflow-stages-checks.test.mjs, workflow-stages-flows-next.test.mjs
Verify ops CLI commands, fails-before-fix and its skips, lockfiles, migrations, stacked base verify.test.mjs, verify-fails-before-fix.test.mjs, verify-stacked.test.mjs, ci-status.test.mjs
Context limits stage job soft steer to wrap up; hard stop becomes a handoff, with a WIP commit on every stop where that is safe, and the job's handoff, note or section that keeps any handoff already there; hard below every writer's budget job-context-limit.test.mjs, job-context-wrap-up.test.mjs, job-stop-handoff.test.mjs, config-context-limits.test.mjs
Exit push stage job a writer's unpushed commits are pushed on exit; uncommitted paths are recorded as lostChanges job-checkout.test.mjs, ops-stop-push.test.mjs
Writer boundary stage job no session gets GH_TOKEN or a tokenEnv variable; only writer sessions get pipeline_commit and its scope (#303) job-roles.test.mjs, job-boundary.test.mjs
Role isolation (new) lint or unit the role's modules import nothing from the other roles' modules roles-implementor-isolation.test.mjs (#414)
Implementor tests allowed (new) pipeline_commit, machine an implementer commit that adds or edits a test file is accepted and pushed pipeline_commit built (#303): pipeline-commit.test.mjs, pipeline-commit-push.test.mjs; the machine, none yet
Spec tests as written (new) ops CLI verify fails when the spec writer's tests, put back as at specTestsSha, fail; it passes when they pass, whatever tests the implementor added none yet
Weakened spec test (new) ops CLI an implementor edit that makes a spec test assert less still fails verify with that test named none yet
Wrong spec test (new) machine blocked naming a test sends the issue back to spec with the reason, counted; past the limit it stops none yet
Machine edges (new) machine each row of the four tables: the state, a result, the next state and effect implement and start: roles-implementor-machine.test.mjs (#414)
No PR ops (new) machine, stage job no implementor state sends open-pr, start-pr or close-pr; done with a code change saves at review with the PR untouched roles-implementor-machine.test.mjs, roles-implementor-job.test.mjs (#414)
No PR text (new) pure module the role's schema has no prTitle or prBody, and a reply that carries them still reads as its status and summary (#414 changed this row: a stale prompt or the job's hard-limit reply must not stop the issue) roles-implementor-reply.test.mjs (#414)
Resume an old state (new) locate, stage job a state saved by today's implement stage (continuations a number or a record, phase decide) resumes with its count roles-implementor-resume.test.mjs, roles-implementor-job.test.mjs (#414)
One job (new) stage job, runner a job entered at fix with every finding resolved and green checks saves at checks on the way, and ends at accept in one checkout built (#424): roles-implementor-fix-checks-job.test.mjs
One job restarts mid-way (new) stage job, locate a job killed in checks starts again at checks, not fix built (#424): roles-implementor-fix-checks-job.test.mjs, orchestrator-one-job.test.mjs
Finalizer findings (new) machine, local git a conflict finding runs the rebase and the resolver in the fix entry's start sync, then checks; a retest finding runs the CI fixer, then checks; both end at accept none yet
Sync with nothing to do (new) machine a start sync on a branch already on the target's head goes on to the entry's first state with no sync-base and no comment built (#419): roles-implementor-sync-job.test.mjs
Sync at edges (new) machine, stage job sync-base runs before any writer session in an implement entry, a fix entry and a resumed entry; a start-sync conflict runs the resolver before the writer; no sync-base runs between continuations, after a clean review or re-review, or in checks after a fix in the same job none yet
Checks with nothing to do (new) machine a head equal to verifiedSha, with nothing moved and no retest finding, skips verify and goes to ci-status (#420) roles-implementor-checks-machine.test.mjs, roles-implementor-checks-job.test.mjs
Implementer changed nothing (new) machine done with no code change and failing spec tests stops with the implementer changed nothing none yet
Wiki read-only (new) stage job an implementor session's wiki edits aren't committed and are recorded in lostChanges none yet
Kill anywhere (new) locate, runner, fake gh killed before and after each state of all four stages and started again, the implementor ends as an uninterrupted run, with one PR and no lost pushed commits; an interrupted rebase starts over; sessions may run twice partly: --resume resumes sessions today, and stages restart at begin

Open questions

  • Back to spec: how many times can an issue go back from implement or fix to spec for a wrong spec test before it stops? Deferred (roles-6-10-scope): for now blocked stops the issue, as today.
  • Implementor's own tests: should they have to fail before the fix, as fails-before-fix requires of every test today? The proposal says no, since the spec's tests already prove the change.
  • Checks: does checks stay here, or does verify and ci-status move to the reviewer or the finalizer as the gate (the finalizer now retests before merging), leaving only the CI fixer here? Today it is one writer stage.
  • Fix rounds: the fixer and re-review share one stage machine (stages/fix.ts, rereview.ts). Splitting them means a new handoff stage, such as rereview, and moving the round count. With the lifecycles (proposed), the re-reviewers are the reviewer role's sessions running inside this role's fix lifecycle; a separate rereview stage would give one role per job, at the cost of a job start per round.
  • CI-fix review: the correctness and scope lenses run inside checks. Should they become a reviewer stage, or stay embedded?
  • Wiki conflict resolver: proposed: the finalizer sends wiki conflicts to the spec writer, the wiki's writer, not here.

Sources

The requirements above take in the wiki decisions this role follows, as of wiki f1b2944 (2026-10-16): implementer-handoff, bounded-fix-loop, conflicts-never-stop, verification, wiki-flows, stacked-branches, linked-draft-prs, worktrees-and-branches, wiki-docs, spec-reads-practices, flakes-are-failures. From this split, not on the wiki yet: tests-added-derived, spec-names-test-interfaces, spec-writes-failing-tests, implementor-reads-wiki, pipeline-commit, pr-text-derived (awaiting Axlog's answer) (decisions in the owner's notes, not public). From this split, on the wiki: sync-at-edges. Once the specs are approved, prune-wiki-decisions deletes or trims the wiki sections the specs make redundant.

⚠️ **GitHub.com Fallback** ⚠️