From 5fedf3d282f6019a5d92d0423ea939d8a7105bf3 Mon Sep 17 00:00:00 2001 From: Robert Gering Date: Fri, 17 Jul 2026 14:57:20 +0200 Subject: [PATCH 1/3] =?UTF-8?q?Add=20Manager=20glyph=20(=E2=97=89)=20to=20?= =?UTF-8?q?the=20main-repo=20herdr=20tab?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Extend herdr-tab-glyph.sh's refresh sweep with a second exact-cwd match branch: a tab sitting at the main repo root is the "Manager" hub among the ○ ● ◇ ◆ ✓ task satellites and gets a leading ◉. - Stateless and non-exclusive: ◉ marks the location, so every tab at the main root carries it (documented in the script header) - No new trigger — the existing refresh callers (/status, /list, /close, pr-flow's PR-lifecycle skills) stamp both kinds in one sweep - Prefix only, never replace; ◉ joins the idempotency strip so hub↔task moves swap glyphs cleanly and re-refreshes never stack - The extractor's TSV gains a `kind` column; `key` carries the task name or the repo dir name and is never empty — `IFS=$'\t' read` collapses runs of tabs (tab is IFS whitespace), so an empty middle field would shift every later field left (caught in test: `◉ Manager` became `◉ claude-plugins`) Verified against a stubbed and a live herdr agent list: exact-cwd matching, no-churn guard, subdir/foreign-repo tabs untouched, flag-shaped pane ids rejected. Bump work-system 1.8.0 → 1.8.1. Co-Authored-By: Claude Opus 4.8 (1M context) --- .claude-plugin/marketplace.json | 2 +- .claude/knowledge/_index.md | 2 +- .../knowledge/features/herdr-tab-glyphs.md | 30 +++++--- CHANGELOG.md | 3 + .../work-system/.claude-plugin/plugin.json | 2 +- plugins/work-system/README.md | 12 +++- .../work-system/scripts/herdr-tab-glyph.sh | 68 ++++++++++++------- 7 files changed, 82 insertions(+), 37 deletions(-) diff --git a/.claude-plugin/marketplace.json b/.claude-plugin/marketplace.json index e0adbbe..2b01083 100644 --- a/.claude-plugin/marketplace.json +++ b/.claude-plugin/marketplace.json @@ -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", diff --git a/.claude/knowledge/_index.md b/.claude/knowledge/_index.md index e4e9e89..011d419 100644 --- a/.claude/knowledge/_index.md +++ b/.claude/knowledge/_index.md @@ -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 ` 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 (`○ ● ◇ ◆ ✓`) plus the main-root `◉` on herdr tab names: `states` mode inside the self-contained renderer (no sibling), sync-vs-`--cached` PR refresh per caller, exact-cwd rename rules, the no-empty-TSV-field trap, 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) diff --git a/.claude/knowledge/features/herdr-tab-glyphs.md b/.claude/knowledge/features/herdr-tab-glyphs.md index e399ef2..d09daec 100644 --- a/.claude/knowledge/features/herdr-tab-glyphs.md +++ b/.claude/knowledge/features/herdr-tab-glyphs.md @@ -11,7 +11,10 @@ prime: false work-system prefixes each task tab's herdr agent name 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`). @@ -60,15 +63,26 @@ was shipped and caught in review. 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 ==
/.claude/worktrees/` - (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). +- **Matching:** exact realpath equality against `
/.claude/worktrees/` + (→ 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. Rename targets the `pane_id`; `herdr agent list` + exposes `cwd`, `name`, `pane_id` per agent (verified live 2026-07-16). - **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 diff --git a/CHANGELOG.md b/CHANGELOG.md index 41d9140..633ad19 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -52,6 +52,9 @@ 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 name is preserved (prefix only) and `◉` joins the idempotency strip, so hub↔task moves swap glyphs cleanly. + ### 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. diff --git a/plugins/work-system/.claude-plugin/plugin.json b/plugins/work-system/.claude-plugin/plugin.json index aee3d11..ec2eb64 100644 --- a/plugins/work-system/.claude-plugin/plugin.json +++ b/plugins/work-system/.claude-plugin/plugin.json @@ -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" }, diff --git a/plugins/work-system/README.md b/plugins/work-system/README.md index 450f003..13bfb6e 100644 --- a/plugins/work-system/README.md +++ b/plugins/work-system/README.md @@ -221,6 +221,11 @@ 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. @@ -233,8 +238,11 @@ 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 name actually changes, your chosen name is kept and merely prefixed, +and outside herdr everything is a silent no-op. ## Adopting Existing Branches diff --git a/plugins/work-system/scripts/herdr-tab-glyph.sh b/plugins/work-system/scripts/herdr-tab-glyph.sh index 77aa76b..66c13c0 100755 --- a/plugins/work-system/scripts/herdr-tab-glyph.sh +++ b/plugins/work-system/scripts/herdr-tab-glyph.sh @@ -2,7 +2,10 @@ # herdr-tab-glyph.sh — mirror work-system task states onto herdr tab/agent # names as a leading state glyph (○ not-started · ● active · ◇ in review · # ◆ approved · ✓ merged), so the herdr sidebar speaks the same visual language -# as the [ws …] status-line segment. +# as the [ws …] status-line segment. A session sitting in the MAIN repo root +# instead gets ◉ — the "Manager" hub among the task satellites. ◉ marks the +# location, not an identity: if several tabs sit at the main root, they ALL +# carry it. # # The state→glyph mapping and its precedence live in ws-statusline.sh # (`states` mode) — the ONE source both surfaces read; this script only @@ -22,11 +25,14 @@ # refresh [--cached] [] # Re-stamp the glyph on every herdr agent whose cwd IS a task worktree # of the repo containing (default: $PWD) — exact realpath match on -#
/.claude/worktrees/, across ALL workspaces of this herdr -# server, so one refresh fixes every open task tab of the repo. Agents -# outside task worktrees are never touched; a rename is only issued when -# the name actually changes. Prints `checked=N updated=M` when herdr was -# reachable; silent no-op otherwise. Always exits 0. +#
/.claude/worktrees/ — or IS the main repo root itself (→ ◉), +# across ALL workspaces of this herdr server, so one refresh fixes every +# open tab of the repo. Both matches are exact: an agent merely cd'd into +# a subdir of either is never touched, nor is anything outside the repo. +# A rename is only issued when the name actually changes. Prints +# `checked=N updated=M` when herdr was reachable; silent no-op otherwise +# — including for a repo with an empty backlog, where the ◉ stamp is +# skipped too (no workers, no manager). Always exits 0. # --cached forwards to `ws-statusline.sh states --cached` (read the PR # cache + non-blocking background refresh, never a synchronous gh call) — # for pure-survey callers (/status, /list, /check, /close). Without it the @@ -42,7 +48,7 @@ strip_glyph() { local l="$1" while :; do case "$l" in - "○ "*|"● "*|"◇ "*|"◆ "*|"✓ "*) l="${l#* }" ;; + "○ "*|"● "*|"◇ "*|"◆ "*|"✓ "*|"◉ "*) l="${l#* }" ;; *) break ;; esac done @@ -64,10 +70,16 @@ task_glyph() { bash "$ws" states "$2" 2>/dev/null | glyph_lookup "$1" } -# Emit "pane_id\ttask\tcurrent_name" for every agent whose realpath(cwd) is -# EXACTLY
/.claude/worktrees/ (argv[1] = main repo path). Exact -# match mirrors herdr-teardown.sh's cwd philosophy: an unrelated agent merely -# cd'd into a worktree subdir is never renamed. Malformed JSON emits nothing. +# Emit "pane_id\tkind\tkey\tcurrent_name" for every agent whose realpath(cwd) +# is EXACTLY
/.claude/worktrees/ (kind=task, key=) or EXACTLY +# the main repo root (kind=main, key=the repo dir name) — argv[1] = main repo +# path. Exact match mirrors herdr-teardown.sh's cwd philosophy: an unrelated +# agent merely cd'd into a subdir of either is never renamed. `key` doubles as +# the name fallback for an unnamed agent. Malformed JSON emits nothing. +# NO FIELD BUT THE LAST MAY EVER BE EMPTY: the consumer reads with IFS=, +# and tab is IFS *whitespace* — bash collapses a run of them into one +# delimiter, so an empty middle field would silently shift every later field +# left (kind=main once put the name into `key` this way). # The agent fields are UNTRUSTED (any tool in the session can set a name): a # tab/newline embedded in a field would forge extra TSV records and aim a # rename at an arbitrary pane — so the pane id must match herdr shape, a task @@ -76,11 +88,12 @@ task_glyph() { # pattern forbids a LEADING dash (first char excludes `-`) so a value like # `-x`/`--foo` can never reach `herdr agent rename` as an option flag; herdr # ids are `wN:pM` and never start with a dash, so nothing legitimate is lost. -extract_task_agents='import sys, json, os, re +extract_glyph_agents='import sys, json, os, re main = sys.argv[1] if len(sys.argv) > 1 else "" if not main.strip(): sys.exit(0) -wtdir = os.path.join(os.path.realpath(main), ".claude", "worktrees") +root = os.path.realpath(main) +wtdir = os.path.join(root, ".claude", "worktrees") try: agents = json.load(sys.stdin)["result"]["agents"] except Exception: @@ -91,13 +104,16 @@ for a in agents: if not cwd or not re.fullmatch(r"[A-Za-z0-9:_.][A-Za-z0-9:_.-]*", pane): continue cwd = os.path.realpath(cwd) - if os.path.dirname(cwd) != wtdir: + if cwd == root: + kind, key = "main", os.path.basename(root) + elif os.path.dirname(cwd) == wtdir: + kind, key = "task", os.path.basename(cwd) + else: continue - task = os.path.basename(cwd) - if re.search(r"[\t\r\n]", task): + if not key or re.search(r"[\t\r\n]", key): continue name = re.sub(r"[\t\r\n]", " ", a.get("name") or "") - print("\t".join([pane, task, name]))' + print("\t".join([pane, kind, key, name]))' cmd_prefix() { local label="${1:-}" worktree="${2:-}" base name glyph @@ -143,19 +159,23 @@ cmd_refresh() { list="$(herdr agent list 2>/dev/null || true)" [ -n "$list" ] || return 0 agents="$(printf '%s' "$list" \ - | python3 -c "$extract_task_agents" "$main" 2>/dev/null || true)" + | python3 -c "$extract_glyph_agents" "$main" 2>/dev/null || true)" if [ -z "$agents" ]; then echo "checked=0 updated=0" return 0 fi - local checked=0 updated=0 pane task name glyph base new - while IFS=$'\t' read -r pane task name; do - [ -n "$pane" ] && [ -n "$task" ] || continue - glyph="$(printf '%s\n' "$states" | glyph_lookup "$task")" - [ -n "$glyph" ] || continue # worktree without a backlog task — leave alone + local checked=0 updated=0 pane kind key name glyph base new + while IFS=$'\t' read -r pane kind key name; do + [ -n "$pane" ] && [ -n "$key" ] || continue + if [ "$kind" = "main" ]; then + glyph="◉" # the Manager hub — no state, just the place + else + glyph="$(printf '%s\n' "$states" | glyph_lookup "$key")" + [ -n "$glyph" ] || continue # worktree without a backlog task — leave alone + fi checked=$((checked + 1)) base="$(strip_glyph "$name")" - [ -n "$base" ] || base="$task" # unnamed agent → fall back to the task name + [ -n "$base" ] || base="$key" # unnamed agent → task name / repo dir name new="$glyph $base" [ "$new" = "$name" ] && continue # already correct — no rename churn # No `--` guard here: `herdr agent rename` treats `--` as the target itself From ace6d2e488c070913b458c38fc678ff6d5efbae1 Mon Sep 17 00:00:00 2001 From: Robert Gering Date: Fri, 17 Jul 2026 15:44:26 +0200 Subject: [PATCH 2/3] Write tab glyphs to the tab label, not the agent name MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The sidebar renders a herdr tab's LABEL. `refresh` rewrote the agent registry's separate `name` field instead, so every re-stamp since 1.8.0 was invisible: a tab kept its launch-time glyph forever (remove-swarm-composer sat at ● with its PR long in review). Caught by testing the ◉ end-to-end against a live herdr rather than trusting `agent list`. - refresh now joins `herdr agent list` (carries cwd — the match) with `herdr tab list` (carries label — what we prefix and diff), keyed on tab_id, and issues `herdr tab rename` - One tab is stamped once: first matching agent wins, so a multi-pane tab with mixed cwds cannot flip-flop between refreshes - herdr-launch.sh passes the glyph only to `--label`; `agent start` and `claude -n` keep the plain label. Agent and session names are stable identities — nothing refreshes them, so a glyph there freezes at its launch value. Also lifts the ◆ omission in the header's mapping. - The id-shape/injection guard moves from pane_id to tab_id Verified live: the six task tabs plus the ◉ Manager tab now carry current glyphs (● → ◇ where the PR moved), a second refresh reports updated=0, and tabs of other repos are untouched. Co-Authored-By: Claude Opus 4.8 (1M context) --- .claude/knowledge/_index.md | 2 +- .../knowledge/features/herdr-tab-glyphs.md | 25 +++- CHANGELOG.md | 4 +- plugins/work-system/README.md | 6 +- plugins/work-system/scripts/herdr-launch.sh | 22 +++- .../work-system/scripts/herdr-tab-glyph.sh | 124 +++++++++++------- 6 files changed, 117 insertions(+), 66 deletions(-) diff --git a/.claude/knowledge/_index.md b/.claude/knowledge/_index.md index 011d419..bb890e0 100644 --- a/.claude/knowledge/_index.md +++ b/.claude/knowledge/_index.md @@ -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 ` 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 (`○ ● ◇ ◆ ✓`) plus the main-root `◉` on herdr tab names: `states` mode inside the self-contained renderer (no sibling), sync-vs-`--cached` PR refresh per caller, exact-cwd rename rules, the no-empty-TSV-field trap, soft pr-flow shim +- `features/herdr-tab-glyphs.md` — Task-state glyphs (`○ ● ◇ ◆ ✓`) plus the main-root `◉` on herdr tab names: the tab-label-vs-agent-name namespace (the sidebar renders the label — 1.8.0 wrote the wrong one), `states` mode inside the self-contained renderer (no sibling), sync-vs-`--cached` PR refresh per caller, exact-cwd rename rules, the no-empty-TSV-field trap, 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) diff --git a/.claude/knowledge/features/herdr-tab-glyphs.md b/.claude/knowledge/features/herdr-tab-glyphs.md index d09daec..da2b0ae 100644 --- a/.claude/knowledge/features/herdr-tab-glyphs.md +++ b/.claude/knowledge/features/herdr-tab-glyphs.md @@ -58,17 +58,28 @@ 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`. +- **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