Skip to content

An ADW Script is a deterministic Python shell around one non-deterministic Claude Code call. The script makes the agent's work repeatable, composable, and observable.

Primitive

ADW Script

The core shape

Deterministic wrapper. Non-deterministic core.

Every ADW Script has the same structure: deterministic Python on both sides of one subprocess call to Claude.

1.load_dotenv() · parse args

2.ADWState.load() or create() — resume or start fresh

3.check_env_vars() · validate before invoking

4.make_issue_comment("🚀 Starting phase")

🤖

subprocess.run(["claude", "-p", "/command {args}"])

One call. The only unpredictable line in the file.

5.check returncode · gate on success

6.state.save() · persist progress for next phase

7.make_issue_comment("✅ Phase complete")

The agent is the only source of variance. Everything around it is a contract — validated inputs, checked exit codes, persisted state, observable events.

Memory across phases

ADWState is how phases hand off to each other.

Each phase reads the prior state, does its work, writes an updated state. One JSON file per workflow.

adw_plan.py

+ branch_name+ plan_file

adw_build.py

+ worktree_path+ commit_sha

adw_test.py

+ test_results+ pr_url

💾

agents/{adw_id}/adw_state.json

Single source of truth for the whole pipeline run.

issue_number · branch_name · plan_file · worktree_path · adw_ids[]

Build phase fails halfway? Resume from state. Re-running doesn't re-plan. State is what makes the pipeline restartable from any checkpoint.

Composition

Phases compose at the shell level — not in the prompt.

Combined scripts are sequencing wrappers. Each phase stays single-purpose.

uv run adw_plan.py 123

plan

uv run adw_plan_build.py 123

plan→build

uv run adw_plan_build_test.py 123

plan→build→test

Composed scripts call the phase functions, not the individual scripts.

Each phase remains independently invokable — the composition is mechanical sequencing, not a merged prompt.

Observability

The GitHub issue is the console.

Async, out-of-loop execution needs an async, out-of-loop reporting channel. make_issue_comment is that channel.

Issue #123 — "Add session token validation"

adw-a7f3_ops:🚀 Starting plan

adw-a7f3_ops:✅ plan complete

adw-a7f3_ops:🚀 Starting build

adw-a7f3_ops:📦 worktree created at /tmp/wt-a7f3

adw-a7f3_ops:✅ build complete

adw-a7f3_ops:🚀 Starting test

adw-a7f3_ops:❌ test failed — 2 assertions

Every comment prefixed with adw_id so parallel pipelines don't create an unreadable thread.

Triggers, crons, and humans all watch the same stream.

The catalog

The scripts in the Code Factory today.

adw_plan.py

issue number

spec in specs/

adw_build.py

adw_id + plan_file

code changes committed

adw_test.py

adw_id

test results on issue

adw_plan_build.py

issue number

plan + implementation

adw_plan_build_test.py

issue number

plan + impl + tests

Pink rows are composed scripts — they sequence the purple single-phase scripts.

What breaks if done wrong

Each failure mode is a missing contract in the shell.

✗Missing check_env_vars

Script runs against missing API keys. Cryptic subprocess errors, no meaningful failure message.

✗No state persistence

Build fails halfway → must re-run the plan phase. State is what lets you restart from the checkpoint.

✗Blocking subprocess

Long builds block the trigger loop. Use subprocess.Popen() for detached execution.

✗No returncode check

Script proceeds to next phase with bad output. Always gate phase transitions on exit code.

The discipline

Wrap every non-deterministic call in a deterministic shell.

The agent is one subprocess line. Everything else — state, validation, observability, error handling — is Python you can trust. That's how you get a pipeline instead of a pile of prompts.