CodeJob is a chain-of-responsibility orchestrator that drives a coding task
(defined in docs/PLAN.md) through a sequence of external AI agents and closes
the loop by publishing a new version.
All state lives in the frontmatter of docs/PLAN.md. There is no .env
state and no CHECK_PLAN.md: every phase transition is a git commit, so the full
loop runs identically on your machine or in GitHub Actions.
Architecture & diagrams: diagrams/CODEJOB_FLOW.md.
CodeJob distinguishes two kinds of action on a task, declared in the frontmatter:
| Role | Key | What it does |
|---|---|---|
| Executor | EXECUTOR |
Implements the plan and opens the PR; also applies corrections (commits on the PR branch). |
| Reviewer | REVIEWER |
Judges the PR and posts a native GitHub review (APPROVED / CHANGES_REQUESTED). Never commits code. |
The reviewer is an optional quality gate before the human — you still merge
the PR yourself, and the merge is what publishes. Correcting is just executing
again: by default the EXECUTOR applies the reviewer's feedback; set the optional
CORRECTOR key to route corrections to a different agent.
Everything stays in the PR conversation: the reviewer posts to the same thread where you already reply corrections, the executor reads it the same way it reads your comments, and codejob reads the review state to drive the state machine.
The first line of docs/PLAN.md must be ---:
---
PLAN: "feat: what this plan implements"
TAG: v0.2.0
EXECUTOR: jules
REVIEWER: none
---
# Plan — ...| Key | Who writes it | Required | Meaning |
|---|---|---|---|
PLAN |
human | yes | Commit message used when the loop closes (codejob 'msg' overrides it). |
TAG |
human | no | Explicit version (v0.2.0); omitted → gopush auto-bumps. |
EXECUTOR |
human | no | Agent that implements (default jules). |
REVIEWER |
human | no | Agent that reviews the PR; none/absent → human-only review. |
CORRECTOR |
human | no | Agent that applies review feedback (default: the EXECUTOR). |
REVIEW_GUIDE |
human | no | Path to extra review criteria (e.g. docs/REVIEW.md). |
STATUS |
machine | — | dispatch → running → reviewing → review. |
SESSION |
machine | — | Executor session id. |
REVIEW_SESSION |
machine | — | Reviewer session id. |
ROUND |
machine | — | Executor↔reviewer round count (capped, default 3). |
PR |
machine | — | URL of the PR opened by the executor. |
REVIEWED_COMMIT |
machine | — | The last agent commit codejob pull showed the reviewer. codejob close refuses while a newer agent commit exists. |
Unknown keys are ignored. STATUS: dispatch (or no machine keys) means "pending
dispatch".
go install webtyp.com/devflow/cmd/codejob@latest
# Every action is an explicit command:
codejob dispatch # send docs/PLAN.md to the EXECUTOR
codejob pull # fetch/fast-forward the agent's PR branch
codejob reply "..." # answer the agent
codejob approve # approve the plan
codejob close "msg" [tag] # merge the PR and publish
# Read-only status (no state changes)
codejobPreviously, running bare codejob advanced the loop blindly depending on hidden state in docs/PLAN.md (STATUS). A verified incident occurred where a reviewer ran bare codejob attempting to fetch an agent's corrections, but because the status was already review, the command executed a merge and publish instead. Now, codejob alone prints a harmless status block and stops; changes require explicit verbs like dispatch, pull, and close.
codejob commands always run from, and against, the local repo you are standing in — the one you dispatched from. There is nothing else to set up:
codejob dispatch(whenSTATUS: dispatch): sendsdocs/PLAN.mdto theEXECUTOR,STATUS→running. The executor is told to run every stage and open the PR without ever stopping to ask: a question pauses its session until someone answers, and the loop stalls. What it could not do goes under a final## Executor notesheading of the plan and in the PR description, so read that section first when reviewing. It is also told never to edit the plan's frontmatter.codejob pull:- When
STATUS: runningand no PR yet: reports the agent is still working, unless the session stopped: waiting for a reply, waiting for plan approval, finished without a PR, or failed. Then it prints that state and the agent's last message. - When
STATUS: runningand PR is ready: checks out the PR branch in this same local clone,STATUS→review. - When
STATUS: review/reviewing: uses a fast-forward rule (git fetch+git merge --ff-only) to sync your local branch with the PR branch without accidentally publishing it or overwriting local corrections.
- When
A PR that went through rounds holds several agent commits plus codejob's own (every commit
codejob writes starts with chore(codejob): ). Reading only the last commit, or only the net
diff, misleads in both directions: a round-1 fix looks like the whole PR, and a commit that deletes
what an earlier one added makes the net diff look empty. So codejob pull lists every commit,
oldest first, whenever it shows the PR (when it becomes ready, and on every later pull):
Commits on feat-x (2 agent, 2 codejob) — review each one, not only the last:
🆕 a9013a7 feat: ServiceCard google-labs-jules[bot] 3 files +190 −1
74f0aaf (codejob) status transition to review [pr …/pull/10]
🆕 034c3ea feat: ServiceCard google-labs-jules[bot] 2 files +39 −19
561d943 (codejob) status transition to review [pr …/pull/10]
⚠ 034c3ea deletes: tests/old_test.go
⚠ later commits removed 18 lines that earlier commits of this PR added
Net vs main: 4 files +221 −1
Inspect one: git show <sha>
🆕marks the agent commits you were not shown yet. Review each one (git show <sha>).⚠ … deletes:names every file a commit deletes;⚠ later commits removed N linesmeans the net diff hides work a later commit undid.codejob pullrecords the newest agent commit it showed you inREVIEWED_COMMIT(onechore(codejob): reviewed up to <sha>commit; when the PR just became ready, inside the status commit).codejob closerefuses commits you were not shown: with no review recorded, or with agent commits newer thanREVIEWED_COMMIT, it stops and tells you to runcodejob pull. Corrections you committed locally but did not push do not count (close pushes them); ones you already pushed appear in the nextcodejob pull.- Diagram and tests: diagrams/CODEJOB_PR_COMMITS.md.
Never gh repo clone the repo elsewhere or gh pr checkout by hand to inspect
a plan's PR — codejob pull handles it and correctly syncs branches for you.
When the session stopped, answer it from the same repo. Both read the session id from the
docs/PLAN.md frontmatter and use the Jules API key from the keyring:
codejob reply "Yes, delete Tilde too. Finish every stage and open the PR."
codejob approve # the session is waiting for plan approvalreply resumes the session (also one that finished without a PR: tell it to open the PR).
STATUS does not change; run codejob pull later to see the PR.
# Close the loop with a required message and optional tag override.
codejob close 'feat: implemented feature'
codejob close 'feat: implemented feature' v0.3.0Review corrections on the PR branch reach the merge either way: uncommitted changes are
committed as "review: corrections before merge", and the branch is always pushed before
gh pr merge, so corrections you already committed are not lost.
codejob close on a PR that GitHub already merged resumes: it pulls, deletes docs/PLAN.md and publishes; the review check is skipped because nothing is left to review.
# Scaffold the workflow and register the secrets from your keyring.
codejob --init-action # this repo only
codejob --init-action --org webtyp --visibility all # once for the whole orgAfter that, the loop runs without opening your PC:
- Edit the
docs/PLAN.mdheader and commit → the workflow dispatches the executor. - The (optional) reviewer runs when the PR opens and posts its review.
- You review the PR from web/mobile and merge → the workflow publishes
(
gopush, tag-only) and deletesdocs/PLAN.md.
The workflow invokes codejob --ci <phase> (dispatch, review, verdict,
publish); you never call --ci yourself.
One identifier everywhere — keyring key, environment variable and GitHub Actions secret share the same name:
| Purpose | Name (keyring = env = secret) |
|---|---|
| Agent (Jules) | JULES_API_KEY |
| GitHub token (PAT) | GH_TOKEN |
GH_TOKEN(notGITHUB_TOKEN): Actions secrets cannot start withGITHUB_, and a commit pushed with the defaultGITHUB_TOKENdoes not trigger the next workflow — the chained cloud loop needs a PAT.codejob --cireads these from environment variables (injected by the Action from the secrets); locally it reads them from the keyring under the same name.codejob --init-actionreads them from the keyring and registers them as secrets (repo-level, or org-level with--org).
Token names changed to JULES_API_KEY / GH_TOKEN with no backwards-compat
shim. Simplest path: run codejob; when the key isn't found under the new name it
prompts you and stores it. Optional cleanup of the old entries (keyring service
devflow):
# Linux (libsecret)
secret-tool clear service devflow username jules_api_key
# macOS (Keychain)
security delete-generic-password -s devflow -a jules_api_key
# Windows: Credential Manager → search "devflow"To rotate the GitHub token: codejob --reset-gh-token.
Implement CodeJobDriver and register it for a role:
type CodeJobDriver interface {
Name() string
SetLog(fn func(...any))
// Send runs one job. JobSpec carries the role (executor/reviewer), the
// target branch (for reviews/corrections), the plan path and the prompt.
Send(spec JobSpec) (string, error)
}| Driver | File | Doc |
|---|---|---|
| Jules | code_jules.go |
codejob/JULES_AUTOMATION.md |