Files
damascus-wiki/concepts/builder-contract.md
2026-06-23 03:55:56 +00:00

6.6 KiB

title, created, updated, type, project, tags, sources, related
title created updated type project tags sources related
Builder Contract 2026-06-23 2026-06-23 concept damascus-orchestrator
agent
builder
claude-code
contract
git
worktree
pr
raw/articles/multi-project-orchestration-plan-1.md
damascus-orchestrator-overview
spec-refiner-contract
reviewer-contract
state-resume-protocol

Code Builder — Contract

What the code builder (Loop B) takes in, what it must produce, and how the reviewer resumes from its output. This is the contract the design doc §4 Loop B and §6 promise; the E2E suite tests adherence to it.

Input (what the agent receives)

The builder reads the row from work_items where phase='build' (atomic claim via SELECT ... FOR UPDATE SKIP LOCKED).

Field Source Required?
id work_items.id yes
project work_items.project yes
story_id work_items.story_id yes
file_scope work_items.file_scope (from spec-refiner) yes
spec_path work_items.spec_path yes
base_commit work_items.base_commit (the main tip) yes — set by claim
attempts work_items.attempts yes
budget_cycles work_items.budget_cycles yes
Spec file contents loaded from spec_path yes
## TDD Plan parsed from spec yes
## Test Command parsed from spec yes

What the builder must do (sequentially, no skipping)

  1. Worktree isolation (design doc §6 Layer 1): create a git worktree off base_commit at /workspace/worktrees/<story_id>, branch feat/<story_id>. This is physical isolation — concurrent builders cannot overwrite each other.
  2. Hand the spec to Claude Code via LiteLLM, model minimax-m3:
    ANTHROPIC_BASE_URL=http://host.docker.internal:4000
    ANTHROPIC_API_KEY=sk-dummy
    claude --print --model minimax-m3 --max-turns 50+ <full spec as prompt>
    
    Critical: --max-turns 50+ is the realistic starting point for non-trivial component work. 12 turns is too few; the model burns turns reading files.
  3. Constrain Claude's filesystem access to the worktree and the file_scope paths. Files outside the declared scope → reject (defense in depth; the spec refiner should not have allowed them, but verify).
  4. Run the spec's ## Test Command in the worktree. The TDD plan says tests must fail before code exists; after Claude writes, they must pass.
  5. Rebase onto the project's main branch before opening a PR. Branch detection: try main, master, develop in order. The current default for wh40k-pc is master. Rebase conflict → verdict rebase_conflict, route back to builder with the conflict as context.
  6. Commit + push the branch, then open a PR via Gitea API:
    POST /api/v1/repos/<owner>/<repo>/pulls
    { head: feat/<story_id>, base: master|main, title: <story title>, body: <spec link> }
    
  7. Persist the pr_url on the row:
    UPDATE work_items SET
      phase = 'review',
      branch = 'feat/<story_id>',
      pr_url = '<url returned by Gitea>',
      base_commit = '<new main tip after rebase>',
      updated_at = NOW()
    WHERE id = '<row id>'
    
  8. Emit events_outbox events:
    • build.committed (after git commit)
    • build.pushed (after git push)
    • build.pr_opened (after Gitea API returns 201)
    • phase.transitioned (build → review)

Side effects on the row

On success:

  • phase = 'review'
  • branch = 'feat/<story_id>'
  • pr_url = <real Gitea PR URL>MUST be non-null before transition
  • base_commit = <sha after rebase>
  • last_verdict not set yet (the reviewer will set it)
  • attempts does NOT increment on success

On tests_failed (Claude wrote code, tests don't pass):

  • phase = 'build' (stays — retry)
  • attempts++
  • last_verdict = 'tests_failed'
  • last_feedback = { "test_output": "...", "files_changed": [...] }
  • The worktree persists so the next attempt can read the partial work

On rebase_conflict:

  • phase = 'build'
  • attempts++
  • last_verdict = 'rebase_conflict'
  • last_feedback = { "conflicting_files": [...], "their_sha": "..." }

On no_pr (defensive verdict — Claude claimed success but no PR exists):

  • phase = 'build'
  • attempts++
  • last_verdict = 'no_pr'
  • This verdict was added after observing the bug where the build phase returned success-without-opening-a-PR. The reviewer must never merge a row with pr_url IS NULL.

On attempts >= budget_cycles:

  • phase = 'blocked'
  • events_outbox emits blocked.exhausted
  • The row is parked; surfaced to the human

Files-touched audit (the E2E test point)

The builder MUST record every file it modified, in last_feedback.files_changed. The reviewer and the metrics analyzer both consume this. This is the data the scope-disjoint check (§6 Layer 2) runs against.

Acceptance criteria (the contract)

  • Builder claims a row in build phase via SELECT ... FOR UPDATE SKIP LOCKED
  • Builder creates a worktree on a unique branch per story
  • Builder invokes Claude Code with the spec as prompt, model minimax-m3, max-turns ≥ 50
  • Builder runs the spec's ## Test Command and only succeeds on exit 0
  • Builder rebases onto the project's main branch (mainmasterdevelop in order)
  • Builder opens a real Gitea PR via the Gitea API (not just local commit)
  • Builder persists pr_url on the row BEFORE transitioning to review
  • Builder records files_changed in last_feedback for every file it touched
  • Builder NEVER sets phase='review' with pr_url IS NULL (this was the bug)
  • On test failure: attempts increments, last_verdict='tests_failed', row stays in build
  • On budget exhaustion: phase='blocked', no further attempts
  • Concurrent builders on different stories cannot overwrite each other (worktree isolation)

How the next phase resumes

The reviewer, when it claims a row in review:

  1. Reads work_items.pr_url → fetches the PR diff from Gitea API
  2. Reads work_items.branch → knows which branch to validate
  3. Reads work_items.base_commit → knows what master|main tip the rebase was tested against
  4. Re-runs ## Test Command in the worktree to confirm
  5. Looks up the spec from work_items.spec_path to know what was promised

If pr_url IS NULL → reviewer MUST return verdict no_pr and NOT merge. This is the defensive guard against the bug.

Cross-references