Working offline¶
CodeCrew keeps its state in GitHub on purpose: the plan, the decisions, the gates and the verdicts are issues and pull requests, not files (SPEC §1). So the verbs that transact over that state need GitHub, and there is no local write queue that would replay them later — a queue is scar tissue, and under latest-wins it is a consistency hazard. What is left is still most of the day's work: reading the contract, planning, branching, implementing, running the tests. This page says which verbs run with no network, which wait and exactly what each one prints when it cannot reach GitHub, and the recipe for work that was begun offline.
Everything below was read off the CLI, not off its intentions: every quoted
line comes from running the binary in a network namespace with no route out
(unshare -rn), from a hub, from a spoke, from a fresh repository and from a
protocol 1.x one.
What runs with no network¶
| Verb | Offline behaviour |
|---|---|
codecrew version |
Prints the release and protocol version. Both are stamped into the binary at build time; nothing is fetched. |
codecrew help, and --help on any verb |
Prints usage and exits 0. --help is read before the verb runs, so it never reaches a gate or a fetch. |
codecrew roles show <role> (in a hub) |
Composes the contract from disk: the hub's .codecrew/roles/<role>.md, then its .local.md extension. A hub reads its own pointer and its own contracts, so nothing is fetched (CLI.md). |
codecrew roles show <role> --latest |
Prints the contract embedded in the binary. Works anywhere, hub or spoke. |
codecrew roles diff <role> (in a hub) |
Compares the local contract with the embedded one. Both sides are on this machine. |
codecrew init |
Scaffolds the files and makes the one local commit. See the note below: it completes, with two note: lines. |
codecrew migrate |
Moves the repo to the 2.0 layout and commits, locally. Completes unless a 1.x identity has to be typed; see below. |
And everything git-local, which is where most of a task's time goes: reading
.codecrew/AGENTS.md and the role contract, drafting the plan in a file,
creating a branch, committing, running the tests, rebasing.
The gh floor check is local too. Every verb that reads the pointer asks
gh --version first and refuses GH_TOO_OLD below 2.50.0 — that call runs
no HTTP, so an offline machine is never told its gh is unreadable.
init offline. Its work is files plus one commit, and both are local; the
two things it asks GitHub are not, so both degrade to notes and the scaffold
stands:
note: could not ask GitHub whether the default branch requires pull requests, so the commit is on codecrew-bootstrap — git push -u origin codecrew-bootstrap, then open a PR (or merge it locally if the branch is unprotected)
note: could not ask GitHub which repository this is (…) — the cc: labels are created on first use instead, with GitHub's own colour
The first is the branch-protection probe: unanswered, init takes the
cautious branch and commits on codecrew-bootstrap rather than on the
default branch — which is the one thing an offline init does differently
from an online one in an unprotected repository. Two cases do not reach it:
a repository with no origin at all, which is known not to require pull
requests, and a repository with no commit yet, which has no branch to cut
from — in both the scaffold lands on the current branch. The second is the label step, which is the one thing the
scaffold cannot write to disk; the cc: labels then get created implicitly,
with GitHub's own colour, the first time a verb applies one. Running
gh codecrew init again once you are online creates them properly.
migrate offline. Same shape, and for the same reason: the migration is
a local move and one local commit, so no remote failure stands in its way.
It reports the same label note and exits 0. There is one exception, and it
refuses rather than guesses: a 1.x routing table that names a bare login
(identity: someuser) cannot be moved to the 2.0 grammar without asking
GitHub whether that login is a human or an App, so migrate refuses
GH_UNREACHABLE before writing anything and the whole migration waits.
A 1.x table whose identities already carry user:, app: or a team's
slash needs no lookup and migrates with no network.
What waits, and what it prints¶
Every verb that transacts with the record goes through one load path: read
the pointer, check the gh floor, then ask GitHub which repository this is.
That last call is where an offline run stops — before any write, on every one
of these verbs, with the same line on stderr and exit status 1:
codecrew: refused[GH_UNREACHABLE]: GitHub could not be reached (gh repo: error connecting to api.github.com
check your internet connection or https://githubstatus.com) — check the network and that gh is authenticated (gh auth status), or mint the seat's token with gh codecrew identity token <slug>; codecrew version, help, and roles show/diff in a hub need no network (SPEC §6)
The parenthesis is gh's own message, so the wording there varies with how
the network is failing — no route, no DNS, no credentials all land here. The
code does not: GH_UNREACHABLE is raised for exactly this condition and is
never folded into another one, which is what lets an orchestrator tell
"offline" apart from "that issue does not exist" (SPEC §10, and the verb's
own row in CLI.md).
| Verb | Where it stops |
|---|---|
codecrew status |
The load. Nothing of the board is printed. |
codecrew role <name> |
The load — the routing answer is local in a hub, but the verb still asks GitHub to name the current repository first. |
codecrew task new/start/finish |
The load. |
codecrew milestone new/close/evidence |
The load. |
codecrew checkpoint |
The load. The gate is not raised; nothing local records it either. |
codecrew roles show <role> (from a spoke) |
The hub's contract has to be fetched, so it refuses GH_UNREACHABLE rather than reporting a contract that is merely elsewhere. |
A dry run is no way past this, for the three verbs in the tables above that
take one. task finish, milestone new and milestone close accept
--dry-run, and it means "every gate in order, then what the verb would do,
nothing written" — the gates are read from GitHub, so the preview needs the
network exactly as much as the write does, and offline all three stop at the
load with the line above. The other three verbs in those two rows never had
the flag: task new, task start and milestone evidence reject it while
parsing their arguments, before the load and identically online, with
flag provided but not defined: -dry-run or the verb's usage line.
migrate --dry-run is the exception, and it is the useful one. It is the
fourth verb with the flag, and the only preview that completes with no
network — because what it previews is local. Offline it lists every move,
write and rewrite it would make, reports the label step it cannot do as the
same note:, ends dry run: nothing written and exits 0, working tree
untouched. Use it: the migration is the one destructive local operation the
CLI has, and being offline is no reason to run it unseen.
One condition in this table is not about the network at all, and reading the
code rather than the symptom saves the confusion: roles diff from a spoke
fails with no local .codecrew/roles/implementer.md — run from the hub
(spokes hold no contracts) whether or not you are online — a spoke holds no
contracts to diff.
The identity verbs are the exception worth knowing. identity token and
identity webhook talk to api.github.com directly rather than through
gh, so they never reach the refusal above. Offline they exit 1 with the
transport error alone and no refusal code:
codecrew: Get "https://api.github.com/app/installations?per_page=100": dial tcp: lookup api.github.com: Temporary failure in name resolution
Their local refusals are unaffected: a missing key or an unusable one still
refuses NO_CREDENTIALS or BAD_CREDENTIALS with no network, because
resolving the credential and signing the App JWT happen on this machine. Only
the two calls after that — discovering the installation and exchanging the
JWT for the one-hour installation token — need GitHub. A token you already
hold keeps working until it expires, and it cannot be renewed offline.
The recipe: work begun offline¶
The boundaries of a task are GitHub's; the middle is yours. So:
Offline.
- Read the contract —
gh codecrew roles show implementerin a hub, or the copy in.codecrew/roles/— and.codecrew/AGENTS.md. - Draft the plan in a file. Be honest with yourself about what this costs:
SPEC §4 wants the plan on the issue before the first commit, and
offline you cannot put it there. Writing it first and posting it
unchanged when you reconnect keeps the substance of the rule — deciding
before doing — and posting a plan you have already implemented against,
edited to match what you did, does not.
task startrefusesNO_PLANuntil the section is on the issue either way. - Branch locally, with the name
task startwould create:task/<issue number>-<slug of the title>. The slug is the title lower-cased with every run of non-alphanumeric characters collapsed to a single-, trimmed to 40 characters. - Implement, commit, run the tests. Commit messages reference the task
(
(#123)) exactly as they would online.
Back online.
- Put the plan on the task issue.
gh codecrew task start <ref>— it verifies the plan first and refusesNO_PLANwithout one, then assigns the issue to you (not to an App: GitHub does not accept one as an assignee, so an App-held seat is never offered), posts the start record that makes you the ownertask finishwill hold to, and creates the linked branch.- Reconcile your local branch with the one it just created, then push, open the pull request, and carry on.
What step 6 actually does to a branch you already have. task start
creates the linked branch through GitHub (gh issue develop), which knows
nothing about your machine. A local branch of that name is not consulted, and
the branch GitHub creates is cut from the default branch's current head and
carries none of your commits. The verb reports success and prints, for a
seat that commits and a branch GitHub created:
linked branch task/329-working-offline-what-runs-what-waits-and created
locally: git fetch && git switch task/329-working-offline-what-runs-what-waits-and
started radiusred/gh-codecrew#329 as @radiusred-cody[bot]
The last line is the receipt every start prints. The first two are not
guaranteed: a qa or reviewer caller gets role <name> does not commit …;
no linked branch created in their place, because those contracts forbid
commits, and a DevelopBranch that GitHub refuses becomes a note: naming
the branch to create by hand. It is the second line that matters here.
Follow that second line literally and nothing happens, which is the part to
watch for: git switch finds the local branch you already have and stays on
it — "Already on …" — rather than checking out the branch that was just
created, and because a local branch of the name existed, git set up no
tracking either. You are on your own commits, with no upstream, next to a
remote branch of the same name that is linked to the issue and empty.
Reconciling the two is one rebase, and the push that follows sets the upstream that is missing:
branch=task/329-working-offline-what-runs-what-waits-and
git fetch origin
git rebase origin/$branch # your commits, replayed onto the linked branch
git push -u origin $branch
If you branched from the same commit the linked branch was cut from — the common case, since both come from the default branch — the rebase is a no-op and the push is a fast-forward. If the default branch moved on while you were offline, the rebase is where you find that out, and it is the rebase you would have done before opening the pull request anyway. Push under the same name: the link between the issue and the branch is the name, so a branch pushed as something else is not the linked one.
Two things not to do. Do not git switch to the fetched branch under a new
name and cherry-pick onto it — that is the same commits twice, and a tangle
to unpick at review. And run task start before you push, not after: the
linked branch is the one gh issue develop creates, and what it does with a
branch already sitting on the remote under that name, put there by something
else, is not a question this page has tested. Taking the steps in the order
above never asks it.
Re-running task start after the linked branch exists is safe on GitHub's
side — the call is idempotent, it answers with the existing branch, creates
no second one and moves nothing — but the verb posts another start record
every time it runs, since the latest record is what names the task's owner
(SPEC §4). Run it once.
None of this is task start being clever, because today it is not:
#324 is the capture
for making it reconcile a branch begun offline — assign, verify the plan and
link the branch that is already there rather than create a second — and until
that ships, the rebase above is the step you do yourself. This page describes
what the CLI does today, and promises nothing of #324.
Why there is no offline mode¶
Two things follow from the protocol's own shape, and both are deliberate (SPEC §1, §4).
The record is the source of truth, and it is shared. A decision comment written to a local queue is a decision nobody can see, react to or gate on until it syncs; a gate raised offline is not raised. Under latest-wins, replaying a queue after a conflicting change on GitHub silently overwrites somebody's answer. So the verbs refuse loudly instead, with a code an orchestrator can branch on, and the operator decides what to do about it.
And there is no local mirror to fall back to. A hub reads its own pointer and
its own contracts from disk — which is why roles show and roles diff work
there — but the issues, the labels, the reviews and the checks have no
on-disk representation to consult. What CodeCrew keeps on your machine is
what git already keeps: the code, the branch, and the contracts.