CodeCrew Protocol Specification¶
Version 2.1 — 2026-09-24
CodeCrew is a lightweight framework for agent-driven software delivery. It keeps the auditability and reproducible discipline of heavyweight frameworks like GSD while discarding their ceremony, and it uses the tools teams already have — GitHub issues, PRs, and CI — as its state store and message bus instead of maintaining a parallel documentation corpus.
The design rationale is recorded in docs/founding-decisions.md and the analysis that motivated the project in docs/gsd-vs-frontier-orchestration.md.
1. What CodeCrew is¶
Three parts, in order of importance:
- A protocol — conventions for how project state is represented in GitHub: what a milestone, task, plan, decision, deviation, gate, and summary look like, and how agents and humans transact over them. This document is the protocol. Any agent that can read and write GitHub issues can participate.
- Role contracts — short, harness-neutral prompt files defining what each role (implementer, reviewer, QA, doc-synthesizer) reads, writes, and is accountable for. Any frontier-model harness (Claude Code, Codex, Gemini CLI, …) can load them; orchestrators such as Paperclip assign them to agents.
- A thin CLI —
codecrew, a single static Go binary wrappinggh. It implements the workflow verbs so agents don't each reimplement the conventions, and it runs the deterministic gates as code, not judgment. It is the only real software in the framework, and it is deliberately small. Installed as aghextension the command isgh codecrew; this document writes the barecodecrewwhen naming the protocol's verbs, and the contracts, the scaffold and the CLI's own output write the runnable form, because they are loaded verbatim as agent instructions.
CodeCrew does not dispatch or schedule agents. A human operator, a cron job, or an orchestrator does that; CodeCrew defines what a dispatched agent reads and writes.
2. Principles¶
The kernel of discipline CodeCrew enforces — everything else is left to the model's judgment:
- Externalized state. All coordination state lives in GitHub, sharded by task. There is no global state file and nothing that two agents can merge- conflict over; GitHub serializes concurrent writes server-side.
- Plan before nontrivial work. A task's plan — intended changes, requirement IDs covered, pre-identified ask-the-human points — is written into the task issue before the first commit.
- Atomic commits with greppable task references. History is bisectable and
progress is inspectable from reality (
git log --grep), not from reports. - Gates run code, not judgment. Build and full test suite (CI required checks) must be green before anything is called done. CodeCrew reads check status; it never defines CI.
- The verifier is never the doer. Every task is reviewed by a role other than the one that implemented it — ideally a different model on a different harness — because self-evaluation shares the blind spots of the work itself.
- Human gates at designed points. Ask-the-human points are pre-marked in plans, and any participant can raise one mid-task. A raised gate blocks progression until a human resolves it.
- Decisions are captured at decision time. Trade-offs and rejected alternatives are recorded in a structured comment the moment they are made, not reconstructed later.
- Requirements have IDs, so "done" is checkable against something.
- Documentation is synthesis, not maintenance. The only documents the framework produces are per-milestone "why" documents, compiled at milestone close from the decisions and deviations recorded during the work.
- Intentional platform footprint. The protocol knows which GitHub features it depends on and what plan tier they need (§5, Platform requirements); a dependency that would exclude free-plan adopters is taken knowingly or not at all.
3. Topology: hub and spokes¶
A CodeCrew project is a hub repository and one or more spoke repositories.
There is no single-repo mode. A single-repo project is the degenerate case where N=1 and the hub is the spoke. Representations are identical in both cases, so growing from one repo to many requires no migration.
The hub holds: the roadmap, milestone tracking issues, synthesized
milestone documents (docs/milestones/), role contracts
(.codecrew/roles/), and the CodeCrew configuration.
Spokes hold: task issues, the PRs that implement them, and their own CI gates. Task issues live in the repo whose code they change, so GitHub's native traceability (closing keywords, PR linkbacks, CODEOWNERS, repo-scoped permissions) works without convention.
Every repo carries a pointer file, .codecrew/config.yml, so an agent
dropped into any repo can find the coordination point. In the hub it declares
hub: self; in a spoke it names the hub (hub: owner/repo). Everything
CodeCrew owns operationally lives under .codecrew/ — the pointer, the role
contracts, and .codecrew/AGENTS.md, the instructions a dispatched agent
reads — so the framework never competes for a name in a project's own tree
(roles/ is Ansible's before it is ours). The human-facing record does not:
ROADMAP.md, docs/milestones/, AGENTS.md and CLAUDE.md stay where
readers and harnesses look for them — and the root AGENTS.md is only a
pointer at .codecrew/AGENTS.md, so the file an adopter owns holds two
lines the framework never has to rewrite.
A spoke belongs to one hub. The single hub: field is permanent — a
repo is in one delivery stream at a time, and the pointer is the answer to
"where is my coordination point", not a list. A task created in a spoke by
another hub's milestone is not the exception it looks like: such a task
resolves its hub through its parent milestone, not through the repo's
pointer, so the pointer keeps naming the stream the repo belongs to
(#177 carries the
resolution itself).
Choosing a hub¶
The hub is wherever the pointer files say it is — any repo the project owns can serve. The decision ladder:
hub: selffor a single-repo project. The degenerate case above; nothing to decide.- The org's
.githubrepo for a multi-repo org whose delivery record can be public. The hub's cargo — roadmap, milestone issues, milestone documents, role contracts — is org-level coordination state, which is what.githubexists to hold. The caveat is visibility:.githubrepos are public by convention, and milestone issues carry goals, decisions, and gate discussions, so this choice publishes the delivery narrative even when spoke repos are private. - A dedicated hub repo when the tracking must stay private, or when one org runs several projects that should not share a delivery stream.
Three constraints make every rung workable:
- One hub per spoke. The pointer file has a single
hub:field; a repo belongs to one delivery stream at a time. The field is blessed as permanent — see the pointer paragraph above for the cross-hub task. - One milestone number-line per hub. Milestone numbering scans the hub's milestone issues, so a hub is one serialized stream of work. An org running several concurrent projects wants a hub per project, not interleaved numbering in a shared one.
- Multiple hubs per org coexist. All protocol state — milestone issues, numbering, labels, the roadmap — is hub-scoped. The only org-global pieces are the role App identities, and those are meant to be shared: the same implementer identity can serve every hub in the org.
The hub is not the framework's repo. radiusred/gh-codecrew is both the
distribution point for the CLI (gh extension install radiusred/gh-codecrew)
and this project's own hub — a dogfooding coincidence, not a pattern.
Adopters install the extension from it and point their .codecrew/config.yml
at a repo they own; the protocol requires write access to the hub (labels,
sub-issue attachment, milestone close), so a hub you don't control is not a
hub.
Growth and restructuring¶
- Adding a spoke: create the repo, add a
.codecrew/config.ymlnaming the hub. Nothing moves, nothing converts. - Cross-repo references use the qualified form
owner/repo#123. Short#123references remain valid forever within their own repo because issues never leave it. - Hub changes happen at milestone boundaries. The protocol's only live
state is the open milestone and its open sub-issues; everything else is
inert record that nothing re-reads. So a hub move (splitting a project onto
its own hub, extracting the hub from a repo that outgrew hub-is-spoke) is:
close the current milestone in the old hub as normal, repoint each spoke's
.codecrew/config.ymlin a one-line PR, and start the next milestone in the new hub — a fresh number-line. History stays where it happened: closed milestone issues, milestone documents, and decision trails are point-in-time snapshots, and moving them rewrites the audit trail. Leave a tombstone row in the old hub's ROADMAP naming where the stream continued. Worst case this closes a milestone slightly early with requirements carried forward — cheap, and it keeps every milestone's record gathered from one hub and synthesized into one document. - Mid-flight transfer is the discouraged exception. GitHub issue transfer preserves comments and leaves redirects, but drops label associations and is unverified for sub-issue links — moving an open milestone risks the record the boundary rule keeps intact. Reach for it only when a boundary close is genuinely impossible.
- Repo splits: open task issues are transferred with GitHub's issue transfer. Transfer drops label associations, so conventions must be cheap to re-apply (re-label, re-attach the sub-issue link to the milestone).
4. State model¶
Roadmap¶
ROADMAP.md, committed in the hub. A short ordered list of the milestones
delivered so far, with a one-line goal each and links to their tracking
issues and milestone documents. Each row is added, already Done, by the
milestone's document PR (the doc-synthesizer's, §7): nothing writes a row
while a milestone is open — milestone new creates the tracking issue and
nothing else — so the roadmap lists finished milestones and codecrew status
reports the open one. The alternative, a row committed at open time, had no
PR to ride in when a milestone's tasks all lived in spokes
(#197).
Milestone¶
A tracking issue in the hub, labeled cc:milestone. This is the canonical
milestone object — GitHub's repo-scoped Milestone feature is never canonical,
even in single-repo projects. Body structure:
## Goal
One paragraph.
## Requirements
- **M1-R1** — <requirement>
- **M1-R2** — <requirement>
## Gates
What "done" means beyond CI: e2e suites, manual UAT, sign-offs.
The milestone's tasks are attached as GitHub sub-issues, not listed in the body: the tracking issue shows live per-task states and progress natively, and nothing is hand-maintained — a checkbox list rots the moment someone forgets to tick it (state is inferred, not bookkept).
Requirement IDs are M<milestone>-R<n> and are referenced by task plans and
PR descriptions. A requirement may span multiple tasks; it is only marked
complete when every task covering it is closed.
The grammar is enforced, not conventional: every ID under ## Requirements
carries its own milestone's number. milestone close and milestone
evidence refuse REQUIREMENT_ID_MISMATCH on an ID that does not, naming
it, so no verdict is ever counted against a requirement belonging to
another milestone; status prints the same condition as a line, because it
reports the board rather than gating it. IDs written outside the section
are not requirements at all.
A requirement can be struck: withdrawn from the milestone's scope by a
recorded Decision, because nothing was built and nothing is owed. Striking
is a scope change, and it belongs to the coordination layer, not to QA:
whoever holds coordination owns it and records it — the human, a human and
an agent jointly, or an agent the human has deliberately put in charge of
coordination. The Decision comes first, as a **Decision:** (or the
**Gate resolved:** answering a question about the requirement) that names
the ID, on the milestone issue or one of its tasks. Then milestone strike
posts one line on the milestone issue:
The body is never edited: the requirement's line stays verbatim and bold,
and a strikethrough written into the body means nothing to the protocol — a
body edit is not a record, so ~~**M1-R2**~~ is still a requirement. The
verb refuses DECISION_UNRECORDED unless the link is such a Decision, and
milestone close checks every struck line it counts again, so a line posted
by hand is held to the same record. Only the coordinator seat's holder's
lines count, and a struck requirement is terminal: it wants no QA verdict,
and no verdict before or after it undoes it. A strike is undone the same way
it is made — a Decision, then milestone strike --reinstate, which posts
**M1-R2 — reinstated.** <link> — after which QA verdicts count again.
status reports each strike, and the milestone document's requirement table
carries the word.
Task¶
An issue in the spoke whose code it changes, labeled cc:task, attached
to its milestone as a sub-issue. Body structure:
## Goal
What this task delivers.
## Requirements
M1-R2, M1-R4
## Adopts
- #12 — a backlog capture this task takes up. Written by `task new
--adopts`; absent when the task adopts none.
## Plan
Intended changes, in enough detail that a deviation is detectable.
## Ask-the-human points
Pre-identified judgment calls that must stop for a human. "None" is valid.
The plan is written or updated by the implementer before the first commit. Trivial tasks may have trivial plans; they may not have absent ones.
A task may also adopt backlog issues — the unlabelled captures a
milestone takes up, which describe what it delivers but carry none of the
scaffold above. task new --adopts <ref> lists them under an ## Adopts
section of the body and comments on each capture naming the task; task
finish closes each of them after the merge, pointing back at the task and
its pull request. Adoption is the protocol's own link, so a capture stays
open exactly as long as the task carrying it, and no pull request body has
to remember a closing keyword for one.
The section is read strictly, because what it lists is closed after a merge
where nothing can refuse: the heading counts only as a heading — prose
quoting ## Adopts does not open the section, and the section ends at the
next heading of level 1 or 2 — only the ref at the head of a list line is
an adoption, the prose after it being the capture's title, and the body is
read through the same code rule the records are (a ref inside a code span,
a fenced block or an indented one is content, not an adoption).
Task lifecycle¶
State is inferred, not bookkept:
| State | Signal |
|---|---|
| Ready | Issue open, unassigned |
| In progress | Assignee set |
| Gated | cc:needs-decision label present |
| In review | Linked PR open |
| Done | PR merged, issue closed via closing keyword |
The label set is deliberately tiny: cc:milestone, cc:task,
cc:needs-decision. Anything inferable from GitHub state is inferred.
The three carry protocol defaults — a fixed colour and a one-line description — so a repository's labels say what they mean rather than wearing whatever GitHub generated for an implicit creation:
| Label | Colour | Description |
|---|---|---|
cc:milestone |
#01d4ff |
CodeCrew: the milestone tracking issue in the hub (SPEC §4) |
cc:task |
#92edff |
CodeCrew: a task issue, attached to its milestone as a sub-issue (SPEC §4) |
cc:needs-decision |
#f0aeff |
CodeCrew: a human gate is raised — the protocol's verbs refuse until it is resolved (SPEC §8) |
The colours are the crew images' accents: the mark's cyan for the
milestone, the test seat's lighter tone of the same hue for the task that
is part of one, and the review seat's pink for the gate — a gate being a
question for a human, and review the seat whose job is asking one. The
pairing is chosen for the two labels that co-occur, cc:task and
cc:needs-decision on a gated task; the milestone and its tasks share a
hue and never share an issue.
init creates the three that a repository does not already define, hub and
spoke alike, and checkpoint creates cc:needs-decision when it is missing
rather than leaving the first gate in a repository to define it implicitly
(CLI.md). Neither restyles a label that already exists — colour
and description both — because a project may have restyled one deliberately
and nothing there can tell that from a GitHub default.
migrate is the exception, and the only one: it sets all three to the
defaults above, whatever they were wearing. The one-shot move to the 2.0
layout exists to leave a repository indistinguishable from a fresh 2.0
init, and a 1.x repository's cc: labels were all created implicitly by
the first verb that mentioned one — a generated colour, no description —
so leaving them is leaving the migration half done. The asymmetry is
deliberate: init is the verb that reruns on a repository whose labels
somebody may have chosen, so it leaves them; the move migrate performs
happens once, on a repository whose labels nobody chose.
The label step is the part of migrate a rerun repeats — it is the
documented recovery from a step that could not reach GitHub
(CLI.md) — so a cc: label restyled deliberately after the
migration will be set back to the defaults by the next migrate. That
follows from the recovery and is accepted rather than overlooked:
--dry-run previews every change and the receipts name each one, and a
project that wants its own styling to stand has init, which never touches
an existing label, as the verb it reruns.
These are defaults, not enforcement: only the names are protocol.
Housekeeping¶
Every change that has a decision in it is a task. A change with none is
housekeeping, and it takes a lighter path. The test is mechanical, not
a matter of size: a tool states the target, and the diff is the whole
decision — there is no Decision to write, and the pull request needs no
"because". Bringing the contracts and .codecrew/AGENTS.md to the embedded
release (roles sync),
the layout and labels to the protocol's (migrate), a dependency to the
version a bot proposed, a file to what its formatter or linter asks for:
each is housekeeping when the tool's output is the diff and nothing else is
in it. A hand-spotted typo is not — no tool defined it — and neither is
anything the tool did not produce. The moment the change needs a reason, a
choice between options, or an edit beyond the tool's output, it is a task
(#343).
One change besides a tool's output takes the path, and only one: the
milestone record (below). No tool states its target, but the protocol
does — docs/milestones/<n>-<slug>.md, the milestone's ROADMAP.md row,
and the front-door claims the milestone changed (§7) — and its content is a
synthesis of Decisions and Deviations already on the trail, so it has no
Decision of its own to write. Its test, in place of the tool's: the diff is
those three things and nothing else, every claim in the document traces to
the trail, and nothing in it is a choice the doc-synthesizer made for the
project. A record that needs such a choice — a front-door rewrite beyond
the milestone's claims, one reading of the trail over another — is a task,
as any housekeeping change that fails its test is. Nothing else a contract
or this document names is admitted by this clause
(#349).
A housekeeping change travels as a chore: commit whose body names the tool
— and its version, where it has one — that defined the target, and
references no task; as one pull request per run of the tool; through review;
and by a rebase merge. There is no task issue, no milestone, no plan, no
task start or task finish, no QA verdict and no record entry: the diff
carries the information those would, and the commit is the record. The
milestone record travels the same way, except that its commits are docs: and
reference the milestone issue in place of a task; its pull request names
the milestone issue with no closing keyword, because milestone close
closes the milestone and a merge never does; and it belongs to the
milestone it records, whose issue carries the doc-synthesizer's Deviations
and any raised at the close — where milestone close gathers them and
milestone evidence walks them.
The review is what the path keeps, and what it means depends on who holds the reviewer seat (§5). No ruleset is assumed — where a repository has one, it enforces in addition:
| Reviewer seat | What the housekeeping PR needs before it merges |
|---|---|
Routed to a distinct principal (app:, user:, team:) |
That holder's approval. Other approvals do not count — the rule task finish applies as NO_HOLDER_REVIEW. |
Operator-held (~), the PR authored by someone else (an App-held seat, a colleague) |
A non-author human's approval — the rule task finish applies as NO_NONDOER_APPROVAL. |
| Pure solo: operator-held, and the operator is the author | An operator confirmation comment on the PR, in the form task finish --operator-confirm posts, stating that no independent principal exists. A crew identity never posts one. The strongly encouraged form is still a clean-context model review whose findings land on the PR first. |
In every case the repository's checks are green where it reports any. The
PR's author then rebase-merges it; no verb gates the merge, so the contracts
(§7) are the fence: the reviewer refuses a PR that fails the test and sends
it back for a task, and a seat never merges a housekeeping PR before its
approval is on it. The milestone record's author is the doc-synthesizer,
which merges it and deletes its branch; milestone close sweeps task
branches only.
milestone close's DOC_MISSING gate is deliberately as loose as the rest
of the path: it checks that the document is on the default branch, not how
it got there. A record committed straight to the default branch passes the
gate and breaches the doc-synthesizer's contract all the same — the gate
does not see provenance, and no verb claims to.
Decisions and deviations¶
Recorded as issue or PR comments at the moment they occur, using a structured prefix so synthesis can find them:
**Decision:** <what was decided>
**Trade-off:** <what was weighed>
**Rejected:** <the alternative and why>
A decision made at a human gate uses the resolution form and is gathered as a Decision:
A deviation that changes what a requirement means, or an ask-the-human point being reached, raises a human gate (§8) instead of being silently recorded.
The label may carry a parenthetical qualifier — **Decision (superseding the
R1 counting claim):** — which is gathered verbatim and carries no
semantics. Gathering is per labelled paragraph: a comment that is one
record is one record, and a record written after other text in the same
comment is still found. A label that does not open a paragraph — mid-line,
or at the start of a line inside a paragraph — is not a record, and any
other bold label opening a later paragraph (**Finding 3:**) ends the
record before it. One record per comment remains the practice to prefer.
The placement rule is the whole label family's, **Gate raised:** and
**Gate resolved:** included: a gate raised in a comment's second
paragraph is a gate, and a gate answered in a comment's second paragraph is
answered.
A QA verdict (§7) is read by the same reading. Supersession is per
comment: for each requirement ID the latest comment carrying a verdict for
it wins, and within that comment the first verdict for the ID counts — so a
comment may quote the verdict it supersedes without superseding itself. Code
is content, in records as in citations: a verdict inside any of Markdown's
three code forms — an inline code span, a fenced block, or a block indented
four columns anywhere it does not continue a paragraph — is not a verdict,
exactly as a URL in code is not a citation (CLI.md, milestone
evidence). Those are the shapes a form quoted from a contract and an
earlier verdict shown verbatim take. The struck and reinstated lines (§4,
Milestone) are read by the same reading, from the coordinator seat's holder.
Line endings are not part of the grammar: a body saved with CRLF — what a
web editor writes — is read exactly as the same body written with LF, in
the record scans and the ## Adopts section alike.
Summary¶
The PR description, finalized at merge time: what was done, which requirements it satisfies, and any deviations (linking the deviation comments). The merged PR is the task's summary artifact; no separate file is written. It is read by the doc-synthesizer directly and never gathered as a record: records live in comments, and a decision that exists only in a PR body is unrecorded.
Commits¶
Atomic, one logical change each, every commit referencing the task issue
((#123) suffix — the local number suffices because commits live in the same
repo as their task issue). Linear history (rebase merging) is recommended.
The exceptions are housekeeping (above), which has no task: its chore:
commit names in its body the tool that defined the target instead, and the
milestone record's docs: commits reference the milestone issue.
Milestone document¶
docs/milestones/<id>-<slug>.md, committed to the hub when the milestone
closes. It is the "why" document: the architectural, pattern, and technology
choices made during the milestone, their trade-offs and rejected alternatives —
synthesized from the Decision and Deviation comments recorded during the work,
never reconstructed from raw history. Its requirement table gives each ID its
final status — the latest QA verdict, or struck with the Decision linked. It
lands as a housekeeping PR (above) with no task behind it, through the same
review gate as code, merged by its author, the doc-synthesizer.
5. Configuration¶
.codecrew/config.yml in every repo. Spokes need only the pointer; the hub
carries the full configuration. The codecrew field is the protocol
version — this document's version, naming the conventions the file speaks
— and is independent of the CLI release: codecrew version prints both, as
v2.1.0 (protocol 2.1). The CLI implements one protocol major and checks
the pointer's on every verb that loads it: a different major is refused
(refused[PROTOCOL_MISMATCH]), and the two directions differ — a pointer
ahead of the binary asks for an extension upgrade, one behind it is told the
repo predates this protocol and is moved with codecrew migrate; neither
asks anyone to edit the version field, which describes the repo rather than
choosing for it. Within the major, a binary must implement at least the
pointer's minor; the CLI compares majors only, so that check is the
dispatched agent's, before any verb but version and identity token,
per .codecrew/AGENTS.md. A pointer naming an earlier minor of the same
major is current: a minor is additive and keeps the layout, so nothing has
to move the field forward and no verb does — migrate leaves it as it is —
while init writes the version its binary implements.
A missing field is assumed current, with a note — the
pointer's own path is the layout's proof, since a repo still on 1.x has no
.codecrew/config.yml for the check to reach and refuses LAYOUT_LEGACY
first. There is no other leniency: 1.0's acceptance of "0.1", the pre-1.0
form of the same conventions, is gone with the other 1.0 shims (§10) — a
0.1 pointer is two majors back and is migrated like any other. Decided at
the M6 gate on
#114:
codecrew: "2.1"
hub: self # spokes: owner/repo
# Advisory role routing, read by whoever dispatches agents.
roles:
implementer: { harness: claude-code, model: claude-fable-5, identity: app:my-org-coder }
reviewer: { harness: codex, model: gpt-5.5, identity: "team:my-org/review-crew" }
qa: { harness: codex, model: gpt-5.5, identity: app:my-org-qa }
doc-synthesizer: { harness: claude-code, identity: user:alice }
coordinator: { identity: ~ } # the seat that dispatches the other four; ~ = the operator
Role routing is advisory in one sense only: CodeCrew does not dispatch
agents, so the table is a contract for the orchestrator (or human) that
does. The verbs themselves read it — the holder-review gate, verdict
counting, the crew-identity refusals — so it is fetched, checked, and failed
closed on (CLI.md). The hub carries the one table. A spoke's
pointer that also carries a roles: block is refused at load
(refused[SPOKE_ROUTING], naming the hub and the rows), because a copy in a
spoke either silently outranks the hub's or goes stale, and the protocol
will not pick a winner between them; a spoke resolves every role from the
hub's table, fetched on each run.
Every role is always staffed — solo is a routing configuration, not a
reduced protocol. The routing table says who holds each role: a GitHub
App (an agent acting as itself), a specific human, a team, or — with no
identity (~) — the human operator. An orchestrator dispatches sub-agents
or delegates to other harnesses per this table; a solo operator embodies
whatever routes to ~. The hub's config should declare all five roles at
project onboarding — the four crew seats and the coordinator that dispatches
them (§7) — each routed explicitly; an orchestrator finding no routing table
should prompt for one rather than assume. codecrew init (CLI.md)
scaffolds exactly this — the table with every role routed ~ — so
onboarding starts explicit. The CLI tolerates an absent table (every role is
then operator-held) but says so in its output; a table written before the
coordinator row existed still has a coordinator — the operator — and
codecrew role coordinator says so.
identity is typed: its value names the kind of GitHub principal that
holds the seat, and there are exactly four forms.
| Value | Who holds the seat |
|---|---|
~, or no identity key |
The operator — and any agent session acting under the operator's own auth. The grammar types the GitHub principal, not who is at the keyboard: a main agent with an App of its own is app:<slug> on the coordinator row, not ~. |
app:<slug> |
A GitHub App — an agent acting as itself. Its viewer login is <slug>[bot], which the protocol matches with or without the suffix. |
user:<login> |
One named human, by GitHub username. |
team:<org>/<slug> |
Any member of the team (child-team members included). |
The type prefix is not decoration: it is the one fact a routing table
alone can carry about a seat, and every rule below reads it rather than
guessing from the value's shape. An identity carrying no prefix — a 1.0
table's bare my-org-coder — is refused at load
(refused[IDENTITY_UNTYPED], naming the row and the four forms), because
an App slug and a username are the same string and the protocol treats
them differently; gh codecrew migrate rewrites a 1.0 table by resolving
each login's kind through the API.
A team-held role is held by any member for every purpose an identity
serves — the holder review gate, verdict counting, the crew-identity
refusals, and branch rules
(#44).
Latest-wins verdict supersession runs across members: one teammate can
supersede another's verdict — correct for a pool, and stated here so it is
never merely implied. A team-held reviewer seat composes with GitHub's
native required-review counts with zero framework code: repo rules demand
N approving reviews from the nominated team, GitHub enforces the count,
task finish merely reads the result. Platform footprint (per this
section's tier-intentionality obligation): teams are org-only — a
personal-account project cannot route to them and does not need to — and
required-review counts on private repos are plan-gated, so the
composition win is public-repo or paid-private territory. Each agent-staffed role gets its own app identity
so agent-authored work is attributable and distinct from the operator —
without this, every PR is authored by the operator's account and GitHub's
prohibition on self-approval makes the non-doer review gate unsatisfiable.
App private keys live outside the repo (by convention
~/.config/codecrew/<slug>.*.private-key.pem); short-lived installation
tokens are minted per invocation.
Where a verb gates on a role — milestone close counts only the qa role
holder's verdicts — "holder" means the routed identity, or, for an unrouted
role, any human identity not routed to a different role. Crew identities can
never stand in for a role they are not routed to.
Crew identity means app:-typed, and nothing else. It is the test the
verbs apply where an act belongs to a human — --operator-confirm,
--bypass — and a human who holds a seat (user:, or a member of a
team:) is not one: they hold the role and keep the operator's acts. A
1.0 table could not distinguish the two, so every routed login was refused
as crew.
Identity tiers¶
App identities are optional infrastructure, required only where a GitHub-native approval must come from a party with no human account:
- Solo — one human, agents act under the operator's own auth. The whole
protocol works on
gh auth loginalone; the review gate degrades to explicit operator confirmation (CLI.md) because GitHub forbids self-approval. The confirmation must come from a human identity — a[bot]login or anapp:-typed role holder is refused — and when the confirmer is also the PR author, the recorded comment states that no independent principal exists. See docs/identities.md for the operational path. - Multi-human team — a colleague satisfies the review gate natively; app identities add only attribution (distinguishing an agent's work from its dispatcher's).
- Enforced review with one human, or agent-reviews-agent — app identities are required: the author and approver must be distinct principals, and each automated role needs its own.
Credential resolution is uniform across tiers, and codecrew identity token
<slug> (CLI.md) is the act: orchestrator-injected env vars — the
App's id and private key under whatever names the platform binds
(GITHUB_APP_ID or GITHUB_CLIENT_ID; GITHUB_PRIVATE_KEY or
GITHUB_PEM, as PEM text or a file path), with the installation discovered
from the App itself (a supplied GITHUB_INSTALLATION_ID is a hint at most:
the run on Paperclip was handed a stale one — #119, findings 12 and 35) —
then the locally-held private key and credential stub identity new wrote.
The verb refuses with a code past those two; the operator's gh auth is the
identity only of an unrouted role, never a fallback for a routed one.
Platform requirements¶
Everything the protocol requires is available on every GitHub plan, public or private: issues and sub-issues, labels, comments, pull requests, rebase merging, GitHub Apps, and CI check reading. A free-plan solo operator loses nothing.
Features CodeCrew benefits from but deliberately does not require, because they are plan- or visibility-gated on private repos:
- Branch rulesets / required status checks and reviews — free on public
repos; private repos need a paid plan. Without them,
task finishis the enforcement: it refuses on red checks and missing approval even where GitHub wouldn't block the merge button. - Auto-merge — a convenience some hubs enable;
task finishmerges through the API and never depends on it. - Actions minutes — private repos have quotas; the protocol reads check results but never defines CI, so a spoke's CI budget is its own affair.
The standing obligation: any future protocol dependency on a GitHub feature states its plan availability — public/private differences included — at the moment it is introduced.
6. Workflow verbs¶
The CLI's surface, and therefore the backend interface. Every verb is safe to
run by any role from any repo in the project (the pointer file resolves the
hub). The verbs are specified in CLI.md — per verb: synopsis,
options, what it reads and what it writes, --dry-run behaviour, the refusal
codes it can exit with and its exit status. This section keeps its number so
§7–§13 keep theirs; what stays here is the contract every verb shares.
The exit-code contract. A verb exits 0 when it did what it was asked
and 1 on every failure — a refused gate, an unusable flag, an unreachable
GitHub, a gh that failed. There is no exit-code taxonomy and none is
coming within this major: a status of 1 says only "this did not happen",
and anything finer would break every caller already asserting on it the day
it arrived. --dry-run follows the same rule, exiting 1 when it reports a
gate that would refuse.
The machine channel is one line on stderr:
The code is the branch point — a fixed vocabulary, catalogued in §10,
whose meanings are stable within a major. The detail is prose for a
human and may be reworded in any release; nothing should parse it. Output
a caller consumes — a minted token, role <name>'s identity, a report with
the note: and warning: lines that qualify it — goes to stdout, so it
stays clean whatever stderr says. A note: is advisory, never a failure,
and stands beside a verb that went on to succeed. The notes that go to
stderr instead are the ones raised while the pointer is read, before the
verb prints anything, and identity token's, which keeps its receipt and
notes there so that stdout carries the token alone;
CLI.md names each.
7. Roles¶
Role contracts live in the hub under .codecrew/roles/, one short markdown file each,
loadable by any harness (and referenced from .codecrew/AGENTS.md, the entry
point, for harnesses that read a repository's instructions natively — the root
AGENTS.md points at it and CLAUDE.md imports the root, so every harness
arrives at the same text). Roles are contracts, not accounts: no GitHub App needs to
exist for a role to be staffed — every role can act as the human operator
(§5, and docs/identities.md for both the solo path and
App creation). v1 roles:
- implementer — writes the plan into the task issue, does the work in atomic commits, records decisions/deviations as they happen, opens the PR, finalizes the summary. Never approves their own PR.
- reviewer — reviews the PR against the plan and requirement IDs; the conversation is ordinary PR review comments and requested changes. Ideally a different model/harness from the implementer, per role routing.
- qa — exercises the built thing against the milestone's gates and the
requirements' intent (not just the tests the implementer wrote); reports
findings as issue/PR comments.
struckis not QA's word: QA did not judge a struck requirement, verdicts none, and does not strike. - doc-synthesizer — at milestone close, compiles the recorded decisions and deviations into the milestone document, opens its PR on the housekeeping path (§4) and merges it once the review is on it.
- coordinator — the coordination layer as a seat: opens milestones
(
--requirement) and tasks, dispatches the four crew seats by the routing table, owns the review loop in both directions (reviewer on a PR, implementer on changes requested, the task's owner on approval), raises the gates only a human can answer, and drivesmilestone evidenceandmilestone close. It never writes code, reviews, QA verdicts or merges; its App holds contents: read, issues: write, pull requests: read and metadata, never more. Unrouted it is the operator — every project has a coordinator, solo included. The contract states what the orchestrator run taught the seat (#119, #164): one wake path per transition, state re-read at the act, execution events one-shot, dispatch on the platform and cite on GitHub, never the milestone number in requirement prose. Striking a requirement (§4) is this seat's: the scope change is the coordination layer's, recorded as a Decision and posted withmilestone strike, whoever holds coordination.
Local extensions. A project's own instructions for a role — house style,
local conventions, what its orchestrator injects — go in
.codecrew/roles/<role>.local.md, never into the contract. The contract is
the project's fork of the framework's (CLI.md, roles diff); an
extension is append-only text loaded after it, so reconciling the contract
against a newer release never has to re-merge project additions, and
status's drift check never sees them. Reconciling a contract that is still
some release's text, unedited, needs no judgment: codecrew roles sync
writes the embedded contract — and one a newer release added, which status
reports as missing — as one local commit for a housekeeping PR (§4). It
never overwrites a contract that differs from every release's text: that is
a fork, reconciled by judgment in a task, with the project's additions moved
into the extension. .codecrew/AGENTS.md is CodeCrew's file in hub and spoke
alike and is synced by the same rule — written while absent or still some
release's scaffold, never overwritten once it is the project's own — so a
new release's instructions reach a repository that init and migrate,
which write it only when absent, never touch again
(#372). Load order is
fixed: the hub's .codecrew/roles/<role>.md, then the hub's
.codecrew/roles/<role>.local.md, then the working repo's
.codecrew/roles/<role>.local.md when it is a spoke. There is no merge
language and no precedence beyond that order — an extension that contradicts
its contract is a review finding, not a resolver's job. codecrew roles show
<role> prints the composition a dispatched session should load; a harness
that reads the entry point natively follows the same order by hand.
The inter-agent protocol is GitHub itself — issue comments, PR reviews, labels. There is no other message bus, so any two harnesses interoperate by construction.
8. Verification and gates¶
Three independent layers, attacking different failure modes:
- Deterministic gates — each spoke's CI required checks. Owned by the
repo, read by
task finish. Catch what code can catch. A PR with zero reported checks refuses (refused[NO_CHECKS], no override): this layer cannot be satisfied by absence, so every repo usingtask finishcarries at least onepull_requestworkflow. A check that reportsskippedis a reported check — the platform's own fact, and what a docs-only path should produce (a job the committed workflow skips byif:);[skip ci]produces no fact at all and is refused. - Independent review — the reviewer role holder's approval required to
merge when the seat routes to a distinct principal; any non-doer approval
only when the role is operator-held. The doer is fixed too: the seat that
started a task is the seat that finishes it (
refused[NOT_OWNER]otherwise; an operator overrides on the record with--bypass) — a merge by another seat is a misattribution even when the review gate passed (#165). The norm is a model review: a clean-context session under the reviewer contract, optionally a different harness — even in pure solo, where its findings land as a PR comment before the operator confirms. A housekeeping PR (§4) passes this layer and the checks of layer 1 withouttask finish: the approval each reviewer routing requires is the same, and the contracts hold the fence. Catches correlated self-evaluation failure: the model grading its own work shares the blind spots of the model that did the work, and a briefed reviewer shares the briefing's. - Human gates — pre-marked ask-the-human points plus ad-hoc
checkpoints. Catch the boundary cases where capability doesn't help: "should this task be done as specified at all." Acc:needs-decisionlabel blockstask finishuntil a human removes it, and the resolution must be recorded as a**Gate resolved:**comment (§4) so it is gathered into the milestone record as a Decision —task finishrefuses (refused[GATE_UNRECORDED]) while a raised gate has no resolution comment, even after the label is removed. Both labels are read per paragraph (§4), and only**Gate resolved:**resolves: a**Decision:**on another subject answers nothing. Resolution is per gate — a**Gate resolved:**record resolves every gate raised before it and still open, so one comment may answer several questions, and never a gate raised after it. A question about a requirement has no task to carry it: it is raised on the milestone issue, wherestatuslists the gate beside the tasks' gates, marked (#200), andmilestone closerefuses (refused[MILESTONE_GATED]) while the label is present (#219).
9. Environments¶
CodeCrew is harness-neutral by construction: participation requires only the ability to run a CLI and read/write GitHub. Supported shapes:
- Solo operator — one human, one harness, hub-is-spoke.
- Mixed-model team — role routing assigns different models to different roles (e.g. Claude implements, Codex reviews); they converse through PRs.
- Orchestrated company (e.g. Paperclip) — the orchestrator maps its agents
to CodeCrew roles via the routing config and dispatches them; CodeCrew
defines what each one reads and writes. Exercised end to end on Paperclip
(#119: three milestones on a proving-ground repo, the third driven by the
App's webhook events with one gate and no other operator touch on the
workflow; #164: a fourth cycle on a fresh repo, driven by a coordinator
agent from the first event). The platform installs the coordinator as a
seat like the others —
.codecrew/roles/coordinator.md(§7) composed byroles show coordinator, the platform's wake syntax, ids and tooling in the project's.codecrew/roles/coordinator.local.md, its identity minted byidentity new coordinator— instead of a hand-written brief, and wires each seat's App to the receiver that dispatches it (identity new --with-webhook --webhook-secret,identity webhook; one App hook covers every repository the installation sees). The whole shape — the separation of concerns, the seat mapping, credentials, wake paths, the onboarding checklist, what four cycles cost and the Paperclip recipe — is docs/platform-interop.md, which also names the seams that remain open.
10. The CLI¶
What 2.0 broke. 2.0 is a protocol major, and the break is the layout:
every CodeCrew-owned operational file moved under .codecrew/ — the pointer
from .codecrew.yml to .codecrew/config.yml, the contracts and their
extensions from roles/ to .codecrew/roles/, the agent instructions from
AGENTS.md to .codecrew/AGENTS.md — so the framework stops competing for
names in the root of a repo it does not own. There is no compatibility shim
and no dual-read: a 2.0 binary meeting a 1.x repo refuses LAYOUT_LEGACY
and names gh codecrew migrate, the one-shot verb that moves the files,
rewrites the pointer and commits the result locally for the operator to read
and push (CLI.md). Nothing else about a 1.x project changes — the
issues, labels, branches, records and roadmap are untouched, and
ROADMAP.md, docs/milestones/, AGENTS.md and CLAUDE.md stay at the
root where readers and harnesses expect them, the last two now pointing at
the instructions rather than holding them.
What 1.0 promises (decided at the M6 gate, #114). Within a major release
series of the CLI: verb names and their flags are additive — nothing is
renamed or removed; a refusal code's meaning is stable — codes may be added
in a minor, never repurposed, and removed only in a major; the
refused[CODE]: detail line and the version output are stable shapes,
other human-facing text is not; pointer fields are additive; the embedded
role contracts may change in a minor — status's drift report, roles diff
and roles sync are the mechanism: a contract — or .codecrew/AGENTS.md —
that is still some release's text is brought up to date by roles sync on
the housekeeping path (§4), and a fork's reconciliation is the project's
judgment. A change to
this document that invalidates existing pointers or recorded comments is a
protocol major, and the CLI that implements it refuses the old pointer. A
protocol minor is additive — new verbs, codes, records and contract text,
with every pointer, layout and recorded comment the major accepted still
valid — and its CLI reads an earlier minor's pointer as current (§5).
The refusal codes. Forty-seven, and this table is the catalogue: a code
absent from it is not one the protocol promises. Every row is raised as
refused[CODE]: detail (§6), and every one of them exits 1. "any verb"
below means any verb that loads and validates the working repo's pointer —
every verb except version, help, identity token and identity
webhook, which require none and raise none of the pointer's codes
(identity token reads one only when present, to prefer the hub's owner),
and init and migrate, which raise the layout codes themselves.
| Code | Raised by | Meaning |
|---|---|---|
ADOPT_NOT_OPEN |
task new |
A --adopts ref is not an open issue: it could not be read, or it is already closed. Checked before the task is created. |
AGENTS_FORKED |
roles sync |
.codecrew/AGENTS.md differs from the embedded scaffold and from every release's, and no contract among those named is forked: it is the project's own (§7), and the verb never overwrites it. Checked before anything is written. |
BAD_CREDENTIALS |
identity token, identity webhook |
GitHub rejected the App JWT, or knows no App by the id it was signed as: the key and the id are not the same App's. |
BOTH_LAYOUTS |
migrate |
The 1.x and 2.0 layouts overlap, and neither file is migrate's to overwrite. |
CHECKS_FAILING |
task finish |
A CI check on the closing PR failed. |
CHECKS_PENDING |
task finish |
The closing PR's checks are still running. |
CLOSED |
task start, task finish |
The task issue is already closed. |
CONTRACT_FORKED |
roles sync |
A hub contract differs from the embedded one and from every release's text: it is the project's fork (§7), and the verb never overwrites one. Checked before anything is written; the detail also names a forked .codecrew/AGENTS.md. |
CREW_BYPASS |
task finish |
--bypass was given by a crew identity; the override is a human operator's act. |
DECISION_UNRECORDED |
milestone strike, milestone close |
A struck or reinstated line's link is not a comment on the milestone issue or one of its tasks carrying a **Decision:** or **Gate resolved:** record that names the ID (§4). |
DOC_MISSING |
milestone close |
No docs/milestones/<n>-*.md on the default branch: the milestone document is delivered as a housekeeping PR (§4) before the close. |
EVIDENCE_UNREACHABLE |
milestone evidence |
A github.com citation in the milestone's record does not resolve. |
FOREIGN_ROLES_DIR |
migrate |
A root roles/ holding CodeCrew's files also holds entries it does not recognise; it stops rather than guess which are its own. |
GATED |
task finish |
The task carries cc:needs-decision: a human gate is open. |
GATE_UNRECORDED |
task finish |
A gate was raised and the label removed, but no **Gate resolved:** comment records the answer (§8). |
GH_TOO_OLD |
any verb | The installed gh is below the floor the verbs need, checked up front rather than met inside a gate. |
GH_UNREACHABLE |
any verb, migrate, roles show, task new |
gh never reached GitHub — no route, no DNS, no credentials — named as itself and never folded into another condition. |
HUB_UNREADABLE |
any verb in a spoke | The hub's pointer could not be fetched or parsed, so no role resolves; routing fails closed rather than degrade to ~ everywhere (§5). |
IDENTITY_UNRESOLVED |
migrate |
GitHub answered and the answer did not type a 1.0 identity: nothing, both a user and an App, or an organization. |
IDENTITY_UNTYPED |
any verb | A routing row's identity names no kind of principal; the grammar is ~, app:, user:, team: (§5). |
INSTALLATION_AMBIGUOUS |
identity token |
The App is installed on several accounts and nothing selects one. |
LAYOUT_LEGACY |
any verb, init |
The repo is still on the protocol 1.x layout; nothing reads it, and migrate moves it (§3). |
MIGRATION_UNSUPPORTED |
migrate |
The pointer's protocol major is not 1: below 1.0 predates the conventions the move assumes, above it is not a 1.x repo. |
MILESTONE_GATED |
milestone close |
The milestone issue itself carries cc:needs-decision: a requirement-level question, answered before anything is counted. |
MILESTONE_NUMBER_TAKEN |
milestone new |
The issue was created but another milestone holds its M<n>: prefix, and the verb's own renumbering did not settle it. |
NOT_A_TASK |
task start |
The issue is not labelled cc:task. |
NOT_FOUND |
task new, milestone close, milestone evidence, milestone strike |
No milestone with that number — open, for all but evidence; open or closed, for evidence. |
NOT_OWNER |
task finish |
The caller is not the seat that started the task, or nothing records a start at all (§8). |
NO_CHECKS |
task finish |
The closing PR reports no CI checks; absence cannot satisfy a deterministic gate, and there is no override. |
NO_CHECKS_PERMISSION |
task finish |
The installation token cannot read the PR's checks at all: a private repo needs permissions the App has not been granted. |
NO_CREDENTIALS |
identity token, identity webhook |
Nothing to sign with: no App id and key in the environment, and no key and stub under ~/.config/codecrew/. |
NO_HOLDER_REVIEW |
task finish |
The reviewer seat routes to a distinct principal and that holder has not approved; other approvals do not satisfy it. |
NO_INSTALLATION |
identity token |
The App is installed on no account its key can see. |
NO_NONDOER_APPROVAL |
task finish |
The reviewer seat is operator-held and no non-author has approved (solo tier: --operator-confirm). |
NO_PLAN |
task start |
The task's Plan section is empty; plans come before work (§4). |
NO_PR |
task finish |
No open PR closes the task. |
NO_REQUIREMENTS |
milestone close |
The milestone's ## Requirements section declares no bold ID, so there is nothing to verdict. |
NO_WEBHOOK |
identity webhook |
The App was minted without a webhook, and GitHub's API cannot create one. |
OPEN_TASKS |
milestone close |
Tasks under the milestone are still open. |
PROTOCOL_MISMATCH |
any verb | A pointer's protocol major differs from the one this binary implements — the local one, or the hub's on the spoke's fetch (§5). |
REQUIREMENT_ID_MISMATCH |
milestone close, milestone evidence |
An ID under ## Requirements is not the milestone's own; the grammar is M<milestone>-R<k> (§4). |
REQUIREMENT_UNDECLARED |
milestone strike |
The ID to strike is not declared under that milestone's ## Requirements. |
REVIEW_NOT_COUNTED |
task finish |
The protocol's review gate passed but GitHub's own required-review rule has not; the detail names the supported paths. |
SELF_CONFIRM |
task finish |
--operator-confirm was given by a crew identity; agents never waive review, in any tier. |
SPOKE_ROUTING |
any verb, migrate |
A spoke's pointer carries a roles: block; the hub carries the one routing table (§5). |
VERDICT_MISSING |
milestone close |
A requirement has no QA verdict from the qa seat's holder. |
VERDICT_UNSATISFIED |
milestone close |
The latest verdict on a requirement is not satisfied. |
- Go, single static binary (
CGO_ENABLED=0), cross-compiled for linux/mac/windows. Also installable as aghextension (gh codecrew …), since a gh extension is just a binary namedgh-codecrew. - Wraps
ghin v1 — authentication comes for free. github.com only in 1.0 and 2.0: the URLs the verbs build and recognise are github.com's; GitHub Enterprise Server is a non-goal (§12) until it is proven. - Backend interface is shaped by the workflow verbs (§6), not by GitHub's feature set. The GitHub adapter is the only v1 implementation; the interface keeps two ports conceptually separate even though GitHub fuses them — the tracker (issues, labels, comments) and the review surface (PRs, reviews, checks) — so a future backend pairing a different tracker with a git host stays possible.
- Clear errors over mystery: if the hub declares a spoke the caller's token cannot see, the CLI says exactly that.
11. Auditability and mutability¶
Live tracker state is mutable, and CodeCrew does not police edits — teams that rewrite the record either have good reason or pay the price. The durable audit trail is what lands in git: atomic commits with task references, merged PR descriptions, and the committed milestone documents, which are point-in-time snapshots of the decisions that mattered. The immutable record is in git; the working record is in the tracker.
12. Non-goals¶
CodeCrew deliberately does not have:
- wave orchestration, worktree manifests, dispatch scripts, or heartbeats — execution scaffolding ages badly; frontier models isolate and merge correctly from intent
- a global state file — state is sharded by task and mediated by GitHub
- agent dispatching or scheduling — that is the operator's or orchestrator's job
- a maintained documentation corpus — documents are synthesized at milestone boundaries, from records captured at decision time
- a Jira adapter — GitHub only, with the backend interface as the seam if that ever changes
- GitHub Enterprise Server, in 1.0 — github.com only; the verbs hardcode its URLs, and no GHES run has been made
- configuration for behavior the model can decide well — the framework manages trust (specification, verification, audit), not capability
13. v1 scope and open questions¶
v1 delivers: this protocol; the role contracts (four crew seats at 1.0, the coordinator's from 1.1); the CLI with the §6 verbs and the GitHub adapter; CodeCrew's own hub bootstrapped with it.
Open questions, deferred until the bootstrap surfaces evidence:
- Whether
task finishshould also verify requirement-coverage claims in the PR body against the milestone's requirement list, or leave that to review. - An optional read-only GitHub Projects mirror as a human dashboard, derived from issue state.
- Issue/PR templates shipped as
.github/templates versus created by the CLI atnewtime.