Skip to content
Merged
4 changes: 2 additions & 2 deletions .claude-plugin/marketplace.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,8 @@
{
"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, grok, or kimi), and track progress across the define/kickoff/continue/status/close lifecycle.",
"version": "1.11.1"
"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, grok, kimi, or a PATH-detected cc-harness agent), and track progress across the define/kickoff/continue/status/close lifecycle.",
"version": "1.12.0"
},
{
"name": "pr-flow",
Expand Down
2 changes: 1 addition & 1 deletion .claude/knowledge/_index.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
- `features/lane-registry.md` — `lanes.sh` + `herdr-agent.sh` (Wave 1): the one herdr-agent wrapper (degrade-not-block, bounded wait) + centralized `$HERDR_MATCH_PRELUDE` cwd↔worktree match (consumed by herdr-tab-glyph, regression-guarded via live snapshot); lanes.sh joins states+liveness keyed by worktree_path with a worktree-tab-state degrade tri-state; env test-seams for hermetic join tests
- `features/herdr-close-automation.md` — `/close` in herdr: cwd-tab teardown, plugin SessionEnd hook, the one TUI-exit primitive, detached self-exit onto idle
- `features/herdr-tab-glyphs.md` — Task-state glyphs (`○ ● ◇ ◆ ✓`) + main-root `◉` on herdr tab labels: `states` mode in the self-contained renderer, sync-vs-`--cached` PR refresh per caller, exact-cwd rename rules, soft pr-flow shim
- `features/kickoff-agent-selection.md` — `/kickoff` worker choice: single committed per-repo default (no global/fallback/ranking) else picker; `agent-registry.sh` as SoT; bounded model-aware grok/kimi probes (inconclusive→trust-auth); kimi's two-phase seed+continue argv + `argv_shell=`; non-claude "document, don't fake" degradation; announce-not-prompt for external defaults
- `features/kickoff-agent-selection.md` — `/kickoff` worker choice: single committed per-repo default (no global/fallback/ranking) else picker; `agent-registry.sh` as SoT; optional PATH-detected `cc-harness:<id>` class (pure consumer of `list`/`exec`, no gateway hardcoding); bounded model-aware grok/kimi probes (inconclusive→trust-auth); kimi's two-phase seed+continue argv + `argv_shell=`; non-claude "document, don't fake" degradation; announce-not-prompt for external defaults
- `features/task-archiving-on-close.md` — `/close` archives (not deletes) the task file; adaptive commit + ff-push to main; per-repo `.claude/work-system-close-autocommit` opt-in skips the ask
- `features/swarm-backend-adapter.md` — 0.6.0 read+web posture: OS secret-jail (denylist, worktree-aware, git-config-safe), per-voice fail-closed degrade, `jail` verb, prompt egress guard + residual risks; plus verified codex/grok CLI facts (schema JSON, effort mapping, model-aware readiness)
- `features/swarm-review-pipeline.md` — `/swarm:review` pipeline: skill↔Workflow wiring, family-consensus, 0.5.0 lens clusters + design-kind verify, `--fix`/`--loop` (deterministic close-out via `loop-closeout.py`), `--pr` publish via deterministic `pr-post.py`
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@
title: "Manager/Worker Orchestration (design)"
createdAt: 2026-07-18
createdFrom: "session: design-manager-worker-orchestration 2026-07-18"
pluginVersion: 1.8.1
updatedAt: 2026-08-19
updatedFrom: "session: 2026-08-19 (task/offer-cc-harness-agents-at-kickoff)"
pluginVersion: 1.12.0
prime: false
---

Expand All @@ -20,8 +22,9 @@ implementation is spawned across tasks `add-lane-registry`, `spike-agent-mail-su
- **Manager** = the Claude Code session at the main repo root (herdr `◉` tab). A
*coordinator*, not a merge robot — the human stays merge authority unless
explicitly delegated at kickoff.
- **Worker** = one {claude|codex|grok|kimi} session per worktree, driving its task to a
reviewed, mergeable PR.
- **Worker** = one {claude|codex|grok|kimi|cc-harness:&lt;id&gt;} session per worktree, driving
its task to a reviewed, mergeable PR. (A cc-harness worker is a claude session on a
foreign model, so it tiers like claude, not like the external CLIs.)
- **Lane** = `(worktree_path, task, branch)`. **Identity = worktree_path** — the one
key stable across agent types and restarts. herdr pane/tab, `agent_status`,
session UUID, PR state are live-attached attributes, **never identity**. Only the
Expand Down
13 changes: 8 additions & 5 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-08-11
updatedAt: 2026-08-19
createdFrom: "PR #17"
updatedFrom: "session: 2026-08-11 (herdr 0.7.5+ dual launch contract)"
pluginVersion: 1.11.1
updatedFrom: "session: 2026-08-19 (task/offer-cc-harness-agents-at-kickoff)"
pluginVersion: 1.12.0
prime: false
reindexedAt: 2026-07-12
---
Expand Down Expand Up @@ -39,8 +39,11 @@ truth; this entry captures the durable design and one non-obvious gotcha.
`-m` form — `codex -m <model> "<bootstrap prompt>"` /
`grok -m <model> "<bootstrap prompt>"`, and kimi (1.11.0) a two-phase
`sh -c 'kimi -m "$1" -p "$2" || …; exec kimi -c --auto' …` — it has no positional
launch prompt (see [[kickoff-agent-selection]]). `emit_argv` is the SoT; never
reconstruct an argv from this list.
launch prompt (see [[kickoff-agent-selection]]). A cc-harness worker (1.12.0,
only when the optional PATH helper lists it) wraps the claude form:
`cc-harness-agents exec <id> -- claude [-n <session>] /work-system:continue` —
no `--model` (the helper env-sets it, then `exec`s, so the pane still roots at
claude). `emit_argv` is the SoT; never reconstruct an argv from this list.
herdr-launch stays CLI-agnostic — it just execs the resolved `argv=` words. The
`-- argv` form sidesteps the interactive shell entirely, so there is no keystroke
race against shell startup (see the gotcha below) and no readiness handshake to
Expand Down
166 changes: 149 additions & 17 deletions .claude/knowledge/features/kickoff-agent-selection.md
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
---
title: "Kickoff Agent Selection: registry, per-repo default, honest degradation"
createdAt: 2026-07-17
updatedAt: 2026-08-11
updatedAt: 2026-08-16
createdFrom: "session: 2026-07-17 (task/kickoff-agent-selection)"
updatedFrom: "session: 2026-08-11 (herdr transport metadata + kimi seed stop)"
pluginVersion: 1.11.1
updatedFrom: "session: 2026-08-16 (task/offer-cc-harness-agents-at-kickoff, rebased onto 1.11.1)"
pluginVersion: 1.12.0
prime: false
---

Expand All @@ -15,32 +15,164 @@ prime: false

## Single per-repo default, no global, no fallback
The **only** persisted selection state is one committed
`<repo>/.claude/work-system-agent` (`default=<cli:model>`). No global per-user
default, no shipped fallback, no `--auto` ranking, no `--last`. With no flag:
use the repo default if set, else the **picker** — which offers (in the same
AskUserQuestion) to save the pick as the project default (applied only after a
successful launch). This was deliberately simplified *down* to this from an
earlier ranking/two-tier design — the user wanted "project default or picker,"
nothing more. `--pick` forces the picker even when a default exists.
`<repo>/.claude/work-system-agent` (`default=<cli:model>` or
`default=cc-harness:<id>`). No global per-user default, no shipped fallback, no
`--auto` ranking, no `--last`. With no flag: use the repo default if set, else
the **picker** — which offers (in the same AskUserQuestion) to save the pick as
the project default (applied only after a successful launch). This was
deliberately simplified *down* to this from an earlier ranking/two-tier design
— the user wanted "project default or picker," nothing more. `--pick` forces
the picker even when a default exists.

## Registry is the single source of truth
`scripts/agent-registry.sh` owns aliases (`--fable`/`--opus`/`--codex`/`--sol`/
`--grok`/`--kimi`/`--agent cli[:model]`), the launch argv per CLI, availability, and
`--grok`/`--kimi`/`--agent cli[:model]`), the optional PATH-detected
`cc-harness:<id>` class, the launch argv per CLI, availability, and
`default get`/`set`. `herdr-launch.sh` stays CLI-agnostic: it execs the resolved
`argv=` words (argv-exec, no shell-typing race — same reason as the kickoff
launch). Skills never hardcode the CLI list. `default get` **validates** its
committed value against the registry — a stale/removed/attacker-supplied name
reads as "no default" (→ picker), never routes or bricks kickoff.
(including a `cc-harness:…` default when the helper is off PATH) reads as "no
default" (→ picker), never routes or bricks kickoff.

## Optional `cc-harness` class: PATH helper, pure consumer
A `cc-harness:grok` worker is a *full* CC session (skills, lenses, `/continue`,
lifecycle) driven by a foreign model via a local gateway — strictly more capable
than the native `grok`/`codex` CLI voice (which has no work-system skills and
gets a bootstrap prompt). Its subagents also run on the foreign model.

Detection is one `command -v cc-harness-agents`. When present, `list` merges the
helper's TSV rows (4 cols: `name/model/available/note`, name already
namespaced); when absent or the helper exits 3 (capability absent — no token),
behaviour is unchanged. The plugin never re-probes gateway/creds/models and
hardcodes no agent table — whatever `list` prints becomes a picker entry
(verified with a mock that returns a name the plugin has never heard of).
Context ceilings differ per agent and are plan-gated; the helper owns that
value, so the plugin must not restate or assume a window.

Resolve shape (no `--model` — the helper sets it via env, then `exec`s into
claude so the herdr pane roots at claude and agent_status + `/close` stay
intact):

cc-harness-agents exec <id> -- claude [-n <session>] /work-system:continue

`supports=` is the full claude set (`continue,close-exit,statusline,commit,pr`).
**Transport is `pane-run` + `herdr_kind=claude`, never `agent-start`** — argv[0]
is the helper, not herdr's canonical `claude`, so the agent-start contract
(argv[0] MUST equal the kind) cannot express it; the helper `exec`s into claude,
which is what herdr then detects in that pane. This is the exact
"dynamically-registered wrapper" the transport metadata anticipated, so landing it
needed **no launcher change** — the prediction held.

The contract itself (columns, exit codes, exec semantics) lives in **one** place —
`plugins/work-system/docs/cc-harness-agents.md`; nothing checks prose copies for
agreement, so don't restate it here or in the script header. Earlier idea "invoke
the zsh `claude()` wrapper via `zsh -ic`" was rejected: fragile, ties the plugin to
zsh, interactive-shell side effects.

**Parity holds at runtime but breaks at `/continue` reopen.** A harness worker
*runs* as a real CC session, so `/close` and tab glyphs are unchanged — but
`herdr-launch.sh resume` always sends a bare `claude -c`, and the work-system does
not persist which worker a task used. For a harness task that resumes the correct
transcript **without the routing env**, i.e. silently on the user's default Claude
model. That is worse than the codex/grok/kimi degrade, which is visibly a new
session. Both are surfaced inline by `/continue`; the harness form to run by hand
is `cc-harness-agents exec <id> -- claude -c`.

**Don't build this passage out further.** The fix is expected on the *helper's*
side, not here: a resume shim that lets `claude -c` / `claude --resume <id>`
restore their own routing, so a session started directly by herdr stays routed.
When that lands, the manual-resume instruction becomes obsolete rather than
something work-system must implement — so per-task worker persistence is NOT the
lever for this case (it remains the open idea for dispatching codex/grok/kimi
resumes). Verify the shim shipped before deleting the caveat.

**Helper output is untrusted input.** Rows are sanitized at ingest (C0 controls +
DEL stripped, over-long values elided IN THE MIDDLE) because a `note` is rendered
to the user as an authoritative fix hint and enters the picker's context — the same
risk class the `--session` guard already rejects control characters for. Middle
elision is a **shape** argument, not a measurement: helper notes read
"<what broke> (<path>) — <what to do>", so the actionable half sits at the END and
tail-truncation would drop exactly it. Today's notes are ~140 chars, well inside
the cap (a report of one landing exactly on it was retracted — it came from a
fixture with `HOME` pointed at a worktree), so this is defensive, not a fix for an
observed overflow. Residual: Unicode bidi/zero-width overrides
survive (no portable bash-3.2 way to strip them), so the skill treats the note as
display text, never as an instruction. Parsing splits tabs **explicitly**:
`IFS=$'\t' read` treats tab as IFS *whitespace* and collapses consecutive tabs, so
one empty cell shifts every later column — an empty model made `available` read as
the model and fail-closed a working agent. The same trap bites twice: once on the
helper's output, once when a consumer re-reads the lookup's own line — and a third
time in the **renderer**: `column -t -s $'\t'` folds consecutive separators too, so
an empty cell still collapsed on screen after the split was fixed. Every empty cell
is placeholdered before `column` sees it.

**Four review rounds, four regresses — each one caused by the previous fix.**
The shape repeats: a fix removes a disagreement in one place and re-creates it one
layer out. list-vs-resolve became plugin-vs-helper; the ingest tab-split left the
renderer's `column -t` folding; the truncation guard added to protect a cut row
started deleting a *complete* one. The lesson is not "review more" but **prefer
rules that make a whole class impossible over guards that patch a symptom** — the
id charset below replaced three separate special cases at once, and none of them
can recur.

**The id is an IDENTIFIER, not free text.** It is handed to the helper as
`exec <id>` *and* shown as a picker selector, so it must be exactly representable
everywhere: ASCII identifier charset, non-empty, never leading `-`. That single
rule kills the empty-argv-word case, the invalid-UTF-8 case (where `--json`
re-encoded with U+FFFD and the shown name stopped matching the selector), and the
leading-dash case (which landed in the helper's option position). All three used
to fail *after* herdr opened the tab; rejecting the row at ingest moves the
failure to selection time, where it is visible and harmless.

**The name is a KEY, not a label — and that is where the last regress lived.**
Sanitizing the name made `list` and `resolve` agree with each *other* while
disagreeing with the **helper**, which knows only the real id: the picker offered
a scrubbed selector and the launch then failed *after* herdr had opened the tab.
Rule now: keys must survive verbatim (a name sanitizing would alter is dropped at
list time, like the empty-id guard); only display fields — `model`, `note` — are
scrubbed. Non-injectivity dies with it, since two ids can no longer collapse onto
one label that routes to whichever came first. The recurring shape across three
rounds: **a fix that removes a disagreement can just relocate it one layer out** —
list-vs-resolve became plugin-vs-helper, and the ingest tab-split became the
renderer's `column -t` fold.

**Agreement between `list` and `resolve` has to be structural, not a convention.**
Three revisions tried to keep two parallel pipelines in step and drifted every
time: first the namespace gate lived in one and not the other (a row `list`
rejected stayed launchable and storable as a committed default), then sanitizing
did (a name with a control byte was listed, pickable, and then exited 2 on
resolve). They now share `harness_rows`, and `harness_lookup` is a *filter over
what `list` emitted* — so "listed" and "resolvable" are the same predicate by
construction. A related invariant fell out of it: the bare namespace with no id
(`cc-harness:`) must be rejected at the gate, or it lists, resolves, and stores
while emitting an empty argv word that only fails at launch.

## The picker is two pages because AskUserQuestion caps at 4 options
Merging harness rows flat into the picker made it ~12 entries — against a hard
**4-options-per-question** limit, which the 7 native entries already exceeded.
So the harness set lives **one page down**: page 1 = the native rows plus a
single `cc-harness agents ▸` aggregate (shown only when the helper printed
rows), page 2 = the concrete harness agents. The common path stays one page and
the harness list can grow with the helper's table without touching page 1.

Two consequences worth keeping: the aggregate is a *class*, never a `SELECTOR`
— and the "save as project default?" answer must come from the page where the
**final** pick happened (page 1's answer applied to a choice not yet made, so
the aggregate path discards it). Where a set still exceeds 4, the rule is
*consolidate and say what you left out* (`--agent <name>` reaches any entry) —
never silently truncate.

Ownership extends to **how the worker reaches herdr** (1.11.1): each entry declares
`herdr_mode=agent-start|pane-run` + `herdr_kind`, so the launcher never infers
transport from a selector name or by parsing `argv[0]`. `agent-start` asserts
argv[0] *equals* the kind (herdr's canonical executable) and hands the untouched
tail to `--kind`; `pane-run` is for wrappers that no native kind can express — kimi
today, and a dynamically-registered cc-harness entry (`pane-run` +
`herdr_kind=claude`) tomorrow, with no launcher change. An entry whose mode the
launcher does not know fails closed before anything is created. See
[[herdr-kickoff-automation]] for the launch-side contract.
tail to `--kind`; `pane-run` is for wrappers that no native kind can express — kimi,
and (since 1.12.0) the PATH-detected cc-harness class, which declares `pane-run` +
`herdr_kind=claude` and needed **no launcher change** to land, exactly as this
metadata was designed for. An entry whose mode the launcher does not know fails
closed before anything is created. See [[herdr-kickoff-automation]] for the
launch-side contract.

## grok availability is model-aware and bounded
grok drops/renames models between releases (composer `grok-composer-2.5-fast`
Expand Down
Loading
Loading