Imported from goern/bead-workflow-skills (
AGENTS.md). Install upstream withnpx skills add goern/bead-workflow-skills. Copyright stays with the author.
Agent Instructions
How an agent uses work-on / done-with / deliver, and how to change them
without breaking the pair.
The contract between the two skills
They are one state machine with two transitions, and the shared state lives outside this repository:
| State | herdr tab label | Claude session name | worktree |
|---|---|---|---|
| idle | free |
free |
none |
| working | <bead-id> |
<bead-id> |
<bead-id> branch + worktree |
/work-on performs idle → working; /done-with performs working → idle. Any
change to one side must be mirrored on the other, or the tab label stops being a
truthful busy/free signal. That is the single invariant this repo exists to
protect. The session name is the same label on a second display surface: always
set it from the same argument, in the same step, or the two drift apart.
Running /work-on
Invoked as /work-on <bead-id>. Steps, in order, and each one's failure mode:
bd show <bead-id>— validity gate. An unknown id aborts the whole skill; nothing is renamed and no worktree is created. The bead body is the task description for the rest of the session; do not re-fetch it.scripts/herdr-rename-tab.sh <bead-id>— best effort. Renames the tab of this session (via$HERDR_PANE_ID). Exit 3 = not inside herdr, 4 = pane unknown, 5 = rename rejected; say so and continue.scripts/session-title.sh queue <bead-id>— best effort, same label. Exit 3 = not a Claude Code session. The rename is queued, not applied: see The session rename is deferred below.Skill(wt-switch-create, <bead-id>)— worktree creation is delegated. That skill owns the branch-already-exists, entry-denied and worktree-unreachable recovery paths. Do not hand-rollwt switchorgit worktree addhere; ifwt-switch-createis not installed, install it (npx skills add max-sixty/worktrunk -s wt-switch-create) rather than inlining its logic.- Report one line, then wait.
/work-onsets up a session; it does not start the work. The user drives from there.
The skill never claims the bead in bd. If your workflow wants
bd update <id> --claim, add it after step 1 (see Extension points).
Running /done-with
Invoked as /done-with [<bead-id>]; with no argument it resolves the current
branch. It is a guard first, a cleanup second — steps 2–4 only run once
step 1 passes:
- Current branch is the default branch → nothing to clean up, stop.
- Dirty working tree → stop, report the uncommitted files, touch nothing.
- Not merged and no PR open → stop, report ahead/behind, touch nothing.
- Merged (
main_stateintegrated/empty) or a PR exists (ci.numberpresent, draft included) → proceed.
Then: remove the worktree (ExitWorktree({action: "remove"}), falling back to
wt remove "$BRANCH" --foreground when this session did not create it) and
rename the tab and queue the session name to free. A refused cleanup leaves
both on the bead id — that is the point, not a bug.
/done-with deliberately does not run bd close. Closing the bead is a
judgement call about whether the work is actually done; the skill only reminds
the user that the bead is still open.
Running /deliver
/deliver <bead-id> [--parallel] [--tasks-only] [--team] is a driver, not a
third transition: it calls /work-on at the start and /done-with at the end
and owns everything between. The state table above still holds throughout — a
/deliver run that dies halfway leaves the tab on the bead id, which is
correct, because the work is not finished.
Its own invariants, in the order they bite:
- It leads, it does not implement. Feature code is written by separate
claude --model sonnetsessions, one per child bead, one per herdr tab. The lead plans, briefs, waits, verifies and ships.--teamswaps the tab forteam:dev-loop;--tasks-onlystops after the plan. - The gate is discovered, never invented.
$GATE— the command that must exit 0 before a PR — comes fromCLAUDE.md/AGENTS.md, then the package manifest (package.jsonscripts,Makefile,justfile,Cargo.toml,pyproject.toml), then CI. If none of those answer, ask. A made-up gate goes green on a repo it never ran, and that is worse than no gate at all. Same for$TEST_HOME(where a new test belongs) and$SHELL_CMD(how to enter the dev environment in a fresh pane). - Every spawned session is torn down.
/exit, leave the dev shell, close the tab — on the success path and on the failure path, with the child bead updated first so context survives the teardown.herdr tab listmust show no leftover task tabs when Step 4 ends. - The pane is
$HERDR_PANE_ID. Same rule as the other two skills: never resolve the lead's own pane or tab fromfocused_tab_idor "the focused pane inherdr pane list". The user switches tabs while the run is going. - The full gate, or nothing. Per-task green does not count; the lead runs
$GATEwhole before opening the PR, and narrowing it to make it pass is not a permitted recovery. - The PR goes through a forge skill, CLI or MCP server — never
curl. TheoriginURL and the environment carry tokens.
Unlike the pair, /deliver does run bd state transitions: it claims the
parent after the plan is agreed (never before), claims each child right before
spawning its task session, and closes the children and the parent it planned.
That is deliberate — it is the component that decided the work was done. If you
would rather keep closing manual, drop Step 7's bd close lines; nothing else
depends on them.
Host requirements
The smooth path assumes an agent host with tracked worktree entry — in Claude
Code, EnterWorktree / ExitWorktree delegating to worktrunk through
WorktreeCreate / WorktreeRemove hooks in .claude/settings.json. Both
hooks receive their payload as JSON on stdin:
{
"hooks": {
"WorktreeCreate": [
{ "hooks": [{ "type": "command",
"command": "bash -c 'name=$(jq -er .name) || exit 1; wt switch --create \"$name\" --no-cd --format=json | jq -er .path'" }] }
],
"WorktreeRemove": [
{ "hooks": [{ "type": "command",
"command": "bash -c 'p=$(jq -er .worktree_path) || exit 1; wt remove --foreground \"$p\"'" }] }
]
}
}
Without WorktreeCreate, EnterWorktree builds a plain git worktree under
.claude/worktrees/ that wt list does not know about — which breaks
/done-with's step 1 lookup. Wire both hooks, or neither.
On a host without those tools, both skills still work through the wt CLI
alone: wt switch --create <branch> --no-cd --format=json then cd into the
reported path, and wt remove <branch> --foreground on the way out. What is
lost is the automatic cleanup of an untouched worktree at session end.
The session rename is deferred
A session cannot rename itself mid-turn. Claude Code accepts a sessionTitle
only from the UserPromptSubmit and SessionStart hooks; /rename is a
builtin the human types, not something a skill can invoke, and the live session
registry under ~/.claude/sessions/*.json is owned by the running process — a
write there is overwritten, so do not go near it.
So scripts/session-title.sh queue <label> writes the label to
${CLAUDE_CONFIG_DIR:-~/.claude}/session-title-queue/$CLAUDE_CODE_SESSION_ID,
and scripts/session-title.sh hook, wired as a UserPromptSubmit hook, emits
{"hookSpecificOutput": {"hookEventName": "UserPromptSubmit", "sessionTitle": "<label>"}}
on the user's next message and deletes the file. Consequences to keep in mind:
- One message of latency. The tab flips immediately, the session name on the next prompt. Say so in the report rather than claiming both are done.
- Hook not installed → nothing happens. The file is dropped after seven days. Never treat this as fatal, and never fall back to editing the session registry.
- Hook mode always exits 0. A
UserPromptSubmithook that fails would block the user's prompt; a rename is never worth that.
Extension points
Everything below is a small, local edit inside skills/*/SKILL.md.
Tab naming. work-on and done-with shell out to their own copy of
scripts/herdr-rename-tab.sh <label> and then scripts/session-title.sh queue <label>; /work-on passes the raw bead id, /done-with the literal free.
To prefix or shorten labels, change the argument in all four call sites —
keeping them distinguishable is the only requirement, and tab and session must
always receive the same label.
The script resolves the target tab from $HERDR_PANE_ID, the pane id herdr
exports into every pane it spawns, then herdr pane get <pane> → .tab_id. Do
not go back to herdr api snapshot's focused_tab_id: it names whichever
tab is in front when the call lands, so a user who switches tabs mid-skill gets
an unrelated tab renamed. The two copies of each script are identical by design —
skills install one directory at a time, so a shared helper outside
skills/<name>/ would not ship. Patch both, or neither. The
UserPromptSubmit hook may point at either copy of session-title.sh.
Worktree layout. Owned entirely by worktrunk's configuration
(.config/wt.toml in the repo, ~/.config/worktrunk/config.toml for the
user), not by these skills. Change the layout there and both skills follow.
Branch naming. The bead id is the branch name, in both directions —
/done-with finds the worktree by matching .branch against it. If you
derive branch names differently (bead/<id>, say), change the argument passed
to wt-switch-create in /work-on and the branch resolution in step 0 of
/done-with.
Bead state transitions. None are performed today: /work-on only reads
(bd show), /done-with only reminds. To make the pair own bead state, add
bd update <id> --claim after /work-on's validity gate and bd close <id>
after /done-with's cleanup. Do the claim after the gate, never before — a
bogus id must not mutate anything.
The safety bar. /done-with's "merged or PR open" rule is the one thing
standing between an unmerged branch and a removed worktree. Loosening it (for
example, accepting "pushed to a remote branch" as sufficient) is a real
decision, not a tweak — say so in the report if you do.
Conventions in this repo
- Skills live at
skills/<name>/SKILL.md, the layout theskillsCLI expects. - Nothing here may hardcode a bead prefix, a repository name, a forge, a
package manager, a test command, or a host — bead ids arrive as
$ARGUMENTSand are passed through unchanged, and everything else is discovered at run time or asked for. This is the rule most easily broken by copying a skill in from a working project; check for it before committing one. - Prerequisites belong in the frontmatter
compatibility:field and in the README table.
