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, work in isolated git worktrees, and track progress through the full lifecycle.",
"version": "1.8.0"
"version": "1.8.1"
},
{
"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 @@ -12,7 +12,7 @@
- `features/statusline-integration.md` — Status-line segments: plugins can't own `statusLine.command`; marker-block injection; the `[ks …]` + `[ws …]` two-segment coexistence and ws's never-block-on-network PR cache
- `features/herdr-kickoff-automation.md` — herdr `herdr-launch.sh`: `launch` (`/kickoff`) + `resume` (`/continue <task>` reopens an `/exit`-closed tab)
- `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 (`○ ● ◇ ◆ ✓`) on herdr tab names: `states` mode inside the self-contained renderer (no sibling), sync-vs-`--cached` PR refresh per caller, exact-cwd rename rules, soft pr-flow shim
- `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/task-archiving-on-close.md` — `/close` archives (not deletes) the task file; adaptive commit + ff-push to main
- `features/swarm-backend-adapter.md` — Verified codex/grok CLI facts (schema-enforced JSON, effort mapping, stdin hang, model-aware grok readiness) behind `swarm`'s adapter script
- `features/swarm-review-pipeline.md` — `/swarm:review` P2 pipeline (skill↔Workflow wiring, family-consensus, minimal security) + P5 `--fix`/`--loop` (in-session orchestration between workflow runs; deterministic termination + close-out box in `loop-closeout.py`) + `--pr` (review a PR diff, post via `gh` under the user's identity so pr-flow's `claude`-author poll ignores it; publish path is deterministic `pr-post.py`: per-cell sanitizer + stale-head gate + post)
Expand Down
68 changes: 53 additions & 15 deletions .claude/knowledge/features/herdr-tab-glyphs.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,13 @@ prime: false

# herdr Tab State Glyphs

work-system prefixes each task tab's herdr agent name with the task's state
work-system prefixes each task tab's herdr **tab label** with the task's state
glyph (`○` not-started · `●` active · `◇` in review · `◆` approved · `✓`
merged), so the sidebar mirrors the `[ws …]` statusline (e.g. `● close-herdr`,
`◇ ks-label`, `◆ ready-pr`). `◆` (open PR whose `reviewDecision == APPROVED`,
`◇ ks-label`, `◆ ready-pr`). A tab at the **main repo root** gets `◉` — the
Manager hub among the task satellites (`○` was rejected: it already means
not-started). `◉` is stateless and non-exclusive: it marks the *location*, so
every tab at the main root carries it. `◆` (open PR whose `reviewDecision == APPROVED`,
ready to `/merge`) is the one state needing a second `gh` field beyond
`state`: the PR cache carries `headRef\tstate\treviewDecision`, and an old
two-column cache row degrades safely to `◇` (empty field 3 is never `APPROVED`).
Expand All @@ -25,7 +28,7 @@ surfaces source — is **blocked by the renderer's self-containment contract**:
in the same file*: `ws-statusline.sh states <dir>` prints
`<task>\t<state>\t<glyph>` per backlog task from the very `task_state()` /
`glyph_of()` functions the render path aggregates. One file — the surfaces
cannot drift. `herdr-tab-glyph.sh` only *applies* the result to agent names.
cannot drift. `herdr-tab-glyph.sh` only *applies* the result to the tab label.

## Sync vs cache-only PR refresh

Expand Down Expand Up @@ -55,20 +58,55 @@ was shipped and caught in review.

## Renamer rules (herdr-tab-glyph.sh)

- **Trigger points:** stamped at launch (`herdr-launch.sh prefix`, both modes —
the Claude *session name* stays plain; glyphs would clutter `/resume`);
re-stamped by `refresh` on `/status`, `/list`, `/close` (remaining tabs, main-repo
path — `$PWD` may be the just-removed worktree) and pr-flow's `/open`,
`/merge`, `/cycle`, `/check`.
- **Matching:** exact realpath equality `agent.cwd == <main>/.claude/worktrees/<task>`
(same philosophy as `herdr-teardown.sh`): an unrelated agent cd'd into a
worktree subdir is never renamed; agents outside task worktrees are never
touched. Rename targets the `pane_id`; `herdr agent list` exposes `cwd`,
`name`, `pane_id` per agent (verified live 2026-07-16).
- **The glyph lives in the TAB LABEL — one namespace, and it is not the obvious
one.** herdr has two independent names per tab: `tab rename <tab_id> <label>`
and `agent rename <pane_id> <name>` (the agent registry's own field, which
other tooling owns elsewhere). **The sidebar renders the label.** 1.8.0 shipped
`refresh` writing the *agent name* — invisible, so every re-stamp silently did
nothing and only the launch-time label survived (a task tab sat at `●` while
its PR was long in review). Fixed in 1.8.1: `refresh` writes the label, and
`herdr-launch.sh` passes the glyph *only* to `--label`, keeping the agent and
`claude -n` session names plain — those are stable identities, and nothing
refreshes them, so a glyph there would freeze at its launch value.
- **Trigger points:** stamped at launch (`herdr-launch.sh` → `prefix`, both
modes); re-stamped by `refresh` on `/status`, `/list`, `/close` (remaining tabs,
main-repo path — `$PWD` may be the just-removed worktree) and pr-flow's
`/open`, `/merge`, `/cycle`, `/check`.
- **Matching:** exact realpath equality against `<main>/.claude/worktrees/<task>`
(→ state glyph) or the main repo root itself (→ `◉`) — same philosophy as
`herdr-teardown.sh`: an agent cd'd into a *subdir* of either is never renamed,
and anything outside the repo never is. `◉` needs no new trigger — the same
`refresh` sweep stamps both. The match needs **both** herdr lists joined on
`tab_id`: only agents carry `cwd` (the match key), only tabs carry `label`
(what we stamp). One tab is stamped once (first matching agent wins — a
mixed-cwd multi-pane tab would otherwise flip-flop each refresh). The join is
the tab list passed **by file path** (not an argv element): the JSON can be
hundreds of KB and would blow ARG_MAX/E2BIG, a failure `|| true` would mask as
`checked=0`. **Caveat (accepted):** only *agent-backed* tabs are reachable —
`cwd` has no other source than `herdr agent list`, so a main-root tab that is a
bare shell (no agent) can't be matched and won't get `◉`. In practice a Manager
tab is a Claude session, so it is agent-backed; a plain terminal at the root is
not a Manager session.
- **`◉` is stateless — never gated on the backlog.** The main-root mark is a
location mark, so `refresh` does NOT early-return on an empty `states` (no
tasks): it stamps `◉` on main-root tabs regardless, and only the per-task
`glyph_lookup` finds nothing to do. Coupling `◉` to a non-empty backlog was a
latent bug (the early-return predated `◉` and it inherited the gate) that also
contradicted the README/CHANGELOG "every main-root tab carries it" promise.
- **Idempotency:** leading glyphs are stripped before re-prefixing via
byte-exact `case "○ "*` patterns — a bracket expression (`[○●◇✓]`) would
match per *byte* for multibyte chars under C locale. Renames are only issued
when the name actually changes. Everything is best-effort exit-0.
match per *byte* for multibyte chars under C locale. The strip set includes
`◉`, so a tab moving between the hub and a worktree swaps glyphs cleanly.
Renames are only issued when the name actually changes. Everything is
best-effort exit-0.
- **The extractor's TSV must never have an empty middle field.** The consumer
reads it with `IFS=$'\t' read -r …`, and tab is IFS *whitespace* — bash
collapses a run of tabs into ONE delimiter, so an empty field silently
shifts every later field left. Adding the `kind` column bit exactly here: a
main-root row emitted an empty `task`, and the agent's *name* slid into it
(`◉ Manager` → `◉ claude-plugins`). Hence the `key` column carries the task
name **or** the repo dir name — never nothing. A non-whitespace delimiter
(`\x1f`) would also work; non-empty fields were the smaller change.

## pr-flow coupling stays soft

Expand Down
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,11 @@ entries are grouped per plugin, newest first.

## work-system

### 1.8.1 — 2026-07-17
- Mark the main-repo session in the herdr sidebar: a tab sitting exactly at the main repo root is now prefixed with `◉` — the Manager hub among the `○ ● ◇ ◆ ✓` task satellites. Stateless and non-exclusive (it marks the location, so every tab at the root gets it), stamped by the existing `refresh` sweep — no new trigger. The chosen tab label is preserved (prefix only) and `◉` joins the idempotency strip, so hub↔task moves swap glyphs cleanly.
- Fix: state-glyph refreshes never reached the sidebar. herdr keeps two names per tab — the tab label (what the sidebar renders) and the agent registry's own name — and 1.8.0's `refresh` rewrote the latter, so a tab kept its launch-time glyph forever (a task sat at `●` while its PR was in review). `refresh` now rewrites the tab label, joining `herdr agent list` (carries `cwd`) with `herdr tab list` (carries `label`).
- The glyph now lives in the tab label only: `/kickoff` passes the plain label to `herdr agent start` and `claude -n`, so the agent and session names stay stable identities instead of freezing a launch-time glyph. Existing tabs correct themselves on the next refresh; agent names stamped by 1.8.0 are left as-is (rename them yourself if the leftover glyph bothers you).

### 1.8.0 — 2026-07-16
- Mirror task states onto herdr tab names as a leading state glyph (`○ ● ◇ ◆ ✓`: not-started / active / in-review / approved / merged), matching the `[ws …]` statusline. The mapping + precedence stay in `ws-statusline.sh` (new `states` mode, single source of truth); the new `herdr-tab-glyph.sh` stamps the glyph at launch (`/kickoff`, `/continue`) and re-stamps it idempotently on `/status`, `/list`, and `/close` (and via pr-flow's PR-lifecycle skills). `◆` approved is derived from the PR's `reviewDecision`. Survey surfaces (`/status`, `/list`, `/check`, `/close`) read the PR cache without blocking; state-changing skills (`/open`, `/merge`, `/cycle`) do a bounded synchronous refresh.

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, work in isolated git worktrees, and track progress through the full lifecycle: create, start, continue, check, close.",
"version": "1.8.0",
"version": "1.8.1",
"author": {
"name": "gering"
},
Expand Down
16 changes: 13 additions & 3 deletions plugins/work-system/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -221,9 +221,14 @@ both surfaces speak one visual language (e.g. `● close-herdr`, `◇ ks-label`,
- `○` not-started · `●` active (worktree) · `◇` in review (PR open) ·
`◆` approved (PR review APPROVED — ready to `/merge`) · `✓` merged.

A session sitting in the **main repo root** — the hub you kick tasks off from —
gets `◉` instead (e.g. `◉ Manager`), so the sidebar reads as one hub plus its
task satellites. `◉` marks the *location*, not an identity: every tab at the
main root carries it. It is stateless — the state glyphs belong to tasks.

The mapping and its precedence live in `scripts/ws-statusline.sh` (a `states`
mode next to the render mode — one file, so sidebar and statusline can never
disagree); `scripts/herdr-tab-glyph.sh` applies it to herdr agent names.
disagree); `scripts/herdr-tab-glyph.sh` applies it to the herdr tab label.

The glyph is stamped when `/kickoff` or `/continue` opens the tab, and
re-stamped — idempotently, only when it changed — whenever you survey or move
Expand All @@ -233,8 +238,13 @@ same refresh, so `●` flips to `◇` when the PR opens, to `◆` when it is app
and to `✓` when it merges. Survey surfaces (`/status`, `/list`, `/check`,
`/close`) read the PR cache and never block; the state-changing skills (`/open`,
`/merge`, `/cycle`) do a bounded synchronous `gh` refresh so the new state shows
at once. Agents outside task worktrees are never renamed; outside herdr
everything is a silent no-op.
at once. The same refreshes stamp `◉` on the main-root tabs — no separate
trigger. Only tabs sitting *exactly* at a task worktree or the main root are
renamed (one cd into a subdir and yours is left alone); a tab is renamed only
when its label actually changes, your chosen label is kept and merely prefixed,
and outside herdr everything is a silent no-op. The glyph lives in the **tab
label** and nowhere else — the herdr agent name and the `claude -n` session name
keep the plain label, since those are stable identities.

## Adopting Existing Branches

Expand Down
22 changes: 15 additions & 7 deletions plugins/work-system/scripts/herdr-launch.sh
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,9 @@
# herdr-launch.sh resume <label> <worktree-abs-path> <workspace-id>
# label short, sidebar-friendly agent/tab name (e.g. close-herdr).
# Pass it PLAIN — this helper prefixes the task's state glyph
# (○ ● ◇ ✓, via herdr-tab-glyph.sh) itself, best-effort.
# (○ ● ◇ ◆ ✓, via herdr-tab-glyph.sh) itself, best-effort,
# onto the TAB LABEL only; the agent and session names keep
# the plain label (see the stamping block below).
# worktree absolute path to the worktree (becomes the new pane's cwd)
# workspace-id herdr workspace to open the tab in (e.g. $HERDR_WORKSPACE_ID)
# session-name (launch only) `claude -n` name; defaults to <label>
Expand Down Expand Up @@ -86,15 +88,21 @@ command -v python3 >/dev/null 2>&1 || { echo "python3 not on PATH" >&2; exit 1;
}
[ -d "$worktree" ] || { echo "worktree dir not found: $worktree" >&2; exit 1; }

# Stamp the task's CURRENT state glyph onto the sidebar label (○ ● ◇ ✓ — the
# Stamp the task's CURRENT state glyph onto the sidebar label (○ ● ◇ ✓ — the
# same mapping the [ws …] statusline renders; ws-statusline.sh is the single
# source, applied via herdr-tab-glyph.sh). Best-effort: any failure keeps the
# plain label. The Claude session name (launch mode) was already defaulted from
# the PLAIN label above — glyphs would clutter /resume.
# plain label.
#
# ONLY the TAB LABEL carries a glyph — `$label` stays plain for everything else.
# The tab label is what the sidebar renders and what `herdr-tab-glyph.sh refresh`
# keeps current as the task's state moves. The herdr *agent* name (`agent start
# <name>`) and the Claude session name are stable identities: a glyph there would
# freeze at its launch-time value (nothing refreshes them) and clutter /resume.
tab_label="$label"
glyph_helper="${0%/*}/herdr-tab-glyph.sh"
if [ -f "$glyph_helper" ]; then
stamped="$(bash "$glyph_helper" prefix "$label" "$worktree" 2>/dev/null || true)"
[ -n "$stamped" ] && label="$stamped"
[ -n "$stamped" ] && tab_label="$stamped"
fi

# JSON extractors. null / missing / malformed all yield empty, and a stray
Expand Down Expand Up @@ -138,7 +146,7 @@ case "$mode" in
# Relocate the agent into its own background tab (agent start splits the
# caller's tab). If the move fails, Claude is still running — report it in
# place rather than claiming a tab that does not exist.
if move_json="$(herdr pane move "$pane" --new-tab --label "$label" --no-focus 2>/dev/null)"; then
if move_json="$(herdr pane move "$pane" --new-tab --label "$tab_label" --no-focus 2>/dev/null)"; then
tab="$(printf '%s' "$move_json" | python3 -c "$extract_moved_tab" 2>/dev/null || true)"
printf 'pane=%s\ntab=%s\nmoved=yes\n' "$pane" "$tab"
else
Expand Down Expand Up @@ -197,7 +205,7 @@ case "$mode" in
# empty pane id (with a present tab id) stays empty and trips the guard below,
# rather than the tab id being mis-read as the pane id.
create_json="$(herdr tab create --workspace "$workspace" \
--cwd "$worktree" --label "$label" 2>/dev/null || true)"
--cwd "$worktree" --label "$tab_label" 2>/dev/null || true)"
pane_tab="$(printf '%s' "$create_json" | python3 -c "$extract_root_pane_tab" 2>/dev/null || true)"
pane="${pane_tab%%|*}"
tab="${pane_tab#*|}"
Expand Down
Loading
Loading