Ship a ticket¶
This page takes one ticket from its GitHub issue to a merge on the milestone's feature branch. It is for the ticket's owner, a person or an agent. The orchestrator's side (batching, dispatch, verification and landing across many tickets) is in Run a milestone.
The design behind each step lives elsewhere. Pull requests: small and stacked holds the PR rules, Verification scope holds the three check stages, and CI holds the jobs and the merge queue.
Before you start¶
- Set up the clone once with Agent workflow setup, and read the standing orders. They apply to a person as well as to an agent.
- Install the tools with
just install: Go,just,ghand Graphite (gt). Authenticateghand rungt auth --token <token>once. .monaco/agents.tomlnames the feature branch (feature_branch), the tracking issue (tracking) and the per-row check budgets.feature_branch = "auto"means theFEATURE_BRANCHrepo variable (or theMONACO_FEATURE_BRANCHenv when set), which each checkpoint moves to the next<name>-<N>. The examples below write it as<name>-<N>.gh variable get FEATURE_BRANCHprints the current name.- An agent owner gets a dispatch prompt with five fields:
ticket,worktree,parent,briefandorders. The brief isdocs/agents/owner.md, andordersnamesdocs/agents/standing-orders.md. The worktree already exists at the parent SHA, and.git/.monaco/agents/<ticket>.jsonregisters it, so the agent guard hooks apply to it. - A person owning a ticket makes the worktree by hand (step 2). The Claude Code hooks do not run in a plain terminal, so a person follows the same rules without the guard.
Steps¶
-
Read the ticket in full.
gh issue view <n>The header line names the milestone,
Blocked by, the base branch andTouches. Change only the pathsToucheslists. When the Proposal says "Implement exactly", do that and nothing more. An unrelated defect you find goes under "What came up" in the PR body, or into a new ticket. -
Work in a worktree under
.worktrees/, never in the primary checkout. A dispatched agentcds into theworktreepath from its prompt. A person makes one at the feature-branch tip:git fetch origin <name>-<N> git worktree add --detach .worktrees/<n> origin/<name>-<N> cd .worktrees/<n>Use absolute paths inside the worktree. An agent's shell may reset its working directory between commands.
The worktree needs no secrets setup. The secret recipes find the key in this order:
.env.keysin the worktree,.env.keysin the primary clone,DOTENV_PRIVATE_KEY_LOCALorDOTENV_PRIVATE_KEY, then Dotenvx Armor. See Secrets in worktrees. Never copy.env.keysinto a worktree. -
Start the first branch and track it on the feature branch:
git switch -c <n>-<slug> gt track --parent <name>-<N>Each further PR of the stack starts with
gt create <branch> -m "<subject>". Amend the current branch withgt modify, which also restacks the branches above it. Order the stack so each PR proves the next: deletions and renames, then schema, thendomainandapp, then adapters and HTTP, then theflows.tsvstatus change. -
Write the change. Read
apps/backend/AGENTS.mdfirst for backend code, and the skill it names for the job:Change Read first A command, query, consumer, provider or module .claude/skills/go-backend-module/SKILL.mdGoroutines, channels or fan-out .claude/skills/go-concurrency/SKILL.mdLedgers, shares, swaps, transfers, the relayer or Privy signing .claude/skills/money-change/SKILL.mdA consumer or a new event .claude/skills/nats-consumer/SKILL.mdA failed e2ejob or a flow moving to verified.claude/skills/verify-backend/SKILL.md -
Commit with a Conventional Commit subject:
type(scope): subject, where the type is one offeat,fix,docs,style,refactor,perf,test,build,ci,choreorrevert. PR format fails any other subject in the PR's commits. -
Run stage 0 from
apps/backendon the committed tree:cd apps/backend go run ./cmd/monacoctl agents checkIt diffs
HEADagainstorigin/<name>-<N>and runs one row per kind of changed path: PR size and gate changes always, thengo build,go vet, the lint row andgo test -short -count=1on the affected packages, and the shell,scripts, Python, Swift,ready, migration, OpenAPI andmkdocsrows when their paths changed. Verification scope lists every row and its trigger.- Each row has its own budget under
[check.budget]in.monaco/agents.toml. Thego test -shortrow has none as a whole. Instead each package gets thepackagebudget (20 s), the same limit that fails a package in CI. - It prints at most 20 lines. The full log is the
log:path it prints, under.git/pstack/<milestone>/logs/. - It refuses a dirty tree, because it records
HEAD's tree. On a pass it writes.git/pstack/<milestone>/checks/<tree>, and the hook lets an owner's push through only when that record matches the current tree. - Run it again after every
gt modify. A tree that already passed printsstage 0 already passed. -
If only a budget fails and the machine is loaded (check
uptime), rerun it once the load drops. Run the named package on its own to prove the code is fine:go test -short -count=1 ./<pkg>. -
Before stage 0, run what the change must regenerate, and commit the output. CI's
readyjob (scripts/ci/ready.sh) fails on a stalego generate ./...,go mod tidy, sqlc output,scripts/gen-docs.shoutput or theverify-backendfeature map.scripts/ci/ready.shruns the same steps locally on a committed tree.
Never run
just test backend,go test -raceorgo test ./...fromapps/backendas an owner. Never run mutation testing locally. The merge queue runs the full suite, and the nightly runs mutation. - Each row has its own budget under
-
Push the stack as drafts:
gt submit --stack --no-interactive --draftA draft runs no CI. Without
--drafta new PR opens ready, with the commit subject as its title and an empty body, and PR format fails at once. -
For each PR, write its body to a file and set it:
scripts/pr-body.sh <pr> "<title>" <file>The script runs the full PR format check locally, sets the title and body, and marks a draft ready, which starts stage 1. Fix whatever it reports and run it again. Never set a body with
gh pr edit --bodyand never rungh pr readyyourself.- Title: what the PR changes, in the present tense, with no issue number and no
feat:style prefix. For example, "Name the child that spent the verify teardown budget". - Body: the six sections of
.github/pull_request_template.md: TLDR, Why, What changed, Proof, What came up, Reviewer focus. Thepr-summaryskill in.claude/skillsdrafts it. - Why says
Part of #<n>. The ticket's last PR saysCloses #<n>instead and adds a## Needs from Logansection, holding "Nothing." or a checklist of what only the operator can do. - Proof pastes the
agents checkoutput and says that CI runs the rest. Write the tree hash it prints as<tree>. - Cite only commit SHAs that are already on the feature branch. The check treats any 7 to 40 character hex string as a commit, and a restack changes the PR's own SHAs.
- A docs-only PR says
No code paths affected:in Proof and names the paths.
- Title: what the PR changes, in the present tense, with no issue number and no
-
Stop. An owner never merges. The verifier reviews the diff against the ticket, posts a
verifystatus and lands the PR (see Run a milestone). A dispatched agent exits with its PR URLs, head SHAs, the stage 0 summary and every decision it made.
Hand off a ticket¶
When you stop before the ticket is done, leave it so another person or agent can pick it up from a fresh clone.
- Commit what you have and push the stack as drafts:
gt submit --stack --no-interactive --draft. Unfinished work still goes up. A draft runs no CI. - Keep or open the draft PR, and write the state into its body: what is done, what is not, and what you decided. Put where you stopped and the next step under "What came up".
- Post a comment on the ticket that names the branch, the PR and the next step:
gh issue comment <n> --body "Handing off: branch <branch>, draft PR #<pr>. Next: <step>."
The next owner reads the ticket, its comments and the draft PR. It makes a worktree at the feature-branch tip, pulls the stack into it by its top PR, and carries on from step 4 of Steps:
git worktree add --detach .worktrees/<n> origin/<name>-<N>
cd .worktrees/<n>
gt get <top pr> --no-interactive
What CI runs after you push¶
| Stage | Trigger | Runs |
|---|---|---|
| 1. PR check | the PR is ready and based on the feature branch | plan, lint, ready, vuln, PR format, PR size and gate-changes. No tests. A push with an unchanged diff reuses the last green result. |
| 2. Queue check | the PR entered the merge queue | Stage 1, plus backend (the race suite with the per-package budget and 100% coverage), the tests -short skips, e2e (scripts/ci/e2e.sh), flake on changed test files, scripts, and mobile-core and ios when their paths changed |
Only the bottom PR of a stack runs stage 1, since CI runs on PRs whose base is the feature branch. The only required check is ci / ci-ok. gate-changes and the status check from agents-status.yml never block. What runs where has every job.
Read the checks¶
Read the checks once, and never watch them:
gh pr checks <pr>
The result says which of these holds:
- Pending: nothing to do. The owner has already exited.
ci / ci-okfailed: open the failing job's log from the link and fix the cause withgt modify,agents checkandgt submit --stack --no-interactive --draft.PR format (title, body and commits)failed: see the first entry under When a check fails.
When a check fails¶
PR format failed because of the body. Fix the body file and run scripts/pr-body.sh again. The body edit runs a fresh check. Never use gh run rerun --failed here. A rerun replays the original event with the old body and fails again.
PR format failed on a commit subject. Reword the commit with gt modify (Conventional Commit subject), rerun agents check and submit again.
PR size failed. Split the branch below 1000 changed lines per PR with gt split --by-hunk, or with the distribute-stack-changes skill. Only a person adds the large-pr label. A committed binary also fails this check. Build into bin/ or /dev/null, never into the tree.
Stage 1 shows a stale red ci / ci-ok from a cancelled run. A newer push cancelled it. The newest run is the one that counts.
The queue ejected the PR. monacoctl agents watch reports it with the failing job and a prompt for a fresh owner. Read the failing job's log, then act on the cause:
| Cause | Fix |
|---|---|
Infra flake: a Go proxy or Docker Hub error, a curl 500 while installing a tool, a runner timeout with every package ok, a run cancelled by a newer one |
Requeue with no code change: gh pr merge <pr> --auto for a single PR, monacoctl agents land-stack <top-pr> for a stack. For a failed PR-stage job, gh run rerun <run-id> --failed once. |
| A real test failure, including a package over the 20 s budget | A fresh owner fixes it on the same branch. The verifier reviews the new head, then the PR lands again. |
| A failure that already exists on the feature branch tip | Not this PR's defect. Fix the tip in its own PR, land that first, then requeue this one. |
| A merge conflict with a stack that landed ahead | Wait for that stack to land, restack onto the tip (Restack a stack), build every branch, and land again. |
land-stack reports a stack still marked queued |
Run monacoctl agents land-stack <top-pr> again. It sees the ejection, clears the mark and relands. |
A test flakes. Fix it the same day, or move it to the nightly with an issue. Never skip it, never raise its budget, and never retry CI until it passes. The rule is in Keeping it fast. The flake job reruns every changed test file 20 times, so a fix proves itself in stage 2.
The e2e job failed. Follow .claude/skills/verify-backend/SKILL.md. It explains the verify-evidence artifact and how to reproduce one flow with monacoctl verify flow <id>.
Money paths¶
A PR that moves or records money meets every item in .claude/skills/money-change/SKILL.md, and its body says how. The verifier checks at least these points by reading the diff:
- Amounts use
money.Micros,money.SignedMicrosormoney.BaseUnits, never floats. - Each balance change and its event commit together inside
uow.Do, with a guarded update. - A transfer stores its signature before broadcast.
- The relayer floor holds and was not lowered to pass a test.
- No key, token or signed transaction reaches a log.
- An external API's units match a recorded real response. In M7 a Jupiter
priceImpactPctread as a fraction instead of a percent would have refused every real trade.