Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .claude-plugin/marketplace.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
"name": "work-system",
"source": "./plugins/work-system",
"description": "Generic task and worktree workflow system for Claude Code. Manage tasks as markdown files, run them in isolated git worktrees with a choice of worker agent (Claude, codex, or grok), and track progress across the define/kickoff/continue/status/close lifecycle.",
"version": "1.9.1"
"version": "1.9.2"
},
{
"name": "pr-flow",
Expand Down
32 changes: 28 additions & 4 deletions .claude/knowledge/features/herdr-kickoff-automation.md
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
---
title: "herdr /kickoff + /continue-reopen Automation"
createdAt: 2026-06-24
updatedAt: 2026-07-19
updatedAt: 2026-07-21
createdFrom: "PR #17"
updatedFrom: "session: 2026-07-19 (surface herdr launch errors)"
pluginVersion: 1.9.1
updatedFrom: "session: 2026-07-21 (fully-qualify kickoff's /continue)"
pluginVersion: 1.9.2
prime: false
reindexedAt: 2026-07-12
---
Expand All @@ -31,7 +31,8 @@ truth; this entry captures the durable design and one non-obvious gotcha.
`herdr agent start "<label>" … -- <worker argv>`, which execs the worker binary
directly. As of work-system 1.9.0 the worker argv is **resolved from the chosen
agent** by `agent-registry.sh` (`emit_argv`), not hardcoded: a claude worker is
`claude --model <m> -n "<label>" "/continue"`, while codex/grok get their own
`claude --model <m> -n "<label>" "/work-system:continue"` (plugin-qualified — see
the shadowing gotcha below), while codex/grok get their own
`-m` form — `codex -m <model> "<bootstrap prompt>"` /
`grok -m <model> "<bootstrap prompt>"` (see [[kickoff-agent-selection]]).
herdr-launch stays CLI-agnostic — it just execs the resolved `argv=` words. The
Expand Down Expand Up @@ -213,6 +214,29 @@ by-hand tab close where a kickoff tab would auto-close. This is the same
agent-detection question flagged as unverified for the deferred race-free-prevention
option above; both wait on live herdr verification.

## Gotcha: launch the worker with `/work-system:continue`, never bare `/continue`

The claude worker's initial prompt is the **plugin-qualified** `/work-system:continue`,
not the bare `/continue` (work-system 1.9.2 fix). Bare `/continue` is not a safe way to
reach a plugin skill: a Claude Code built-in/alias `/continue` can shadow it, and — per
CC's own docs — plugin skills live under a `plugin-name:skill-name` namespace and are
only *guaranteed* reachable via that qualified form; the bare name resolves to a skill
only when nothing at a higher precedence claims it. In a fresh worktree session
(`claude -n <name>`, no prior conversation) a shadowed bare `/continue` either runs CC's
own resume (nothing to resume → worker sits idle, TASK.md never loaded) or errors as an
unknown command — either way the work-system resume flow never runs, breaking the core
kickoff→worker handoff. The exact mechanism (a built-in vs namespace-only resolution)
is CC-version-dependent and was reported intermittently; the fix is orthogonal to which
it is, because `/work-system:continue` is the one documented, unshadowable invocation.
**Do not "simplify" it back to the bare form.**

Sites carrying the qualified form (all machine-generated invocations): `agent-registry.sh`
`emit_argv` (the primary selector path), `herdr-launch.sh`'s legacy no-selector fallback,
and kickoff's outside-herdr manual block; `test_agent_registry.py` asserts the argv ends
in `/work-system:continue`. **Deployment caveat:** `agent-registry.sh` / `herdr-launch.sh`
run from the *plugin cache*, not the repo, so a launch only picks up this fix after a
`/plugin` marketplace update + reload refreshes the cache.

## Gotcha: input into a fresh pane races shell startup

This is *why* the launch uses argv-exec, not `herdr pane run` / typed keystrokes.
Expand Down
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,9 @@ entries are grouped per plugin, newest first.

## work-system

### 1.9.2 — 2026-07-21
- Fix: `/kickoff`-launched Claude workers no longer run the wrong `/continue`. The machine-generated launch prompt is now the plugin-qualified `/work-system:continue`, not the bare `/continue` — a Claude Code built-in/alias `/continue` can shadow the plugin skill (and plugin skills are only *guaranteed* reachable via their `plugin:skill` namespace), so a freshly-launched worker was running CC's own resume (nothing to resume in a new worktree → idle, TASK.md never loaded) instead of the work-system resume flow. Changed at all machine-invocation sites: `agent-registry.sh` `emit_argv` (the selector path), `herdr-launch.sh`'s legacy no-selector fallback, kickoff's outside-herdr manual block, and the README launch examples; `test_agent_registry.py` asserts the qualified form. Human-typed reopen instructions (`/work-system:continue <task>`) updated to match. **Deployment note:** `agent-registry.sh` / `herdr-launch.sh` run from the *plugin cache*, not the repo, so live launches only pick up this fix after a `/plugin` marketplace update + reload refreshes the cache.

### 1.9.1 — 2026-07-20
- `herdr-launch.sh` no longer swallows herdr's own error on a launch/resume failure (`2>/dev/null`). The `agent start`, `pane move`, `tab create`, and `pane run` calls (plus the `tab close` orphan-cleanup that can run after a malformed `tab create`) now capture stderr, strip control/escape bytes both before AND after JSON-decoding a message (a ``-style escape only becomes a real control byte once decoded), parse herdr's `{"error":{"code","message"}}` JSON defensively (raw text as fallback, never a traceback), and print it before the existing generic diagnostic — `pane run`'s failure line now follows the same before-the-generic-message order as the other call sites. When the error names an invalid workspace placement (`agent_placement_not_found`, or the message names the sanitized workspace id as a bounded token within the same clause as "not found"/"placement", case-insensitively), a one-line actionable hint is appended pointing at a stale `HERDR_WORKSPACE_ID` — scoped to the two calls that actually send `--workspace` (`agent start`, `tab create`), since `pane move`/`pane run` can't have a workspace-id problem. `kickoff`/`continue` SKILL.md now explicitly instruct relaying the helper's stderr diagnostic to the user on every failure path, so the richer diagnostic actually reaches the user instead of staying in the tool output — worded per call site, since the stale-workspace hint can only ever appear on the `agent start`/`tab create` paths. Diagnostics only — stdout key=value lines, exit codes, and `moved`/`reused`/`resumed`/`focused` semantics are unchanged. Covered by new `test_herdr_launch.py`.

Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ Lightweight, native knowledge management for Claude Code projects. Three layers

### Work System

Generic task and worktree workflow system. Manage tasks as markdown files, work in isolated git worktrees, and track progress through the full lifecycle. `/kickoff` runs the repo's default worker agent (Claude, codex, or grok — a single committed per-project default), or, when none is set, shows a picker and offers to save your choice; override per run with flags like `--opus`/`--sol` or `--pick`. Inside a [herdr](plugins/work-system/README.md#herdr-integration) session it auto-opens a tab (named after the task, shortened for the sidebar and prefixed with the task's state glyph — `●` active, `◇` in review, `◆` approved, `✓` merged) with the worktree as cwd, starts the chosen worker, and — for a Claude worker — runs `/continue` for you; `/continue <task>` from the main session reopens that tab and resumes it if a stray `/exit` closed it; and `/close` tears the tab down again when the task is merged.
Generic task and worktree workflow system. Manage tasks as markdown files, work in isolated git worktrees, and track progress through the full lifecycle. `/kickoff` runs the repo's default worker agent (Claude, codex, or grok — a single committed per-project default), or, when none is set, shows a picker and offers to save your choice; override per run with flags like `--opus`/`--sol` or `--pick`. Inside a [herdr](plugins/work-system/README.md#herdr-integration) session it auto-opens a tab (named after the task, shortened for the sidebar and prefixed with the task's state glyph — `●` active, `◇` in review, `◆` approved, `✓` merged) with the worktree as cwd, starts the chosen worker, and — for a Claude worker — runs `/work-system:continue` for you (plugin-qualified, since a Claude Code built-in `/continue` shadows the bare skill); `/work-system:continue <task>` from the main session reopens that tab and resumes it if a stray `/exit` closed it; and `/close` tears the tab down again when the task is merged.

**Commands:** `/define`, `/kickoff`, `/adopt`, `/continue`, `/status`, `/close`, `/list`, `/statusline`

Expand Down
2 changes: 1 addition & 1 deletion plugins/work-system/.claude-plugin/plugin.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "work-system",
"description": "Generic task and worktree workflow system for Claude Code. Manage tasks as markdown files, run them in isolated git worktrees with a choice of worker agent (Claude, codex, or grok), and track progress across the define/kickoff/continue/status/close lifecycle.",
"version": "1.9.1",
"version": "1.9.2",
"author": {
"name": "gering"
},
Expand Down
17 changes: 11 additions & 6 deletions plugins/work-system/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -117,11 +117,13 @@ See [Worker agent selection](#worker-agent-selection) below for the full set.
### 3. Continue in the worktree

Open a new Claude session in the worktree — `-n` names the session, and the
`/continue` initial prompt loads the task context in one step:
`/work-system:continue` initial prompt loads the task context in one step (use
the plugin-qualified form: a Claude Code built-in `/continue` shadows the bare
skill name):

```
cd .claude/worktrees/add-dark-mode
claude -n "add-dark-mode" "/continue"
claude -n "add-dark-mode" "/work-system:continue"
```

Loads the task context, checks dependencies, and shows current progress.
Expand Down Expand Up @@ -203,10 +205,11 @@ worktree as its cwd, and starts the task there for you:
`skills/kickoff/SKILL.md` step 13 for the exact rule), so the sidebar shows one
clear entry per task instead of a wall of identical agents. The same short label
names the herdr agent (and, for a claude worker, the `-n` session); the
underlying `task/<name>` branch is unchanged, so `/continue` still resolves the task.
underlying `task/<name>` branch is unchanged, so the resume flow still resolves
the task.
- The chosen worker is launched directly as argv (`herdr agent start … --
<resolved worker command>`), so the real CLI process is what herdr's agent-state
detection sees. A claude worker gets `claude --model <m> -n "<label>" "/continue"`
detection sees. A claude worker gets `claude --model <m> -n "<label>" "/work-system:continue"`
and loads the task context automatically; a codex/grok worker gets a bootstrap
prompt (read `TASK.md`, drive the task to a PR) since they have no work-system skills.
- The new tab opens in the background (`--no-focus`), so your kickoff session
Expand Down Expand Up @@ -240,12 +243,14 @@ marker file (under `$HOME/.cache`), so it never fires on an ordinary session exi
All of this lives in the tested `scripts/herdr-teardown.sh`; see
`skills/close/SKILL.md` step 12 for the flow.

### `/continue <task>` reopens a closed task tab
### `/work-system:continue <task>` reopens a closed task tab

A kickoff tab runs Claude as its **root pane**, so a bare `/exit` (even just to
restart Claude Code) ends that pane and herdr closes the whole tab — the worktree
and the resumable session survive, but you lose your place. Run
`/continue <task>` **from the main session** to get it back: it reopens a herdr tab
`/work-system:continue <task>` **from the main session** to get it back (use the
plugin-qualified form — a Claude Code built-in `/continue` shadows the bare skill
name): it reopens a herdr tab
at the task's worktree and resumes the existing session with `claude -c` (the
most-recent session for that cwd — since each worktree hosts exactly one task, the
cwd identifies it unambiguously), then focuses the tab.
Expand Down
9 changes: 6 additions & 3 deletions plugins/work-system/scripts/agent-registry.sh
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,9 @@
#
# Launch shape per CLI (resolve builds the argv; the launch helper just execs
# the `argv=` words, so the argv-exec path — no shell-typing race — is kept):
# claude -> claude --model <model> [-n <session>] /continue
# (the work-system /continue skill resumes TASK.md deterministically)
# claude -> claude --model <model> [-n <session>] /work-system:continue
# (plugin-qualified so a CC built-in /continue can't shadow the skill;
# the work-system continue skill resumes TASK.md deterministically)
# codex -> codex -m <model> <bootstrap-prompt>
# grok -> grok -m <model> <bootstrap-prompt>
# The bootstrap prompt (codex/grok have no work-system skills) tells the agent
Expand Down Expand Up @@ -265,7 +266,9 @@ emit_argv() {
claude)
printf 'argv=%s\n' claude --model "$model"
[ -n "$session" ] && printf 'argv=%s\n' -n "$session"
printf 'argv=%s\n' /continue
# Plugin-qualified: a CC built-in/alias `/continue` shadows the skill, so
# the bare form would run CC's own resume, not the work-system flow.
printf 'argv=%s\n' /work-system:continue
;;
codex)
printf 'argv=%s\n' codex -m "$model" "$BOOTSTRAP_PROMPT"
Expand Down
5 changes: 3 additions & 2 deletions plugins/work-system/scripts/herdr-launch.sh
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
# can eat the leading keystrokes of a typed command.
# The worker argv is resolved from an agent SELECTOR via
# agent-registry.sh (claude/codex/grok × model); with no
# selector it stays the legacy `claude … /continue`. The
# selector it stays the legacy `claude … /work-system:continue`. The
# registry owns every per-CLI launch detail, so this script
# is CLI-agnostic — it just execs the resolved argv.
#
Expand Down Expand Up @@ -252,7 +252,8 @@ case "$mode" in
agent_name="claude"
note=""
if [ -z "$selector" ]; then
worker_argv=(claude -n "$session" "/continue")
# Plugin-qualified: a CC built-in/alias `/continue` shadows the skill.
worker_argv=(claude -n "$session" "/work-system:continue")
else
registry="${0%/*}/agent-registry.sh"
[ -f "$registry" ] || { echo "agent-registry.sh not found next to herdr-launch.sh" >&2; exit 1; }
Expand Down
6 changes: 3 additions & 3 deletions plugins/work-system/scripts/test_agent_registry.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
or via scripts/check-structure.py's "plugin tests" check.

Guards the registry's contract: alias/name/cli selector resolution, the per-CLI
launch argv shape (claude `/continue` vs codex/grok bootstrap prompt), the
launch argv shape (claude `/work-system:continue` vs codex/grok bootstrap prompt), the
availability probe (codex login status + grok auth file + grok model-list), the
exit-code map (2 unknown selector, 3 resolved-but-unavailable), and the
project-default state (set/get, bogus rejection, no-git-repo error).
Expand Down Expand Up @@ -114,13 +114,13 @@ def kv(out):
check("--fable -> claude:fable", r.get("name") == "claude:fable")
check("--fable cli", r.get("cli") == "claude")
check("claude argv shape",
r["argv"] == ["claude", "--model", "fable", "-n", "close-herdr", "/continue"])
r["argv"] == ["claude", "--model", "fable", "-n", "close-herdr", "/work-system:continue"])
check("claude supports lifecycle", "continue" in r.get("supports", ""))

r = kv(e.run("resolve", "--opus").stdout)
check("--opus -> claude:opus", r.get("name") == "claude:opus")
check("claude argv without session omits -n",
r["argv"] == ["claude", "--model", "opus", "/continue"])
r["argv"] == ["claude", "--model", "opus", "/work-system:continue"])

r = kv(e.run("resolve", "--sol").stdout)
check("--sol -> codex:gpt-5.6-sol", r.get("name") == "codex:gpt-5.6-sol")
Expand Down
47 changes: 44 additions & 3 deletions plugins/work-system/scripts/test_herdr_launch.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,8 +36,18 @@ def shquote(s):


def herdr_stub(cases):
"""Build a fake `herdr` script from {"<argv1> <argv2>": (stdout, stderr, exit)}."""
lines = ["#!/usr/bin/env bash", 'case "$1 $2" in']
"""Build a fake `herdr` script from {"<argv1> <argv2>": (stdout, stderr, exit)}.

When $HERDR_ARGV_LOG is set, every call appends a `=== <subcmd>` header line
followed by each received arg on its own line — so a test can assert the exact
argv herdr-launch.sh execs (e.g. the worker argv after `agent start … --`)."""
lines = [
"#!/usr/bin/env bash",
'if [ -n "${HERDR_ARGV_LOG:-}" ]; then',
' { printf \'=== %s\\n\' "$1 $2"; printf \'%s\\n\' "$@"; } >> "$HERDR_ARGV_LOG"',
"fi",
'case "$1 $2" in',
]
for key, (out, err, rc) in cases.items():
lines.append(f' "{key}")')
if out:
Expand All @@ -55,7 +65,7 @@ class Env:
"""A throwaway PATH with a fake `herdr` stub, plus a worktree dir for
herdr-launch.sh to target."""

def __init__(self, cases):
def __init__(self, cases, log_argv=False):
self.tmp = tempfile.TemporaryDirectory()
root = Path(self.tmp.name)
self.worktree = root / "wt"
Expand All @@ -67,6 +77,15 @@ def __init__(self, cases):
herdr.chmod(0o755)
self.env = dict(os.environ)
self.env["PATH"] = f"{bindir}:{self.env['PATH']}"
self.argv_log = root / "argv.log" if log_argv else None
if self.argv_log is not None:
self.env["HERDR_ARGV_LOG"] = str(self.argv_log)

def logged_argv(self):
"""Lines the herdr stub recorded (headers + one arg per line); [] if none."""
if self.argv_log is None or not self.argv_log.exists():
return []
return self.argv_log.read_text().splitlines()

def run(self, *args):
return subprocess.run(
Expand Down Expand Up @@ -163,6 +182,28 @@ def close(self):
"herdr tab create did not return a pane id" in r.stderr)
e.close()

# --- legacy no-selector launch: worker argv is the plugin-qualified skill --- #
# Guards the shadowing fix: an empty selector takes the legacy path, whose worker
# argv MUST be `claude -n <session> /work-system:continue` — the qualified form as
# ONE argv token (a bare `/continue` would be shadowed by a CC built-in/alias).
e = Env({
"agent start": (jsonlib.dumps({"result": {"agent": {"pane_id": "w1:p5"}}}), "", 0),
"pane move": (jsonlib.dumps(
{"result": {"move_result": {"created_tab": {"tab_id": "w1:t9"}}}}), "", 0),
}, log_argv=True)
r = e.run("launch", "t", str(e.worktree), "w1", "", "sess1") # "" selector = legacy path
argv = e.logged_argv()
check("legacy: exit 0", r.returncode == 0)
check("legacy: agent=claude on stdout", "agent=claude\n" in r.stdout)
check("legacy: worker argv carries the qualified skill as one token",
"/work-system:continue" in argv)
check("legacy: bare /continue is NOT emitted", "/continue" not in argv)
if "/work-system:continue" in argv:
i = argv.index("/work-system:continue")
check("legacy: `-n <session>` precedes the skill token",
argv[i - 2:i] == ["-n", "sess1"])
e.close()

# --- success path: stdout contract untouched by any of the above ----------- #
e = Env({
"agent start": (jsonlib.dumps({"result": {"agent": {"pane_id": "w1:p5"}}}), "", 0),
Expand Down
8 changes: 5 additions & 3 deletions plugins/work-system/skills/adopt/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -130,11 +130,13 @@ The Bash tool persists CWD between calls — a bare `cd .claude/worktrees/<task>
session — this session stays in the main repo) and run:

cd .claude/worktrees/<task-name>
claude -n "<task-name>" "/continue"
claude -n "<task-name>" "/work-system:continue"
```
`-n "<task-name>"` names the session (shown in `/resume` and the terminal title);
the `/continue` initial prompt runs the resume flow (load TASK.md, recent commits,
progress) deterministically — both in one launch. Do **not** execute the `cd` yourself
the `/work-system:continue` initial prompt runs the resume flow (load TASK.md, recent
commits, progress) deterministically — both in one launch. Use the plugin-qualified
form: a Claude Code built-in `/continue` shadows the bare skill name. Do **not** execute
the `cd` yourself
— it is for the user's new terminal.

## Remember
Expand Down
Loading
Loading