Issues are the unit of work
Nothing starts from a prompt. Every change starts from a tracked issue, and that is not bureaucracy — three separate mechanisms in this harness key off the issue number:
scripts/wt new 1234looks up the issue title and derives the branch name (feat/1234-<title>) and worktree path from it.scripts/check-issue-collision.shreads the issue number back out of the branch name on every push, and blocks the push if that issue already has an open MR from another branch. It works however the branch was created, including a plaingit checkout -b.Closes #1234in the MR description is what actually closes it on merge. Without the keyword the issue stays open and the tracker silently drifts from reality.
The loop
Section titled “The loop”glab issue list --assignee @me # what is mineglab issue view 1234 # read it — including the COMMENTSglab issue update 1234 --label status::wip # optional: tell others you have itscripts/wt new 1234 # branch + worktreecd ../<repo>-wt/1234-short-slug && source .envrc# ... work, with the gate chain from the fast-path table .../mr # opens the MR with Closes #1234# ... merge when green ...scripts/wt prune # from the main checkoutRead the comments before starting, not just the description. A stale branch or a half-done attempt is usually recorded there and nowhere else.
Writing issues the harness can use
Section titled “Writing issues the harness can use”- One issue, one acceptance criterion set. If a discussion surfaces a second problem, file it. A facet recorded as a comment on an open issue is not tracked — it dies the moment the host issue closes on a different facet. The test is simple: does it need its own acceptance criteria? Then it needs its own issue.
- Labels are load-bearing, not decoration. The claim label (
status::wipby default, set withWIP_LABEL) is one you apply yourself. Nothing applies it automatically. The collision gate warns when a pushed branch’s issue carries it but has no MR yet. Priority labels are read by/voc-auditwhen it decides between “file new” and “boost priority”. - Beware label gates that read the tracker rather than the repo. If you add a CI job that fails on a forbidden label, remember its input is not your diff — someone else’s label edit can redden the default branch, in a job log the next person to push has no reason to read. Scope such gates narrowly and say so in the job comment.
- Milestones drive
/dotplanning. It resolves a milestone’s scope into a feature→asset map and flags every screen, endpoint, or decision with no issue behind it. An empty milestone gives you an empty plan. - A dated milestone is a promise, so every issue in one states its share of it.
One scoped label per issue —
release::committed(ships or the release slips, and the only issues that become roadmap bullets),release::reserve(a slot held for inbound work from real users),release::stretch(ships if there is time, moves at feature freeze). Scoped labels are mutually exclusive, so the tracker enforces the “exactly one” part. An agent asks which applies on every move into the milestone and never infers it: a guessedcommittedis a promise nobody made, and a guessedstretchquietly drops work someone meant to ship. Unanswered means unlabeled and said out loud — an unlabeled issue in a dated milestone is a visible gap, a guessed label is an invisible one.