Matthew Dresden · DevBench · Engineering view

The complete technical reference

Every agent, hook, guard, skill, status, configuration key and CLI command in DevBench, read out of the open-source repository and cross-checked against the code that actually runs. Where the repo's own documentation disagrees with its code, section 10 says exactly where.

10
agent prompt files
(6 top-level + 4 review_team)
21
hook command registrations
across 10 event types
5
skills in 2 plugins
(1 execution + 4 authoring)
5
judge verdicts required
by the done-gate
9
work-unit statuses
2 of them claimable
7
block-resolution paths
(BlockedTaskState)
56
registered CLI commands
112
config leaf keys
under 24 top-level keys

All figures on this page were enumerated from the repository at commit c42fc64 (2026-06-05). DevBench is licensed Apache-2.0 (LICENSE, NOTICE).

On this page
On this page
Section 01

System overview

DevBench is three phases sharing one filesystem: a human-driven authoring phase that produces a spec and a backlog, an autonomous orchestration phase that executes one work unit at a time under judged review, and a gitops phase that commits, opens the PR, watches CI and merges. Each phase ships as its own artifact.

Phase 1

Authoring plugin

devbench-authoring v0.2.0: 4 skills, zero agents, zero hooks, zero scripts. Its manifest states the reason: "No PreToolUse hooks; safe to enable in any workspace where backlog authoring happens."

Produces: spec/<project>.md, BACKLOG.md plus one markdown file per work unit, backlog/config/devbench.yaml, and cloned + toolchain-validated target repos.

plugin-authoring/devbench-authoring/.claude-plugin/plugin.json:3
Phase 2

Orchestrate plugin

devbench-orchestrate v0.4.0: 1 skill, 10 agents, 1 hooks manifest, 12 shell scripts. Roughly 1,880 lines of agent prompt and 1,550 lines of guard shell.

The two plugins were split by issue #224 precisely because the work-unit write guard "correctly blocked the executor and incorrectly blocked the authoring skill as collateral damage."

docs/migration-0.4.0.md:1-22
Phase 3

GitOps

Branch, commit, push, PR, CI watch, merge: all behind the devbench git-ops CLI command, not an agent. Four mutually exclusive modes; illegal combinations are rejected at config load.

The pipeline terminates at merge. There is no deployment, release or post-merge promotion stage anywhere in the codebase.

src/devbench/cli.py:9465 · src/devbench/github/git_ops.py:530-559

The orchestrator is not a daemon

devbench start makes exactly one claude_agent_sdk.query() call, with the prompt "Run the devbench-orchestrate:orchestrate skill to process the backlog until complete" and ClaudeAgentOptions(plugins=[{"type": "local", "path": <plugin path>}], permission_mode="bypassPermissions"). The loop is the SKILL.md prompt, executed inside that single session: not a scheduler, not a queue consumer, not a service.

--daemon does not change that. It detaches the same session to the background and writes a PID file; instances, tail, stop-instance and restart then manage the detached process.

What keeps a prompt-driven loop alive is the Stop hook (section 3): it blocks the session from ending while a task is in progress and injects the next step. The circuit breaker is what stops it looping forever.

src/devbench/cli.py:6315 · src/devbench/cli.py:6515 · src/devbench/cli.py:9538-9542

State lives on disk, in markdown

Markdown work-unit files are the source of truth. BACKLOG.md is a derived index; validate-backlog reports drift between them as an error rather than auto-correcting, and the parser logs a warning and trusts the file. Work-unit files carry no YAML frontmatter: status, repo, branch, dependencies and acceptance criteria are parsed out of markdown by regex.

There is no message broker, no database server and no external issue tracker in the data path. SQLite is present, but only as a rebuildable report cache (section 9). Because state is on disk and every agent action appends a timestamped audit comment, the loop resumes from any point after a restart.

docs/backlog-contract.md:234 · src/devbench/backlog/parser.py:206-217 · src/devbench/constants.py:39-52

Per-work-unit pipeline

validate-backlog → next → claim → ensure-branch → executor (TDD)
   → [manifest-amender, conditional] → review-supervisor (4 judges in parallel)
   → security-reviewer → git-ops (commit / push / PR / CI / merge) → mark-done → loop
plugin/devbench-orchestrate/skills/orchestrate/SKILL.md:16-115 · docs/architecture.md:52 ↑ Back to contents
Section 02

The 10 agents

Ten agent prompt files ship in the orchestrate plugin: six at the top level and four in agents/review_team/. Each takes a single argument (a work-unit ID) and each opens with an ## Evidence block whose commands are executed and inlined before the prompt is evaluated, so agents reason over CLI output rather than over free filesystem access.

AgentModelToolsdisallowedToolsRoleInvoked by
executorsonnet Bash, Read, Write, Edit, Glob, Grepnone declared Implements one work unit under TDD (RED/GREEN/REFACTOR); stages files only orchestrate skill step 4 SKILL.md:56, step 6a on REVIEW_FAIL, and git-ops rc 2 / rc 3
review-supervisorsonnet Bash, Agent(code-reviewer, test-reviewer, doc-reviewer, changes-manifest)none declared Discovers the review team, fans out in parallel, parses their JSON envelopes, writes the canonical verdicts orchestrate skill step 5 SKILL.md:83; re-invoked after each executor retry
code-revieweropus BashWrite, Edit, Read, Glob, Grep 52-point rubric: acceptance criteria, SOLID, DRY, fail-fast, 12-factor, security, idiomatic code, infra completeness review-supervisor (parallel fan-out)
test-revieweropus BashWrite, Edit, Read, Glob, Grep 49-point rubric: real tests only, TDD discipline, coverage, git completeness. The only judge that runs the test suite (devbench run-tests is injected as evidence) review-supervisor (parallel fan-out)
doc-revieweropus BashWrite, Edit, Read, Glob, Grep 29-point rubric: documentation synchronisation, API/config/architecture docs, evidence-based content review-supervisor (parallel fan-out)
changes-manifestopus BashWrite, Edit, Read, Glob, Grep 27-point rubric: staged diff vs declared Changes Manifest, plus an independent commit-attribution check over origin/main..HEAD review-supervisor (parallel fan-out)
security-revieweropus BashWrite, Edit, Read, Glob, Grep 57-point rubric across 10 groups (SOC 2, PCI DSS, FINRA, SEC, GDPR, CCPA, SOX). FAIL if any critical or high finding exists orchestrate skill step 7 SKILL.md:95: only after all four review judges pass. Never retried
manifest-amenderopus BashWrite, Edit, Read, Glob, Grep Layer-2 semantic judge of a pending Changes-Manifest amendment: approach authorization, scope minimality, justification coherence, pre-conflict check step 4b, gated on test -f .devbench/amendments/<id>.json
blocker-resolveropus BashWrite, Edit, Read, Glob, Grep 21-question rubric; classifies a blocker and emits a structured proposal JSON for new work units step 4c, only when task_factory.enabled: true and the amender just rejected
task-factoryopus BashWrite, Edit, Read, Glob, Grep Runs exactly one command (devbench materialise-proposal) and surfaces any non-zero exit code. Promotion stays the operator's decision step 4a.d (validation-gate path) and step 4c.c (amendment-reject path)
agent YAML frontmatter, plugin/devbench-orchestrate/agents/**

8 of 10 are Bash-only

Eight agents declare tools: Bash with Read, Write, Edit, Glob, Grep explicitly in disallowedTools. They obtain all evidence through pre-injected CLI output and act only through uv run devbench … commands.

Only the executor writes

executor is the single agent with filesystem write access. Even it is barred from backlog/**/*.md by a PreToolUse guard, and from branching, committing and pushing by its own prompt.

Only the supervisor spawns

review-supervisor is the only agent holding the Agent tool, and its declaration names exactly four subagents. A guard enforces the same allowlist at runtime.

Correction to the repo docs

The "Model Per Role" table in docs/plugin-architecture.md is stale in every row. It claims executor runs on opus, the four review judges on haiku, and security-reviewer on sonnet. The actual frontmatter is: executor and review-supervisor on sonnet, the other eight agents on opus. The configure-devbench skill's own table matches reality; the architecture doc does not.

The stale table also recommends haiku, which config load rejects for every per-agent field, because under load the Agent tool was observed silently dropping off haiku sessions (issue #198). Models are per-agent overridable via the agents: block, which materialises a workspace-local shadow plugin and rewrites the frontmatter line; so any public claim should say "declared model", not "the model that runs".

docs/plugin-architecture.md:115-119 vs. agent frontmatter · config_loader.py:937-990, :1038 · plugin_shadow.py

The pipeline

executoragent · sonnet review-supervisoragent · sonnet security-reviewerjudge · opus git-opsCLI, not an agent mark-doneCLI · done-gate
review-supervisor dispatches all four judges in a single response so they run in parallel; sequential invocation is explicitly forbidden:
code-reviewer → code_review test-reviewer → test_review doc-reviewer → doc_review changes-manifest → changes_manifest

Amendment side-branch (step 4b). If .devbench/amendments/<id>.json exists: manifest-amenderapply-amendment (continue to review) or reject-amendment (revert the files, archive the request, block the task).

Recovery side-branch (step 4c). Only when task_factory.enabled: true and the amender rejected: blocker-resolverwrite-proposaltask-factorymaterialise-proposal → back to step 2.

Both branches are gated on file existence, never on an agent's words. The skill is explicit: "The check is on the FILE, not on the agent's verdict word. The verdict word … is NEVER a control point."

SKILL.md:56-115 · SKILL.md:79 · review-supervisor.md:58-60

Three hard invariants the skill states

  • "Never bypass the done-gate -- review-supervisor must pass before git-ops."
  • "Security review runs exactly once per work unit -- after review-supervisor passes."
  • "The retry loop (step 6) re-runs only review-supervisor, never security-reviewer."
plugin/devbench-orchestrate/skills/orchestrate/SKILL.md:134-136

Verdict names, rubrics and rejection vocabularies

Verdicts must be logged under underscored canonical names, not the agents' hyphenated frontmatter names; the done-gate parses only the underscored forms, so a hyphenated verdict means mark-done can never succeed. Five names satisfy the gate; four more are accepted by log-verdict as audit-only.

Canonical judgeCounts toward done-gate?Rubric sizeControlled rejection vocabulary
code_reviewYes52 itemsMAKE_VALIDATE_FAILURE, HARDCODED_URL, MISSING_AC_EVIDENCE, SOLID_VIOLATION, SECURITY_BYPASS_ANNOTATION, SCOPE_VIOLATION, MANIFEST_TODO_UNFILLED, AGENT_LOG_CONTRADICTS_DIFF
test_reviewYes49 itemsGIT_COMPLETENESS, STUB_TEST, COVERAGE_REGRESSION, TDD_CYCLE_MISSING, DRY_VIOLATION
doc_reviewYes29 itemsREADME_SYNC, CHANGELOG_SYNC, API_DOCS_STALE, EVIDENCE_BASED_CLAIM, CONFIG_DOCS
changes_manifestYes27 itemsSCOPE_GAP, MANIFEST_MISMATCH, STAGING_GAP, OUT_OF_SCOPE_FILES
security_reviewYes57 itemsSECRET_LEAK, UNAUTHORIZED_DEP, SCOPE_VIOLATION
manifest_amenderAudit only4 questionsSCOPE, APPROACH_AUTH, JUSTIFICATION_COHERENCE, PRE_FILTER, OTHER
blocker_resolverAudit only21 questionsverdicts proposed / resolved / escalated / blocked
task_factoryAudit onlyn/asurfaces the CLI exit code
executorAudit onlyn/ablocked by a guard from writing any of the five canonical verdicts
src/devbench/constants.py:204-215, :223-235 · review-supervisor.md:85-95 · guard-verdict-format.sh:24-47

Read these as rubrics, not automated checks. The 52 / 49 / 29 / 27 / 57 figures are counts of numbered prompt line items an LLM judge is instructed to evaluate. They are not independently executed deterministic assertions; the deterministic layer is the hook guards in section 3 and the CLI gates in section 5.

Evidence injection

Every agent opens with an ## Evidence section whose backtick-bang lines run before the prompt is evaluated. That is how Bash-only agents see code at all:

read-unit
work-unit content, repo_path and work_unit_path as JSON (injected for all ten agents). The four review-team judges (plus manifest-amender and task-factory) pass --strip-comments so prior verdicts do not bias them; security-reviewer, review-supervisor, blocker-resolver and executor read the unit with its comment history intact for their injected evidence; blocker-resolver additionally issues a stripped read inside its recovery recipe blocker-resolver.md:87
get-diff
the authoritative scope source for every judge, mode-aware per ADR-12, so judges never compute scope with a raw git diff origin/main
run-tests
injected only for test-reviewer
proposal / amendment JSON
cat-ed directly for task-factory and manifest-amender
agents/**/*.md lines 11-20 · docs/architecture.md:62 ↑ Back to contents
Section 03

Hook system: the deterministic floor

Agent prompts are instructions; hooks are enforcement. One file (hooks/hooks.json) registers 10 event types, 14 matcher groups and 21 command registrations, all pointing at ${CLAUDE_PLUGIN_ROOT}/scripts/*.sh. The contract is blunt: exit 0 allows the tool call, exit 2 blocks it and the guard's stderr becomes Claude's feedback. The Stop hook is the exception: it emits a JSON decision envelope instead.

10
hook event types registered
14
matcher groups
21
command registrations
12
script files (10 wired)

Complete registration map

EventMatcherScripts, in order
PreToolUseBashhook-logger.sh, guard-bash.sh, guard-verdict-format.sh, guard-comment-format.sh, guard-git-stage.sh, guard-destructive-git.sh, guard-review-supervisor-scope.sh
PreToolUseWriteguard-work-unit-write.sh
PreToolUseEditguard-work-unit-write.sh
PreToolUse.*hook-logger.sh
PostToolUseBashhook-logger.sh, assert-tests-pass.sh
PostToolUse.*hook-logger.sh
PostToolUseFailure.*hook-logger.sh
UserPromptSubmitnonehook-logger.sh
Stopnonecontinue-orchestration.sh
SubagentStartnonehook-logger.sh
SubagentStopnonehook-logger.sh
PreCompactnonehook-logger.sh
PermissionRequestnonehook-logger.sh
Notificationnonehook-logger.sh
plugin/devbench-orchestrate/hooks/hooks.json

These ten event names are registered handlers. Whether a given Claude Code build emits all ten was not verified; the safe phrasing is "the plugin registers handlers for ten hook event types".

What each guard enforces

GuardScopeBlocksOperator override
guard-bash.shall Bash callers10 literal destructive substrings: rm -rf, rm -fr, git push --force/-f, git reset --hard, git checkout --, git clean -f/-fd/-fdx, and backgrounded output suppressionnone
guard-destructive-git.shall Bash callers13 regex-anchored git patterns, each with a prescribed alternative: git rm --cached, reset --hard, checkout --/checkout ., clean -f, force push, branch -D, filter-branch, update-ref -d, rebase -i, commit --amend, and --no-verify / --no-gpg-sign on any git commandDEVBENCH_ALLOW_DESTRUCTIVE_GIT=1
guard-git-stage.shall Bash callersRule 1: git commit with an empty index. Rule 2: git add <path> where the path is not a row in the active work unit's ## Changes Manifest; the block message prints both the offending paths and the manifestnone
guard-verdict-format.shlog-verdict callsMalformed verdicts (fewer than 3 positional args, unknown judge name, verdict that is not pass/fail, fail with empty feedback). Plus a scoped rule: when agent_type == devbench-orchestrate:executor, all five canonical reviewer verdicts are blocked: the executor cannot self-attest a reviewnone
guard-comment-format.shlog-comment calls11 forbidden control-language phrases in a comment body: halt orchestration, halting orchestration, halt the loop, halt loop, stop the loop, stop orchestration, abort orchestration, operator action required, resume orchestration once, emergency halt, do not continue. The block message supplies rewritesnone
guard-review-supervisor-scope.shscoped to agent_type == devbench-orchestrate:review-supervisor (a no-op for every other caller)19 mutation command patterns (rm, rmdir, mv, cp, chmod, chown, chgrp, touch, mkdir, truncate, dd, tee, sed -i, awk -i, git mutation verbs, git branch -d/-D, git config writes, find -exec/-delete, xargs into destructive commands), plus > and >> redirection, plus any Agent spawn outside a four-name allowlistDEVBENCH_ALLOW_REVIEW_SUPERVISOR_MUTATIONS=1
guard-work-unit-write.shWrite and Edit on backlog/**/*.mdRule 10: em-dash (U+2014) in work-unit content. Rule 11: Changes-Manifest or Source: paths carrying a checkout_directory prefix. Role gate: writes are refused unless DEVBENCH_AGENT_ROLE=orchestrator; a missing or unrecognized role defaults to BLOCKDEVBENCH_AGENT_ROLE=orchestrator
assert-tests-pass.shPostToolUse on BashA matched test command (pytest …, make test/test-unit/test-functional/validate) that exited non-zero, so the loop cannot walk past a red suitenone
continue-orchestration.shthe Stop eventThe session ending while a task is in progress (see below)stop_hook.* config
hook-logger.sh11 of the 21 registrations, covering 9 of the 10 event types (everything except Stop)Nothing. It appends one JSON line per event to hook-logs.jsonl and always exits 0n/a
plugin/devbench-orchestrate/scripts/*.sh (guard header comments and pattern arrays)

The Agent-spawn allowlist

guard-review-supervisor-scope.sh permits exactly four subagent_type values (the four review-team judges) and blocks anything else. Its header explains the loophole it closed: the previous Bash-only guard "let the supervisor escalate by spawning subagents (executor / git-ops) via the Agent tool", collapsing the documented pipeline into one mega-step.

Two orphans by design and by accident

Of the 12 script files, _hook_lib.sh is a shared library sourced by nine scripts (correctly unregistered), and guard-backlog.sh is dead code: present, executable, documented as active in docs/plugin-architecture.md, but referenced nowhere in hooks.json. Exactly 10 distinct scripts are wired.

The Stop hook: what keeps a prompt-driven loop running

continue-orchestration.sh is the only Stop handler and the only script that emits a JSON decision envelope: {"decision": "block", "reason": "…", "hookSpecificOutput": {…}}. It always exits 0: the block lives in the JSON, not the exit code. Its purpose, per its own header, is to "prevent the orchestrator from stopping mid-loop" and to inject a continuation instruction "so Claude re-enters the loop without human intervention".

Configuration: env var > YAML > hard default

SettingEnvYAMLDefault
Blocks before allowing stopDEVBENCH_STOP_MAX_BLOCKSmax_blocks5
Circuit-breaker windowDEVBENCH_STOP_WINDOW_SECONDSwindow_seconds180 s
Stale-task thresholdDEVBENCH_STOP_STALE_MINUTESstale_task_minutes120 min

Last action → injected next step

Detected last commentInjected instruction
executor completedInvoke review-supervisor; run the four review agents
review passCheck whether all four passed; if so invoke security-reviewer
review failRe-run executor with prior feedback, then re-run review-supervisor
security passRun git-ops <id> then mark-done <id>
git-ops completedRun mark-done, then loop to validate-backlog && next
task doneLoop back: validate-backlog && next
unknownread-unit <id> to reload context, then continue

Circuit breaker and forensics

The counter resets when the window expires. At max_blocks the breaker allows the stop, writes [CIRCUIT_BREAKER] Allowed stop after N blocks in Ns. Human intervention may be needed. as an audit comment and clears its state file (/tmp/devbench-stop-hook-state-<session>.json, per-session so concurrent orchestrators keep independent counters). Every invocation also writes a forensic JSON file to <workspace>/.devbench/stop-hook-diag/<timestamp>-<task-id>.json capturing the task, last action, block count, window, elapsed seconds, stale warning, next step and the exact stdout emitted. The stated purpose: "so a future hang can be post-mortemed from evidence rather than speculation."

plugin/devbench-orchestrate/scripts/continue-orchestration.sh:24-32, :41-63, :135-165, :188-216, :246-271

Why the guards are written in jq and pure bash

Three separate real-bug fixes are encoded in the scripts and worth citing when people ask why hook code looks defensive: python3 resolution "fails silently under asdf-shim PATHs", which made guards bow out and let dangerous calls through, so jq is tried first everywhere; local -n namerefs broke every PreToolUse hook on stock macOS bash 3.2 and were replaced with indirect reads; and a shlex.split tokeniser was silently returning zero tokens, causing legitimate verdicts to be blocked as "missing arguments".

scripts/_hook_lib.sh:8-10, :31-36, :91-107 · scripts/guard-work-unit-write.sh:27-33 · scripts/guard-verdict-format.sh:71-81 ↑ Back to contents
Section 04

Skills: five, across two plugins

A skill is a SKILL.md prompt Claude Code loads by name. DevBench ships five: four authoring skills that a human drives interactively, and one execution skill that is the orchestration loop. Amendment handling, block resolution, reporting and tailing are not skills: they are agents and CLI commands.

SkillPluginModelToolsLinesProduces
create-specauthoringopusRead, Write, Edit, Bash236spec/<project>.md: 18 canonical sections under 16 top-level numbers (0 through 15, plus 3.5 and 3.6), target size "1000+ lines for non-trivial programs"
spec-to-backlogauthoringopusRead, Write, Edit, Bash449BACKLOG.md + one markdown file per leaf task, in a 4-level Epic → Feature → Story → Task hierarchy, each task carrying 15 canonical sections
configure-devbenchauthoringsonnetRead, Write, Edit, Bash496backlog/config/devbench.yaml plus a devbench-commands.txt launcher file (a 16-step walkthrough, each section round-tripped through the real config loader before the file is written)
bootstrap-environmentauthoringsonnetBash, Read, Edit (no Write)218Cloned repos, installed asdf toolchains, and a make validate baseline per repo, with a self-verify retry loop and operator-gated escalation
orchestrateorchestratenot pinnednot pinned166The execution loop itself. Declares only name and description; the orchestrator process model comes from DEVBENCH_CLAUDE_MODEL, and each work agent loads its own model from frontmatter
SKILL.md frontmatter, both plugin trees · src/devbench/cli.py:6515

The chain

create-spechuman-driven spec-to-backloghuman-driven configure-devbenchhuman-driven bootstrap-environmenthuman-driven make startautonomous

The chain crosses a plugin boundary between step 4 and step 5. The migration guide is explicit that these should be sequential in one workspace, never concurrent: "First install authoring at project scope, materialise the backlog, then uninstall authoring at project scope and install orchestrate at project scope."

docs/onboarding.md:1-13 · docs/migration-0.4.0.md:118-122

Correct invocation names post-0.4.0: devbench-authoring:create-spec, devbench-authoring:spec-to-backlog, devbench-authoring:configure-devbench, devbench-authoring:bootstrap-environment, devbench-orchestrate:orchestrate. Only the last one is automatic: devbench start issues its prompt for you.

What the spec must contain

Eighteen canonical sections under sixteen top-level numbers (0 through 15, with 3.5 and 3.6 as separate sections), every one present or explicitly marked "N/A -- reason" (the skill's own heading says "these 16 top-level sections" and then enumerates 18 bullets): behavior-change items · context · goals · existing primitives to reuse · standards audit · trust model · new command surface · data format · version and interoperability semantics · error handling, logging and configuration · documentation updates · parallel or multi-repo scope · testing requirements · completions/integrations matrix · out of scope · resolved decisions · CLI --help reference · future work.

Authoring is an interview (17 numbered questions in 7 blocks), then section-by-section drafting, then a bounded self-critique loop against an 8-item rubric, then operator sign-off before the file is written. The skill emits a [QUALITY_REFERENCE] audit line naming the exemplar it used, or the literal token <embedded-canonical-sections> when none was configured, so the audit trail proves no external exemplar was consulted.

plugin-authoring/devbench-authoring/skills/create-spec/SKILL.md:38-55, :186-192, :205

What the backlog must contain

Fifteen canonical sections per task file, in required order: title · ## Status: · Target Repository · Description · Definition of Ready (5 task-tailored items) · Depends On This · Approach (task-specific RED/GREEN/REFACTOR steps) · Code Standards (6 subsections) · Related Specifications · Dependencies · Acceptance Criteria · Changes Manifest (exactly 2 columns) · Definition of Done (~9 items) · TDD Cycle Log (header only) · Comments (header only).

Regex-parsed markdown, no frontmatter

Status, repo, branch, dependencies and ACs are extracted by named regexes in constants.py. The Changes Manifest is a strict 2-column table; a row with any other column count raises ManifestParseError. Globs are rejected outright; five named sentinels cover execution-determined file lists.

Deterministic post-processing, then validation

The skill calls an 8-pass deterministic post-processor (manifest column normalization, pipe sanitisation, row dedupe, code-standards drift check, index regeneration, dep-ID normalization, N/A suffixing, orphan-path (ref) suffixing) and then validate-backlog, which enforces 20 rules. Exit requires all three: validate-backlog rc=0, every leaf task passing its 12-item rubric, and the Status Summary total matching the index row count. (The rubric enumerates 12 items; the skill's own exit condition still says "all 10 items", an internal drift in the skill text.)

spec-to-backlog/SKILL.md:39-61, :214-227, :315-319 · docs/backlog-contract.md:162-186 · docs/skills/backlog-post-processor.md

Bounded loops, resumable runs

Both authoring skills run an iterate-until-perfect loop bounded by two constants: SKILL_MAX_ITERATIONS = 5 (exhaustion emits [SKILL_MAX_ITERATIONS_REACHED] and exits non-zero) and SKILL_QUALITY_THRESHOLD = 0 (reaching it emits [SKILL_QUALITY_THRESHOLD_REACHED] and exits success). Checkpoints live under .devbench/skill-state/; spec-to-backlog keeps a second per-task checkpoint so a crash resumes mid-backlog instead of regenerating every file. Above skills.fan_out_threshold (default 10) leaf tasks, it spawns one sub-agent per Feature to author that Feature's tasks in parallel.

src/devbench/constants.py:799-807 · src/devbench/config_loader.py:568-569

The orchestrate loop's own discipline

Three CRITICAL directives sit at the top of the execution skill, and all three exist because of observed failure modes: never stop to ask the user (the only valid exits are ALL_DONE or NO_ACTIONABLE); after every Agent tool result the next tool use must be another Agent call or a devbench Bash call ("ending your turn before emitting that next tool call is a loop-exit bug"); and ending a turn with recap prose is forbidden, because Claude Code reads a turn-end-without-tool-call as "agent done" and fires Stop. That last rule is regression-pinned by a test.

Correspondingly, the loop halts on exactly two signals: devbench next returning ALL_DONE/NO_ACTIONABLE, and the stop-hook circuit breaker. Everything a subagent says is diagnostic: "Subagent text is diagnostic, not control flow." That prompt rule is backed by the deterministic guard-comment-format.sh floor.

SKILL.md:8-12, :139-166 · tests/test_integration/test_orchestrate_skill_no_recap_anti_pattern.py ↑ Back to contents
Section 05

Work-unit lifecycle

Nine statuses, two of them terminal, two of them claimable. Four exist specifically as human-decision parking states.

StatusMeaningClaimableTerminal for rollupWritten by
draftAuthored but not approved for autonomous claim. Valid only on Task work unitsNoNoauthoring skill; backlog.default_status_for_new_work_units
in-queueReady to be picked upYesNopromote, set-status, auto-requeue cascades
in-progressAn agent is implementing itYes (resumed first)Noclaim
in-reviewPR open, awaiting human review or mergeNoNogit-ops under pause_before_merge
doneMerged and closedNoYesmark-done (done-gate enforced); auto-rollup for parents
blockedRetry budget exhausted, dependency unmet, amendment rejected, security fail, or git-ops hard failureNoNomark_blocked, sync-blocked, set-status
proposedAuto-emitted draft awaiting operator promote or rejectNoNoproposal materialisation (legacy / hand-authored path)
declinedWill never be done: a final operator decisionNoYesdecline <id> --reason
holdDeferred / under debate. Explicitly non-terminal: a held child keeps its parent openNoNohold / unhold, operator only
src/devbench/constants.py:145-177 · src/devbench/backlog/work_unit.py:52-63 · src/devbench/backlog/manager.py:80

Happy path and side branches

draft → in-queue → in-progress → in-review → done

in-queue | in-progress | in-review  → blocked   (non-terminal)
in-queue | in-progress              → hold      (non-terminal)
in-queue | in-progress | blocked    → declined  (terminal)
Precision matters here

There is no transition matrix in code. _set_status validates only that the target value is a member of VALID_STATUSES; legality is enforced by which function you call, and force_status bypasses even the done-gate (the cascades use it). Present the table above as "transitions the system performs", not "transitions the system enforces".

src/devbench/backlog/manager.py:148-180, :1003-1005

Claimability and the scheduling truth

Only in-queue and in-progress Task work units are actionable; proposed, draft, hold, blocked, declined and in-review are all inert to the scan. But "the next item" is not simply the earliest queued one:

BacklogParser.get_parallel_candidates filters to (status is in-queue or in-progress, type is TASK, all dependencies satisfied) and then sorts by (status_priority, topological_depth, id) with IN_PROGRESS = 0 and IN_QUEUE = 1.

So: interrupted work is resumed before new work is started; then queued work is ordered by topological depth (tasks with no declared dependencies first); with the lexicographic ID as a stable tiebreaker. Depth is computed across the full backlog with memoised recursion and cycle protection: self-loops and unresolvable IDs collapse to depth 0.

src/devbench/backlog/parser.py:328-414, :412-413

A dependency is satisfied when its status is done or declined. A dep on an Epic/Feature/Story is satisfied when every descendant task is terminal; a parent with no task descendants is vacuously satisfied. An unknown dep ID is treated as satisfied so a typo cannot deadlock the loop; validate-backlog reports it as an integrity error instead.

Claim is race-safe

devbench claim <id> refuses a work unit whose Changes Manifest still contains a TBD placeholder row (the same rule as validator 19, enforced a second time at claim time), acquires an exclusive flock(BACKLOG.lock), re-reads the on-disk status under the lock, and raises ClaimRaceError without writing if the status changed. Every transition into in-progress appends a [WU_CLAIMED] audit comment stamped with the session name.

src/devbench/cli.py:1014-1114 · src/devbench/backlog/manager.py:1023-1035

The done-gate

mark-done refuses unless the most recent review round carries a [REVIEW_PASS] from all five canonical judges: code_review, test_review, doc_review, changes_manifest and security_review. The round boundary is the most recent [REVIEW_REJECTED] line: the reader walks the Comments section in reverse and stops there. In practice it never stops early, because no code path writes that token. log-verdict emits only REVIEW_PASS or REVIEW_FAIL cli.py:3607, and [REVIEW_REJECTED] appears in src/ only as a read pattern manager.py:1092 · proposal.py:247, so the window degenerates to the whole comment history. Verdicts from executor, blocker_resolver, manifest_amender and task_factory are accepted by log-verdict but do not count.

On success, _set_status writes the status line in the work-unit file, updates the BACKLOG.md row, rewrites the Status Summary table, ticks every checkbox in Acceptance Criteria and Definition of Done, writes the per-task window-stats aggregate, fires the auto-requeue cascades for anything blocked on this unit, and rolls the parent up. Rollup cascades Task → Story → Feature → Epic and deliberately bypasses the done-gate: parent units are structurally done when their children are, and need no judge review.

src/devbench/backlog/manager.py:182-210, :978-1076, :1078-1097, :1099-1138 · src/devbench/constants.py:204-215

Retry budgets

Global budget max_executor_retries (default 10, env DEVBENCH_MAX_RETRIES), optionally overridden per judge via max_executor_retries_per_judge; the block trips the moment any single judge's budget is exhausted. CI-failure retries (git-ops rc=2) and PR-bot-feedback retries (rc=3) share the same budget, so total per-task work stays bounded. Security review is never retried.

src/devbench/constants.py:258 · SKILL.md:91, :104-105 ↑ Back to contents
Section 06

Block resolution: seven paths

Every work unit whose status is blocked is classified into exactly one of seven states by a single pure function, classify_blocked_task, evaluated first-match-wins in priority order. The classification decides what the operator sees, which notification fires, and whether anything automatic will move the task.

# · StateTriggerAuto or humanBounded by
0 · RUNTIME_DEGRADATION The SDK silently dropped the Agent tool from the review-supervisor session, leaving only Bash. Caught by the supervisor's own Step-0 self-check, which logs [BLOCKED] agent-tool-unavailable and exits FAIL rather than treating an empty reviewer list as "all passed" Automatic orchestrator restart (exit code 42) 24 h detection window, a last-restart marker, and DEVBENCH_MAX_AUTO_RESTARTS (default 3). Then a human takes over
1 · HELD The unit's own status is hold, a deliberate operator pause Human at both ends Nothing automatic exists. devbench unhold releases it
2 · BLOCKED_ON_HELD Carries a [BLOCKED_PENDING_PROPOSAL] marker whose target is on hold. Because hold is non-terminal, the cascade can never fire Human must unhold the target Converts to path 3 the moment the target is released
3 · AUTO_CLEARING_VIA_PROPOSAL The recovery cascade is in flight: markers point at promoted proposal tasks that are not yet terminal. This is the terminus of the executor → amender → blocker-resolver → task-factory chain Automatic once every marker target is terminal orchestrate.max_cascade_depth (default 2); becomes a human approval gate when task_factory.auto_accept_proposals: false
4 · AWAITING_DEPENDENCY No marker, but a row in the ## Dependencies table points at a task that is not yet terminal Automatic: two independent mechanisms Event-driven cascade when the dep completes, plus the sync-blocked sweep. No timer, no escalation
5 · AWAITING_AMENDMENT_RECOVERY A recovery artifact is on disk (a pending proposal JSON, a rejected-amendment archive, or a recent [BLOCKED] comment from a recovery agent), proving the recovery loop is mid-flight Automatic on the next sweep 30-minute window (debug.blocked_recovery_window_seconds, default 1800). On expiry it reclassifies to path 6, the one automatic path-to-path transition in the system
6 · OPERATOR_ACTION_REQUIRED The catch-all: retry budget exhausted, security FAIL, git-ops hard failure (rc=1), PR closed without merge, amendment rejected with task-factory disabled, cascade-depth cap reached, or a recovery window that expired Human, always No automation whatsoever. It also suppresses the path-0 auto-restart: one operator block and the orchestrator will not self-restart
src/devbench/backlog/proposal.py:167-210 (enum), :431-470 (classifier) · pinned at tests/test_backlog/test_proposal.py:3155-3168

The seven states are mirrored consistently across the operator surfaces: seven sub-buckets in devbench status, seven panels in devbench report (each with a resolution hint), and seven of the sixteen notification events map to them 1:1. The report's panel router raises RuntimeError on an unhandled enum member rather than silently defaulting; the codebase forbids silent fallbacks.

Full step-by-step flows for all seven paths (every agent, CLI call, marker, cascade and config knob, with the human checkpoints marked) are drawn in the interactive diagram. Open the architecture diagram →

↑ Back to contents
Section 07

Configuration surface

One file (backlog/config/devbench.yaml), validated against a draft-07 JSON Schema with additionalProperties: false at the root and on every nested object except report.models, which is an intentionally open map of model id → rate table so operators can price new models without a code change. Any other unknown key is a hard failure at load. 24 top-level keys, 112 leaf keys (109 live plus 3 retired-but-schema-accepted so the runtime can fail fast with a message naming the replacement).

Discovery

--config <path>DEVBENCH_CONFIG_PATH<workspace>/backlog/config/devbench.yaml. The flag is stripped out of argv and turned into the env var before the config module is imported, because that module loads YAML at import time.

Precedence

environment variable > YAML value > code default, implemented by typed _resolve_* helpers. Booleans accept 1/0/true/false/yes/no/on/off case-insensitively and raise on anything else; an empty env string counts as unset.

Validation

Missing file, YAML syntax error, non-mapping root, schema violation and cross-field violation each raise with an actionable message. Schema errors include the dotted field path.

src/devbench/config_loader.py:1180-1202, :1358-1677 · src/devbench/cli.py:83-92

Two mandatory environment variables

Both are checked at devbench.config import time and exit 2 with a one-line diagnostic if unset:

DEVBENCH_WORKSPACE_ROOT
Absolute path to the workspace root: the parent directory holding BACKLOG.md, backlog/ and the target repo checkouts as siblings. It is not the backlog repo itself.
DEVBENCH_CLAUDE_MODEL
Model identifier for the SDK caller. It governs the orchestrate skill's coordination calls only; it does not route the work agents, each of which loads its model from plugin frontmatter or an agents: override.

In total, 75 environment variables are read by the Python source, the two above included (64 DEVBENCH_*, 6 legacy JUDGE_*, 5 third-party: AWS_REGION, GH_TOKEN, NO_COLOR, USER, USERNAME), plus 16 read only by shell: the Makefile, the plugin hooks and the EC2 provisioning targets.

src/devbench/config.py:187-192, :332-335 · sample-config.yaml:47-52

The git_ops mode matrix

Four mutually exclusive modes decide PR granularity and who merges. This is the primary human-checkpoint surface in the whole system.

ModeYAMLPR granularityWho mergesWhere the human sits
Multi-PR (default)no mode flagsOne branch + one PR per work unit (backlog/<id>)DevBench, on green CIReviews merged PRs after the fact; can gate with pr_review_resolution
Pause-before-mergepause_before_merge: trueOne PR per work unitHumanPR is pushed, CI watched to green, unit moves to in-review and stops. check-merge reconciles later
Single-branch + defer-PRsingle_branch: <name> + defer_pr: trueOne batch PR per repoHuman by defaultNothing is pushed until git-ops-finalize. Opt in to auto_finalize (opens the PR) and then auto_merge (merges it) to hand each step over
Local-onlysingle_branch: <name> + defer_pr: true + local_only: trueNo PR at alln/aNo remote, no push, no CI: for operational workflows such as teardowns, audits and evidence capture
docs/git-ops-modes.md:13-18 · src/devbench/cli.py:4684-4795

Eight named mutual-exclusion errors, enforced at config load

RuleWhy
defer_pr requires single_branchThere is no deferred branch to finalize otherwise
pause_before_merge cannot combine with defer_pr"defer_pr defers PR creation; pause_before_merge pauses after PR creation. They are mutually exclusive."
pause_before_merge cannot combine with single_branch"there is no per-unit branch to create a PR from"
local_only requires defer_pr"Local-only repos have no remote to push to"
local_only requires an explicit default_branch on every repo"There is no origin to fall back to in local-only mode"
auto_finalize requires defer_pr"without defer_pr there is no deferred branch to finalize"
auto_finalize cannot combine with local_only"Local-only repos have no remote to push to"
auto_merge requires auto_finalize"without auto_finalize there is no PR to merge"
src/devbench/config_loader.py:1316-1335, :1434-1491

The human-gate knobs

These are the settings that decide how much rope the system gets. Defaults are the code defaults from config_loader.py. They are worth stating precisely, because two of them are documented backwards in the repo (see section 10).

KnobDefaultWhat turning it changes
git_ops.pause_before_mergefalseEvery PR waits for a human merge; the unit parks at in-review and the loop moves on
git_ops.auto_mergefalseOff means a human merges by default in batch mode. On requires auto_finalize + defer_pr
git_ops.pr_review_resolution.enabledfalseTurns on PR review-comment polling; the whole phase is a no-op while false
git_ops.pr_review_resolution.decision_blockstrueA human CHANGES_REQUESTED review hard-blocks the merge regardless of the bot allowlist; this is the human-reviewer merge gate
git_ops.pr_review_resolution.agents[]GitHub logins (e.g. review bots) whose unresolved comments block the merge; polled for settle_seconds (60) at poll_interval (5)
git_ops.ci_failure_retrytrueCI failure returns rc=2 and the executor retries with the trimmed failing-job log as feedback. false blocks on first failure
backlog.default_status_for_new_work_unitsin-queueSet to draft to make every new work unit invisible to the orchestrator until a human promotes it
backlog.bulk_update_confirm_threshold10Above this many units, set-status with selectors prompts for confirmation (0 = always prompt; --yes skips; --dry-run previews)
task_factory.enabledfalseThe whole blocker-resolver → task-factory recovery chain is opt-in and off by default. Requires manifest_amendment.enabled: true
task_factory.auto_accept_proposalstrueTrue means generated drafts are auto-promoted at sweep time: no human approval gate. Set false to make every proposal wait for promote-proposal / reject-proposal
manifest_amendment.enabledtrueOn by default. When off, an executor that needs an undeclared file must escalate to a human instead of amending
manifest_amendment.max_requests_per_execution1Caps amendments per executor run, preventing amendment loops
orchestrate.max_cascade_depth2Cap on recovery-of-a-recovery depth; exceeding it escalates the source task to the operator instead of materialising another layer
stop_hook.max_blocks / window_seconds5 / 180How long the Stop hook fights to keep the loop alive before conceding and letting the session end
validate.check_orphan_path_tokenstrueValidator rule 20: backtick-quoted path tokens in ACs/DoD that are absent from the Changes Manifest become errors. Rules 1–19 always run
DEVBENCH_SAFE_PERMISSIONS=1unsetThe coarsest gate of all: launches the interactive session without --dangerously-skip-permissions, so a human approves every tool call
src/devbench/config_loader.py:418-441, :532, :92-97 · src/devbench/constants.py:260-262, :277, :334-366 · Makefile:133-139

Scope: authorizing a slice of the backlog

Scope is a printer-pages selector persisted to scope.json. E2 matches epic E2 and every descendant; E1-E3 expands inclusively across epics; E5-F1-S1-T2-T5 expands across tasks; comma-separated tokens are unioned. Reverse ranges and malformed tokens raise immediately; out-of-range tokens warn without aborting. Written either by devbench start --include … or by devbench scope set; the skill honours the file, not the command that wrote it. Before registering a session, start refuses a scope that overlaps an active session unless --allow-overlap is passed.

src/devbench/scope.py:57-79, :114-201 · SKILL.md:32

Per-agent model overrides

Every field in the agents: block defaults to null, meaning "use the plugin frontmatter". Setting one materialises a workspace-local shadow plugin tree and rewrites that agent's model: line. Values containing haiku (in any form, short name, Anthropic ID or Bedrock ARN) raise at config load with no override path.

Marked as unverified

The use_bedrock / bedrock_region keys are described in the schema as routing LLM calls via AWS Bedrock. Research found their only consumers in src/devbench to be the model-string format validators: no Bedrock client construction, and no reference to SDK routing variables. Treat these keys as selecting which model-ID format the agents: block accepts; do not claim DevBench routes traffic through Bedrock without further verification.

src/devbench/config.py:504-508, :551, :574-579 · config_loader.py:993-1060, :1583
↑ Back to contents
Section 08

CLI: 56 commands

A single registry (_COMMANDS: dict[str, tuple[Callable, int, str]]) maps each name to its handler, minimum positional arity and description. 28 of the 56 are "variadic": they own their flag parsing and receive the raw trailing argv; the rest get a fixed-arity slice and warn on extras. Across all of them there are 52 distinct DevBench long flags.

The CLI is deliberately the only way agents touch state. Agents cannot open files or run git freely; they call devbench, and the guards police those calls.

Orchestrator lifecycle & sessions (10)

Start, detach, observe and stop runs. drain is the graceful path: it asks the loop to finish the current unit and exit rather than killing it mid-task.

startstopstop-instancerestartinstancessessionstaildrainwatchdogscope

Backlog state & lifecycle (14)

Everything that reads or writes work-unit status. next and claim are the loop's entry points; hold, unhold, decline and set-status are the operator's.

statusnextclaimset-statusmark-donepromotedeclineholdunholdnew-taskvalidate-backlogsync-blockedreconcile-cascadeadd-dep

Agent bridge: evidence & audit (8)

The read side that gets injected into agent prompts, and the write side that produces the audit trail. Three of these are guarded for format and content.

read-unitget-diffrun-testsloglog-commentlog-verdictlog-tddlog-rejection-feedback

Amendments & proposals (9)

The mid-execution scope-change workflow and the recovery chain that turns a legitimate rejection into new, reviewable work units.

request-amendmentapply-amendmentreject-amendmentwrite-proposalmaterialise-proposalsweep-proposalslist-proposalspromote-proposalreject-proposal

GitOps (6)

check is the pre-flight (symlinks, origin remotes, default_branch parity, no conflicting open PRs). git-ops is the whole commit → PR → CI → merge path behind one exit-code contract.

checkensure-branchgit-opsgit-ops-finalizecheck-mergecleanup-tracked-orphans

Observability & cost (8)

Three live surfaces plus the cache-maintenance and cost-calibration commands. See section 9.

reportwatchhook-tailwrite-snapshotrebuild-window-statsarchive-sessioncost-calibratenotify-test

Plus one runtime-support command: prepare-plugin-shadow, which materialises the per-agent model-override shadow plugin and prints its path. 10 + 14 + 8 + 9 + 6 + 8 + 1 = 56.

src/devbench/cli.py:9379-9677 (_COMMANDS), :9678-9730 (_VARIADIC_COMMANDS) · devbench --help

The git-ops exit-code contract

One command, four outcomes, and the orchestrate skill branches on the number, never on prose.

rcMeaningOrchestrator action
0PR merged, or the commit landed locally in deferred modeContinue to mark-done
1Hard failure requiring operator attentionBlock the task, log [BLOCKED] with the exact failure surface, return to step 2
2CI failed, retry budget not exhaustedWork unit gains [CI_FAIL] naming .devbench/ci-failures/<id>-<n>.log; re-invoke the executor with a CI-fail payload, then re-run git-ops
3PR has unresolved review feedbackWork unit gains [PR_BOT_FAIL] naming .devbench/pr-bot-feedback/<id>-<n>.json; re-invoke the executor with a PR-bot payload
SKILL.md:99-105 · src/devbench/cli.py:4684-4795

Deterministic merge rails

Two assertions apply to every commit path and involve no LLM: assert_on_branch rejects orphan-branch commits, and assert_staged_matches_manifest rejects any staged path that is not declared in the Changes Manifest. On top of those, a merge can be stopped by the orphan-pattern gate, by CI (check-registration retry exhaustion refuses the merge, an explicit no-fallback decision), by unresolved PR review feedback, by pause_before_merge, or by batch mode leaving the PR open for a human.

src/devbench/github/git_ops.py:226, :673-686 · src/devbench/cli.py:4731-4734 ↑ Back to contents
Section 09

Observability

Three read-only surfaces, each answering a different question, plus an append-only audit trail that lives in the work-unit files themselves.

CommandQuestion it answersShape
devbench report"How far along, how fast, how much has it cost?"Windowed metric tables; streams by default on a TTY
devbench watch"What is the orchestrator doing right now?"One-screen snapshot across 8 panels
devbench hook-tail"What happened as it happened?"Append-only colorized event stream

report

Renders 19 metric rows per window across two windows by default: all-time, and the current run (bounded by a session gap). Four rows are window-agnostic and span the table: recent pace, estimated time to complete, estimated completion date, and estimated total cost at completion. The first line is always a liveness banner ([ORCHESTRATOR ALIVE] / [STOPPED] / [STARTING]), derived from log recency against stop_hook.window_seconds.

Beyond the metrics: in-progress rows carry a humanised attempt duration, plus a Declined panel, a pending-proposals panel, and seven blocked panels, one per BlockedTaskState, each with a resolution hint written for the operator:

PanelHint shown to the operator
auto-clearing via proposal"Resolves when marker targets reach terminal; no action."
awaiting amendment recovery"Recovery agent in flight; orchestrator's next sweep advances these."
awaiting dependency"Resolves when the dependency completes; no action."
held"On hold by operator; unhold to release."
blocked-on-held"Waiting on a held unit; unhold the target or redirect this task."
runtime-degradation"SDK lost Agent-tool access mid-session; task remains blocked until the orchestrator restarts (auto on NO_ACTIONABLE exit; otherwise manual make start)."
operator action required"No automation path; operator must inspect and resolve manually."
src/devbench/reporting/report.py:1795-1814, :2275-2371 · docs/architecture.md:86-87, :106

hook-tail: the JSON firehose, made readable

hook-tail pretty-tails $DEVBENCH_WORKSPACE_ROOT/hook-logs.jsonl, the raw stream every hook registration writes. Columns: timestamp (HH:MM:SS), a two-character event glyph, agent (12 chars), tool (8), description (120), and a stdout preview (80, PostToolUse rows only). All four widths are operator-tunable via hook_tail.*. Timezone resolves --tz > display_timezone > OS local; the log itself always stores UTC. Rotation is detected by inode change. There are deliberately no filter flags: pipe to grep or jq.

-> PreToolUse <- PostToolUse !! PostToolUseFailure U> UserPromptSubmit || Stop +s SubagentStart -s SubagentStop Cp PreCompact P? PermissionRequest No Notification

Because the catch-all PreToolUse/PostToolUse matchers are .*, every tool call by every agent is recorded, plus SubagentStart/SubagentStop at each agent boundary and PreCompact before every context compaction. Each record carries an orchestrator_session field so hook-tail can scope its output to the orchestrator's own session: a mid-run investigation in a side pane no longer pollutes the audit stream.

src/devbench/hook_tail.py · plugin/devbench-orchestrate/scripts/hook-logger.sh:7-22 · docs/hook-activity.md:52-96

Notifications: 16 events, all off by default

Slack is the only transport. A POST requires three independent switches to all be true: notifications.enabled, notifications.slack.enabled, and the specific notifications.events.<name> toggle. All default to false, and the entire block is commented out in the sample config. Webhook URLs are validated at config load (string or null, https:// only) and are expected to arrive via DEVBENCH_NOTIFICATIONS_SLACK_WEBHOOK_URL, never in tracked YAML.

Seven blocked events, mapped 1:1

work_unit_blocked_operator, _runtime_degradation, _held, _on_held, _auto_clearing, _awaiting_dependency, _amendment_recovery (one per BlockedTaskState, via _EVENT_BY_CLASSIFICATION).

Nine lifecycle events

work_unit_done, work_unit_materialised, work_unit_promoted, pr_opened, pr_merged, ci_failure, ci_pass, orchestrator_stop, orchestrator_auto_restart.

Firing is transition-gated, not level-gated: a ping fires when the classification actually changes (initial entry counts), and repeated observations are no-ops. The state cache at .devbench/notification-state.json is written on every call regardless of the toggle, so enabling a toggle later does not retro-fire a backlog of alerts. Notifications are dispatched only from write sites, never from the renderers, so re-rendering a report cannot spam a channel. devbench notify-test --event <name> smoke-tests one event.

src/devbench/notifications.py:82-99, :137-145, :595-663 · docs/slack-notifications.md:199-205

What is written where

ArtifactContentAuthoritative?
backlog/**/*.mdStatus, dependencies, Changes Manifest, acceptance criteria, and the append-only ## Comments / ## TDD Cycle Log audit trail: every judge verdict, every agent comment, every status marker, every TDD phaseYes (source of truth)
BACKLOG.mdThe index: ID, Title, Type, Status, Dependencies, Repo, File Path, plus a Status Summary tableDerived index; drift from the files is a validation error
hook-logs.jsonlOne JSON line per Claude Code hook event, with the full payload and the orchestrator session idAppend-only audit stream
logs/orchestrator.logThe Python-side structured log. The Set <id> to '<status>' line is what the reporter and the Stop hook grep forAppend-only
.devbench/stop-hook-diag/*.jsonOne forensic file per Stop-hook block: counters, detected last action, injected next step, exact emitted stdoutForensic
.devbench/report-cache/events.sqlitemtime + size + offset-keyed incremental parse cache and indexed event store (ADR-16, ADR-19)No (derived, rebuildable)
.devbench/window-stats/<task>.jsonPer-task aggregates written on every status transition, so the reader is O(task count) rather than O(log size) (ADR-17)No (rebuild with rebuild-window-stats)
.devbench/report-snapshot.jsonPre-rendered report written after each mark-done (ADR-20); deleting it is always safeNo (regenerated next loop)
logs/legacy/<session>.parquetOptional columnar cold archive of ended session logs (ADR-21, opt-in extra)No

SQLite is a cache, not a database of record. It ships with CPython, is not a declared dependency, and exists so report does not re-parse the whole orchestrator log on every run. There is no message broker, no database server, no external issue tracker and no queue technology in the data path: the queue is markdown files and an index.

docs/architecture.md:107-112 · pyproject.toml:22-27, :92 ↑ Back to contents
Section 10

Docs gaps this site closes

This page was generated by reading the repository (source files, plugin prompts, JSON schema, shell guards and the CLI registry) rather than by summarizing the repo's own documentation. That turned up a set of places where the docs lag the code. Every row below was verified against both sides. None of them are defects in the running system; all of them are reasons to trust the code over a prose page.

What a doc saysWhat the code doesWhy it matters
docs/plugin-architecture.md:115-119 has a "Model Per Role" table: executor on opus, four review judges on haiku, security-reviewer on sonnet Frontmatter says executor and review-supervisor on sonnet, the other eight agents on opus. Every row of the doc table is wrong It also recommends haiku, which config load rejects outright for every per-agent field (issue #198). Following the doc produces a config that will not load
docs/block-types.md:5-16 says "exactly one of six mutually exclusive states" The BlockedTaskState enum has seven members. RUNTIME_DEGRADATION is absent from the entire document An operator reading the doc has no name for the SDK-degradation case, the one blocked state that self-heals via restart. The doc-consistency test only checks a regex snippet, so the staleness is untested
docs/architecture.md:91 and ADR-11, on task_factory.auto_accept_proposals: "default is false so the 'human reviews every proposal' posture is preserved" In config_loader.py:419, the default is True. The flip is recorded in the CHANGELOG as a breaking change The single most consequential doc error found. It tells operators the human-review gate is on when it is off. (The chain is still opt-in overall, because task_factory.enabled defaults to false)
In docs/architecture.md:61, manifest_amendment is "opt-in via enabled: true" In config_loader.py:439, enabled: bool = True. docs/manifest-amendments.md states it correctly Two live docs give opposite answers about whether an executor may amend its own scope
docs/architecture.md:251,411 and docs/backlog-contract.md:471 say the done-gate checks "only" the four review judges manager.py:1097 returns passed >= ALL_REQUIRED_JUDGE_NAMES, which is the four review judges plus security_review, making five. Pinned by an explicit TestSecurityGate (tests/test_backlog/test_manager.py:525) The README's own "Five judges must pass before a work unit merges" is the correct statement
docs/block-types.md uses devbench list-blocked (11 times) and devbench show (13 times); SKILL.md:91 calls devbench config-resolve None of the three commands exist. They are absent from the _COMMANDS registry and no handler exists in src/. The real equivalents are status, report and read-unit An operator following the block-types runbook hits "unknown command" on the first step. This page lists only commands verified present in the registry
Seventeen sites (the Makefile, the README, ten docs and one shipped example) invoke --plugin-dir plugin/devbench, and the Makefile installs the plugin name devbench plugin/devbench does not exist. The constant is DEFAULT_PLUGIN_SUBPATH = "plugin/devbench-orchestrate", and the installable names are devbench-orchestrate@devbench and devbench-authoring@devbench-authoring The README gives the correct install command 220 lines after documenting the Makefile target that uses the retired one
Both plugin manifests declare "license": "MIT" The project is Apache-2.0: LICENSE, NOTICE, pyproject.toml and the README all agree, and the relicense commit is HEAD Those two strings are the only "MIT" in the repository; the relicense missed them. This site states Apache-2.0
docs/architecture.md:544-559 lists four scripts on the PreToolUse/Bash chain and puts hook-logger.sh on Stop hooks.json registers seven scripts on that chain (adding the comment-format, destructive-git and review-supervisor-scope guards) and registers only continue-orchestration.sh on Stop Three of the strongest guards in the system are invisible in the architecture doc's hook table
docs/plugin-architecture.md:49 documents guard-backlog.sh as an active guard blocking writes to backlog tracking files The script exists and is executable but appears nowhere in hooks.json. Exactly 10 of the 12 script files are wired Either dead code or an accidental de-registration; nothing in the CHANGELOG, ADRs or history explains it. Flagged for the maintainers, not characterised here. The registered guard-work-unit-write.sh covers the same surface more narrowly
Four doc sites still instruct operators to set DEVBENCH_DISABLE_INLINE_ORPHAN_CLEANUP=1 The variable was renamed to DEVBENCH_INLINE_ORPHAN_CLEANUP (truthy/falsy); the asymmetric "DISABLE" form was removed and is no longer read Setting the documented variable silently does nothing
docs/cli-reference.md is presented as the full per-command reference It documents 50 of 56 commands. Missing: reconcile-cascade, instances, stop-instance, tail, restart, notify-test, five of which are user-facing lifecycle commands Section 8 of this page lists all 56, grouped
The four docs/skills/ quickstarts use the pre-0.4.0 prefix (devbench:create-spec) and name a hardcoded exemplar path on a specific machine as "the canonical quality reference" The correct prefixes are devbench-authoring: and devbench-orchestrate:; the skills are application-agnostic and say "Do NOT default to any hardcoded path" No test asserts the post-0.4.0 prefix: the quickstart doc test only requires any one of four invocation patterns (test_skill_doc_has_invocation_example is a four-way or), so the stale form is accepted and CI will not catch it. This page uses the migration guide's forms
README's example-backlog row: "207 work units across 13 epics + 14 features" File enumeration: 13 epics, 37 features, 37 stories, 120 tasks = 207 The 207 and the 13 are right; the feature count is off by 23
docs/execution-modes.md:139,165 says "a blocker-resolver agent file exists in the plugin but the orchestrate skill does not currently invoke it" SKILL.md:77-81 invokes devbench-orchestrate:blocker-resolver explicitly at step 4c, and docs/architecture.md:619 lists that invocation as resolved One doc describes a system without automated block recovery; the skill implements it
In spec-to-backlog/SKILL.md:16, every generated task file opens ## Status: draft; configure-devbench/SKILL.md:326-328 says the default is in-queue Both are true for their own path. The RuntimeConfig default is in-queue, and the shipped new-task template also opens in-queue, but the skill reads the raw YAML itself and writes draft when the key is absent The two paths can disagree for the same workspace, so there is no single global default to quote. This page states the config default in section 7 and the skill behavior in section 5, separately
"devbench next returns the earliest in-queue item"; docs/architecture.md:623 lists "no topological sort for parallel candidates" as a remaining gap Candidates are sorted by (status_priority, topological_depth, id): in-progress work resumes first, then depth order (issue #121) The scheduling model is materially different from the folk description, and the "gaps" list is stale

Two things this site deliberately does not claim

Unresolved

Which identifier form Claude Code passes as subagent_type. The review-team agents' frontmatter names are hyphenated (code-reviewer), while guard-review-supervisor-scope.sh allowlists the underscored, plugin-qualified forms (devbench-orchestrate:code_review) and the unit test exercises only the underscored form. Research could not determine the runtime string from static files. The guard's allowlist is described here as written in the script, not as verified-working.

Unresolved

The BACKLOG.md index column set. The parser and the backlog contract require | ID | Title | Type | Status | Dependencies | Repo | File Path |, declared "required exactly". The spec-to-backlog skill's step-6 sample shows a different seven-column layout with no Type column, which parse_index would reject. It is plausible the deterministic index-regeneration pass writes the correct format and the sample is drift; that was not verified. It is the highest-risk internal contradiction found, and it is flagged rather than resolved.

How to read all of this. Every number, name and default on this page came from enumerating the repository at commit c42fc64: file counts, JSON Schema walks, the _COMMANDS registry, agent frontmatter, hooks.json, guard pattern arrays and the constants module. Nothing was estimated. Where the repository disagrees with itself, both sides are shown above and the code is treated as authoritative.

Nothing on this page was produced by running the orchestrator. Behavioral statements describe what the code path does on a matching input, not an observed production run.

↑ Back to contents