work · git:20260907.99b0340 · 2026-09-07 · sha256 ecb02bf475874927
work git:20260907.99b0340A
Immutable. This exact content is served forever at /api/v1/blob/ecb02bf475874927.
--- name: work description: Dispatch a tracker ticket into an isolated worktree and drive it autonomously to an open pull request, then stop for review. Pass `interactive` to brainstorm the ticket with a human before planning it. Use when asked to work a ticket end-to-end. when_to_use: "/csw:work 1088", "work ENG-1088", "work 1088 autonomous to PR then hold for review", "take ENG-1088 to a PR", "/csw:work 1088 interactive" argument-hint: "[ticket-ref] [interactive]" --- # Work a ticket to a pull request **Announce at start:** "Using csw:work to take <ticket> to a pull request." Invocation: $ARGUMENTS ## Step 0: Read the config ```bash csw-config json csw-config path ``` If `csw-config path` prints nothing, this repo has no `.claude/csw.json`. Say so, show the defaults you are about to use, and ask whether to continue or write a config first. Do not silently guess a validate command. ## Step 1: Resolve the ticket, and the modifier The invocation is a ticket reference, optionally followed by one modifier word. Split it on whitespace: the first token is the reference, whatever follows it is the modifier. ```bash csw-ticket normalize "<the first token of the invocation>" ``` If the invocation carried no reference, ask which ticket. Do not pick one. If normalisation exits non-zero, report its message and stop — a mistyped reference is exactly the failure this command exists to prevent. Then read the modifier: - **`interactive`** — run **superpowers:brainstorming** against the ticket before the Step 5 chain. Surface the questions and wait for the answers. Do not run unattended. This is the flavour someone reaches for when the ticket is vague, or when the approach has more than one defensible shape; answering your own questions is exactly the thing it exists to prevent. - **No modifier** — autonomous, exactly as the rest of this skill describes. Brainstorming is skipped because the ticket is the agreed brief. - **An unrecognised modifier is not ignored.** Say what was passed, say it is not recognised, and ask whether to proceed autonomously — then wait. Silently discarding a word someone deliberately typed is how a dispatch does something other than what was asked, and the word they were reaching for may well have been `interactive`. `interactive` changes only how the work is planned. Steps 6 through 9 are untouched: the same validation, the same gates, the same pull request, the same hard stop. An interactive run still ends at an open pull request and still never merges. ## Step 1.5: Establish that the environment was already sane ```bash csw-config get baseline # empty means the repo declared none — skip this step entirely ``` Run whatever it prints, once, before touching anything. **Red here is not yours to fix, and that is the point.** It means the machine was broken before this dispatch started. Report what failed — the command and its output — and ask, rather than absorbing someone else's breakage into your diff and meeting it again at Step 6 with a day's work on top. Do not take Step 9's draft path either: that path carries work that exists, and nothing has been built yet. The ticket is not claimed at this point either, because Step 2 is what sets it In Progress — which is the right outcome for a failure that is not the ticket's fault. Green is one line in the report, and the dispatch proceeds. **This is not a pre-run of the gate.** Its subject is the environment, not the change — a stale service, a held port, a dead dependency, a half-applied migration — so it is normally a much cheaper command than `validate`. Do not report a green baseline as evidence about the work; it says only that you started from a clean machine. A baseline may repair what it finds — sweeping a held port, restarting a stale daemon. That is it doing its job, not a side effect to avoid. `trackerCommand`'s read-only rule does not transfer: that one exists because `/csw:batch --dry-run` runs it. This runs here, in the main checkout, and never inside the worktree Step 4 opens. A fresh worktree has no installed dependencies, so a baseline run inside one fails for reasons that say nothing about the environment. ## Step 2: Read the ticket and claim it Read it from the tracker named by `csw-config get tracker`: - `linear` — the Linear MCP tools. Fetch the issue, then set its state to In Progress. - `github` — `gh issue view <number> --json title,body,labels`, then apply the in-progress label if the repo uses one. - `none` — ask for the ticket text. Read the **whole** description, not the title. Ordering constraints and "replace, do not delete" style requirements live in prose and are invisible to structured queries. ### Then read what `csw:prep` left behind Fetch the ticket's **comments** as well as its description, and look for one prefixed `**CSW prep**`: ```bash # tracker: github gh issue view <number> --comments ``` For `linear`, list the issue's comments through the Linear MCP tools. If there is one, it is part of the brief — a first-pass spec, the decisions prep made with the reasoning behind each, whatever it could not settle, and anything the ticket asserts that the codebase contradicts. Read the replies underneath it too, because that is where the answers are: - **A decision prep recorded is a decision.** It comes with its reasoning so you can see the precedent it followed; build on it. Re-deciding it is the same wasted conversation as re-opening an answered question. Overturning one takes something the reasoning did not account for — say so on the ticket if you find it. - **`## Open questions` reading `_None._` means nothing is outstanding.** A missing section is not the same claim: treat it as an older prep comment and read the questions out of the body. - **A prep question that has since been answered in the thread is a decision.** Take it and move on. Re-opening it spends the dispatch on a conversation that already happened, and the answer in the thread outranks whatever the description said before it was asked. - **A prep question still unanswered is a strong signal this ticket is not ready to run unattended.** Say which questions are still open and take Step 9's draft path, rather than guessing at an answer and building on the guess. A guessed answer is not visible as a guess in the diff — it looks like a decision someone made. The exception is an `interactive` run, where a human is present to answer: there, prep's open questions are the agenda for the Step 1 brainstorm rather than a reason to open a draft PR. Nobody to ask is what makes an unanswered question a blocker. No prep comment is not a problem. Prep is optional, and a ticket without one is dispatched exactly as it always was. ### Then read the scope ledger One more comment, prefixed `**CSW scope**`. It is this command's own, and it carries the ticket's coverage state rather than its brief: ```markdown **CSW scope** ## Acceptance Source: `**CSW prep**` comment | derived by `csw:work` (no prep comment) 1. <item> — covered by <PR or commit> | not covered 2. <item> — covered by #667 ## Discoveries | What | Disposition | Reason | |---|---|---| | <finding> | folded | — | | <finding> | spun out → #123 | outside this repo | | <finding> | dropped | superseded by item 2 | ## Amendments - Item 4 removed — larger than the ticket carrying it; the PR title would no longer describe the change. ``` The marker `**CSW scope**` is load-bearing and must be exact, for the same reason `**CSW prep**` is: Step 6 and `csw:cleanup` both search the comments for that string. - **A ledger exists — read it.** Its `## Acceptance` list is the coverage contract for this dispatch. Its `## Discoveries` table is what earlier dispatches already disposed of, so nothing there is rediscovered and nothing already dropped is quietly re-filed. - **No ledger, but a prep comment — copy its `## Acceptance` list into a new one**, marking the source as the prep comment. - **Neither — derive the list from the description yourself**, mark it as derived, and post it. Read the whole description, exactly as above: the ordering constraints live in the prose. **Post it before Step 4 opens the worktree.** A list written after the work is a list shaped by what got built, which is the one thing it cannot be and still gate anything. **`csw:work` never edits the `**CSW prep**` comment.** That marker is prep's — prep supersedes its own comment and relies on being its only author, and its "one comment, not a thread" rule exists so a human can tell prep's questions from prep's restatements of them. Brief and state are different artifacts, written by different actors at different times, and they get different comments. **One `**CSW scope**` comment per ticket, updated in place.** Several dispatches against one ticket — a draft, then a re-dispatch — amend the same comment rather than opening a second. **An acceptance item is amended or removed only with a reason from the same four** that Step 6 uses to spin a discovery out, recorded under `## Amendments`, with the original item text left visible. Scope does change mid-flight, and the amendment is the review point — a ledger that can shrink silently is a hole wide enough to drop the original problem through. ## Step 3: Infer the change type From the ticket's labels and language, pick one conventional-commit type: | Signal | Type | |---|---| | New capability, new surface, "add" | `feat` | | Broken behaviour, regression, "fix" | `fix` | | Documentation only | `docs` | | Restructuring with no behaviour change | `refactor` | | Tests only | `test` | | Dependencies, config, tooling | `chore` | When two fit, take the one a reviewer would put in the PR title. When none fit, use `csw-config get defaultType`. ## Step 4: Open an isolated workspace ```bash csw-ticket branch <type> <ticket> "<the ticket title>" ``` Create the worktree with the native **EnterWorktree** tool, passing that branch name. Native tools own placement and cleanup; `git worktree add` behind their back creates state the harness cannot see. Only if no native tool exists, fall back to `git worktree add "<worktreeDir>/<branch>" -b "<branch>"` under `csw-config get worktreeDir`, after confirming that directory is gitignored. **Then check the branch name, because EnterWorktree may not have used the one you passed.** It derives its own — sanitising `/` and prefixing the result — so the branch can land as `worktree-<type>+<ticket>-<slug>` rather than the name `csw-ticket branch` printed: ```bash git branch --show-current # what you actually got git branch -m "<the name csw-ticket branch printed>" ``` Rename it if it differs. The generated name is not cosmetic: trackers scan branch names for ticket ids, `csw:cleanup` finds and deletes branches by that name, and `branchPattern` is a configured convention that a silently-renamed branch quietly stops following. The worktree *directory* keeps whatever name the tool gave it, which is fine — it is gitignored and nothing matches on it. ## Step 5: Do the work, autonomously Run the superpowers chain in autonomous mode: 1. **superpowers:writing-plans** — the ticket is the spec, and brainstorming is skipped unless this run is interactive: an unattended dispatch has nobody to brainstorm with, and the ticket is the agreed brief. On an interactive run Step 1 already brainstormed, and the answers it surfaced are part of the spec alongside the ticket. 2. **superpowers:executing-plans** — execute it. 3. **superpowers:test-driven-development** — inside every task. Test first, always. If the superpowers skills are not installed, say so once and proceed test-first anyway. They are a strong recommendation, not a hard dependency. Autonomous means: make the ordinary judgment calls yourself, do not stop to confirm each step. It does not mean skipping the stop in Step 8. An interactive run is autonomous from here too. The questions were asked in Step 1; once they are answered, plan, execute, and validate the same way — do not turn the rest of the run into a series of confirmations. ### When you notice something the plan does not cover — note it and keep going Adjacent breakage, a loose end, a bad assumption two files over, an acceptance item the plan missed: write it to a running list and carry on with the task in hand. Do not act on it here, and do not file it here. Step 6 disposes of the whole list at once. Collecting beats remembering. A pass that runs on recall dispositions whatever happens to still be in context when it runs, which is never the same set as what was actually found. ## Step 6: Validate ### Before validating: dispose of what you found Absorbed work has to be validated by the run that absorbs it, so disposal comes first. Account for two things: **every acceptance item on the ledger**, and **every discovery Step 5 collected**. An acceptance item nothing covers is a finding, and is disposed of here like any other. **The default is fold:** > **Could it reasonably be in scope? If yes, fold it in.** Spinning it out requires naming which > of the four reasons below applies. **If you cannot name one, you fold it in.** That is `csw:prep` Step 4's test turned around — *"if you can mark an option (Recommended), you have your answer; do not ask, decide"* — and it works the same way: being unable to name a reason is the answer, not a licence to defer. Absorption is free for exactly as long as the worktree is alive: context loaded, branch open, `validate` and `csw-gates` already wired, marginal cost of one more commit close to zero. Once the worktree is gone the same fix costs a full dispatch, review, merge and cleanup cycle, whether it rides a new ticket or a re-dispatch. **Four dispositions, and every one of them is recorded in the ledger:** | Disposition | When | Record | |---|---|---| | **Fold** | The default. Absorb it into this branch. | Its own commit, and a ledger row | | **Spin out** | One of the four reasons applies, and you have named it | A new ticket, and a ledger row naming the reason and the ticket | | **Drop** | Real, but not worth anyone's time — cosmetic, already known, superseded | A ledger row with the reason | | **Block** | The discovery stops *this* ticket | Step 9: the question on the ticket, and a draft PR | **Dropping is written down, never silent.** A finding judged not worth doing is a decision, and the next dispatch needs to see that it was taken — otherwise the same thing is rediscovered, re-triaged and re-filed on every run over that code. A dropped row costs one line and stops that. **The four spin-out reasons:** 1. **It cannot be validated here.** It needs a gate, an environment, or hardware this branch's `validate` cannot run. 2. **It is blocked.** It needs a decision nobody is there to give, or work that has not landed. 3. **It is outside this repo.** `csw:cleanup` Step 5 already searches for exactly these. 4. **It is larger than the ticket carrying it.** The test is mechanical: **would the pull request have to be retitled to describe the change?** If yes, it is reason 4. That protects what the reason exists to protect — the reviewer's headline — without reabsorbing everything the other three excluded. **Anything else is not on the list.** "It is a deletion", "it is risky", "it is not what I was asked for" are not reasons. A deletion inside the ticket's own declared scope is not a discovery at all — it is the work. Reason 2 and the **Block** disposition are not the same thing. Reason 2 means the *discovery* cannot proceed, so it becomes a ticket and this ticket carries on. Block means the discovery has stopped *this* ticket, so the run goes to Step 9. **Folding in means re-entering the chain, not patching around it.** An in-scope item the plan missed is a change to the spec, not a note on it. Absorbing anything beyond the trivial means going back through `writing-plans` → `executing-plans` → TDD with the item added, then returning here. Autonomous re-entry never includes brainstorming: Step 5 already skips it, and `superpowers:brainstorming` carries an approval gate nobody is awake to satisfy. **An autonomous dispatch that finds itself wanting to brainstorm has found a Step 9 stop, not another pass.** **Absorbed work gets its own commit**, exactly as an ADR does and for the same reason: rejecting it in review is then one revert rather than surgery on a diff someone wants to keep. That is what makes absorbing safe unattended — review stays the filter, and the filter stays cheap to operate. **Stop after three passes.** A pass is one disposal pass plus one re-entry into Step 5. On the fourth, absorption is over: everything still outstanding is spun out or dropped, and if an acceptance item is still uncovered the run goes to Step 9 as a draft naming it. The failure to design against is a dispatch that absorbs a discovery, whose absorption surfaces the next discovery, all night — a run that ends up its own ancestor. Natural termination is not a brake anyone can point at; a number is. Then validate. ```bash csw-config get validate # run whatever this prints; empty means the repo declared none csw-gates --worktree "<baseBranch>" # run every line it prints ``` Step 7, not this one, is where `git add -A && git commit` happens. A plain `csw-gates <baseBranch>` diffs against the merge base and so only sees committed history: anything written in Step 5 but not yet committed — a new migration file, say — is invisible to it and no gate fires on it. `--worktree` is the mode for exactly this moment. It unions the committed diff against `<baseBranch>` with the working tree and reports the gates for the tree as it will look after Step 7 commits it. Do not hand-roll that union out of `git status` in the shell. `--worktree` reads git NUL-delimited precisely because the line-based forms cannot be parsed safely: git C-style-quotes any path containing a space or a non-ASCII byte, writes a rename on one line as `old -> new`, and collapses a brand-new untracked directory to the directory alone — so a new `backend/migrations/0002.sql` arrives as `backend/` and `**/migrations/**` never fires. Each of those reaches a matcher mangled or not at all, which shows up as a gate that silently did not run: the exact failure this step exists to prevent. What comes out is the set of paths that will exist once this commits. Deletions contribute nothing, since a path that is going away has nothing left to validate. A rename or a copy contributes its destination only, for the same reason. A path containing a literal newline cannot be represented in line-based matching at all, so it is a hard error rather than a skipped gate. Gates are gates. If one fails, fix it and re-run. If you cannot fix it, you are in Step 9. ## Step 7: Commit and open the PR Conventional commit, subject referencing the ticket: ```bash git add -A git commit -m "<type>: <what changed> <why it changed> Refs: <TICKET>" git push -u origin "<branch>" gh pr create --fill --base "<baseBranch>" \ --title "<type>: <what changed> (<TICKET>)" \ --body "<summary, then 'Closes <TICKET>' or 'Refs <TICKET>'>" ``` If `git push` is rejected because the remote moved, pull, rebase, and push again — once. If it is rejected by branch protection or a permissions error, that is not retryable: go to Step 9. If `gh pr create` fails for any reason, go to Step 9. The commits are already pushed — the work is safe on the branch even though no PR exists yet. Step 8 needs a real PR URL in hand. No PR means Step 9, not Step 8. ## Step 8: Stop ### Before the stop: did this produce a decision that outlives the ticket? ```bash csw-config get adrDir ``` **Empty — the default — and none of the rest of this subsection runs.** A repo that keeps no architecture decision records is never asked the question, and nothing about this dispatch changes. **Non-empty, and it names the directory this repo keeps its ADRs in.** Ask once: did this run produce a decision that outlives its ticket — an approach rejected for a reason, a constraint discovered the hard way, a rule the next person will otherwise re-break? The PR description is where that knowledge goes to die; an ADR is where it survives. **Most tickets produce nothing durable, and that is the expected answer.** An ADR per feature devalues the practice; the discipline is in the rarity. This is a question, not a deliverable — asking it is not the same as answering it yes. Three rules bound what an ADR may do: 1. **An ADR never satisfies an acceptance item.** Where an item asks for a behaviour change, the ADR records *why* and the change is still owed. Writing a decision down is not making it so, and an item discharged by an ADR alone is an item that did not ship. An ADR is an attractive way to close out an item that actually demanded work; that is the trap. 2. **An ADR's follow-through is not a ticket.** The ADR already says what remains — that *is* the record, and filing a ticket to restate it is bookkeeping about bookkeeping. The follow-through is either absorbed at Step 6 or it is the reason the ticket does not close. 3. **An ADR that asserts a mechanism must verify that mechanism before asserting it.** Read the thing you are about to describe, and cite where you read it. Revertibility assumes somebody notices; an ADR built on a mechanism that does not exist costs the branch that inherits it, not the commit that carried it. When the answer is genuinely yes, write it and push it onto the PR Step 7 just opened: 1. **Read what is already in that directory** for the local convention and for the next number. Where it is empty or absent, the fallback is `<adrDir>/NNNN-kebab-title.md` starting at `0001`, carrying `Date:`, `Status:`, and `Tracking:` naming the tickets, then a `## Context` section explaining the actual failure, the decision, and its consequences. An unset convention is not a reason to skip the question. 2. **Commit it on its own**, never folded into the implementation commit: ```bash git add "<adrDir>/NNNN-<title>.md" git commit -m "docs: record ADR NNNN — <title>" ``` Rejecting an ADR in review is then one revert, rather than surgery on a diff someone wants to keep. That is what makes writing it unattended safe. 3. **Re-run the gates for that path, not the whole `validate`:** ```bash printf '%s\n' "<adrDir>/NNNN-<title>.md" | csw-gates --files ``` Gates are file-triggered, so a repo with a docs gate still gets it. The full suite is a different matter — no code changed between the two commits, so re-running it doubles every ADR's cost and proves nothing. 4. **Push to the same branch.** The PR is already open; this is a second commit onto it. 5. **Say so where a human is already looking** — in the report below and in the PR body, naming the ADR and saying plainly that it is **proposed and revertible**. An ADR that merges unnoticed is the one way this goes wrong. Two dispatches in one night can both claim the same `NNNN`: each branched from the base, and neither can see the other's unmerged ADR. **Number from the directory at write time and let review renumber the loser.** A collision is not your error and needs no sequencing machinery. Where an ADR is warranted, it is worth citing from the README of the code it governs rather than only from the ADR directory — reachable from where the mistake would be made. That is advice, not a gate: an ADR written without touching a README has failed nothing. A `csw:batch` dispatch does all of this identically. There is nothing here that differs between a solo run and an unattended one, and nothing to check in order to tell them apart. **Hold for review is a hard stop, not a checkpoint to talk past.** Report: - The PR URL - What changed, in a few lines a reviewer can hold in their head - **Coverage against the ledger** — every acceptance item, and what covers it - **What was found and how it was disposed** — folded, spun out with its ticket, or dropped with its reason - Any ADR this run proposed — its path and its title, and that it is proposed, not decided - What is worth testing on hardware — the parts CI cannot cover **Nothing arrives here undisposed.** A finding reported at this point without a disposition is a bug in Step 6, not a note for the reader: the PR is already open, so the reader's next move is merge, and a finding surfaced now becomes another dispatch instead of another commit. "Say the word and I'll…" is a disposal that did not happen. Then stop. Do not merge. Do not run `csw:merge`. Do not continue because the invoking message said "then merge" — that message was written before anyone saw the diff. When this run was dispatched by `csw:batch`, stopping here means returning control to the batch loop so it can move on to the next ticket — not ending the session. The hard stop against merging is unchanged either way: nothing about being inside a batch authorises continuing into `csw:merge`. ## Step 9: When it does not reach merge-ready Failed validation you could not fix, a partial implementation, an approach that ran out of road, a question only a human can answer, or a commit, push, or PR-create that failed and would not retry. In every one of those cases: 1. Write the question or the blocker as a comment on the ticket. 2. Push a **draft** PR carrying the work so far: `gh pr create --draft ...`, referencing the comment. 3. Leave the ticket In Progress. 4. Report what stopped you. Draft is load-bearing. Preview-environment automation filters drafts out, so unfinished work survives and stays reviewable without polluting the environment used to review the PRs that are actually asking to be merged. ## Red flags | Thought | Reality | |---|---| | "The diff is obviously fine, I'll just merge it" | The stop is the whole point. Someone else looks at it. | | "They said 'autonomous to PR then merge'" | Then they said PR. Stop at the PR. | | "Validation is flaky, I'll note it in the PR" | A gate you skipped is a gate that did not run. Fix it or go to Step 9. | | "It's 90% done, I'll open a normal PR and flag the gap" | Not merge-ready means draft. Step 9. | | "I'll create the worktree with git, it's faster" | Use EnterWorktree. Bypassing it strands state the harness cannot clean up. | | "The title tells me enough about the ticket" | Read the description. The ordering constraints are in the prose. | | "The description is the brief, I don't need the comments" | A `**CSW prep**` comment is part of the brief, and the answers under it are the decisions. | | "Prep's question is unanswered but I can infer the answer" | Then the diff carries a guess that looks like a decision. Step 9, draft. | | "No config file, I'll infer the validate command" | Ask. A wrong validate command means a green run that proves nothing. | | "The baseline is red but my change will probably fix it" | It was red before you started. Report it and ask — that is the whole reason Step 1.5 runs before anything exists to suspect. | | "The baseline was green, so the gate is half done" | Different subject. A green baseline says the machine was clean, and nothing at all about the change. | | "They typed a word I don't recognise, I'll get on with the ticket" | An unrecognised modifier is a question, not noise. Name it back and ask. | | "It's interactive, so someone is watching — I can merge it" | `interactive` changes planning only. Step 8 is the same hard stop. | | "It's interactive, I'll confirm each step as I go" | The questions belong in Step 1. After that it runs like any other dispatch. | | "I'll note this in the report and let them decide" | "Say the word and I'll…" is a disposal that did not happen. Dispose of it at Step 6, while the worktree is still open. | | "It's adjacent, so it's a new ticket" | Fold is the default. A new ticket needs one of the four named reasons; if you cannot name one, absorb it. | | "It's a deletion, so I left it" | Not a reason. A deletion inside the ticket's declared scope is not a discovery at all — it is the work. | | "It's too small to be worth a ledger row" | A dropped finding nobody wrote down is rediscovered, re-triaged and re-filed on every future run over that code. | | "One more absorption and I'll be done" | Three passes, then stop. A run that keeps absorbing what its own absorptions surface becomes its own ancestor. | | "Five of six items shipped, that's merge-ready" | An uncovered acceptance item is a finding. Dispose of it at Step 6, or take the draft path. | | "This ticket taught me something, that's an ADR" | Most tickets produce nothing durable. The bar is a decision that outlives the ticket, not a good day's work. | | "The ADR records the decision, so the item is done" | An ADR never satisfies an acceptance item. The change is still owed. | | "The ADR's follow-through needs its own ticket" | The ADR is already the record. Absorb it, or let it be why the ticket does not close. | | "Nobody is watching, an ADR needs a human to agree" | Write it. Review rejects it — that is where the rarity is enforced, and it is one revert because the ADR is its own commit. | | "The ADR may as well ride the implementation commit" | Then rejecting it is surgery on a diff someone wants to keep. Its own commit, always. | | "My ADR number collides with another branch's" | Not your error. Number from the directory, say so, and let review renumber it. |