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. Keep tasks as GitHub issues or local 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.31.1"
"version": "1.32.0"
},
{
"name": "pr-flow",
Expand Down
1 change: 1 addition & 0 deletions .claude/knowledge/_index.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,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, Manager close-delegation, ask-on-doubt auto-accept + follow-up sweep; `/land` composition + `--delegate` preselect; follow-ups do/ask/record (ROADMAP via `followups.py`) and Manager-owned deploy (`deploy-owner`); the `landing.sh` marker (git common dir, pane+session identity) for a lane torn down under `/land`
- `features/manager-address.md` — `manager.sh`: Manager resolver with both addresses (herdr pane+agent session, SendMessage name); live-revalidated `.ws-kicker` record + Manager-side backfill, stated leftmost-tab tie-break, fail-closed; `/continue` milestone pings; composer draft check (labeled upper rule, dim suggestions; lives in `herdr-agent.sh` as `$HERDR_COMPOSER_PY`)
- `features/manager-worker-status.md` — `workers.sh` (`/work-system:manager` status slice): wrapper-exported preludes (no second classifier), read-only argv proof, own-pane guard in `manager.sh prompt`, dedupe by pane+UUID / lane+head (unknown = no observation, scope-bounded prune), one tri-state ready rule (assess+gh+`covered`, zero checks ≠ green), settings from main, on-blocked command only from user scope, one-shot + flock (no loop)
- `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; optional PATH-detected `cc-harness:<id>` class (pure consumer of `list`/`exec`, no gateway hardcoding); grok's dynamic `latest` token vs. explicit pins (no fallback version); bounded model-aware grok/kimi probes (inconclusive→trust-auth for pins); kimi's two-phase seed+continue argv + `argv_shell=`; non-claude "document, don't fake" degradation; announce-not-prompt for external defaults
- `features/github-issues-task-backend.md` — `task-store.py` as the one backlog path: `tasks.backend` auto|github|local (local files win until migrated; an installed-but-failing gh is exit 4, never a silent local fallback), label + body-marker identity, copy-not-link TASK.md, `[tasks]` issue filter (one predicate, exclude wins, title-slug names) + script-enforced gate labels (exit 7); network-free statusline counts lanes; `migrate`: (marker, archived) identity, write-then-move resume, plan-hash confirm token, adopt tracking issues, comment-preserving backend switch
Expand Down
10 changes: 6 additions & 4 deletions .claude/knowledge/features/manager-address.md
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
---
title: "Manager Address Resolver and Milestone Pings"
createdAt: 2026-10-02
updatedAt: 2026-10-20
updatedAt: 2026-11-04
createdFrom: "session: 2026-10-02 (task/add-manager-address)"
updatedFrom: "branch: task/add-session-plugin-refresh"
updatedFrom: "branch: task/add-manager-status-and-events"
pluginVersion: 1.9.0
prime: false
---
Expand Down Expand Up @@ -91,7 +91,9 @@ terminal gate) and `/close` step 1b delegation, which replaced the name-only
(`[work-system ping from task=… worktree=…] … (info only…)`) both routes send.
SendMessage is skill-side (a script cannot call a tool) and needs exactly one
live session with the name. The herdr route `manager.sh prompt` re-resolves and
types only into an idle/done **claude** Manager whose composer holds no draft.
types only into an idle/done **claude** Manager whose composer holds no draft,
and never into the caller's own pane (`manager-is-self`, added with
`workers.sh events`: a Manager-run timer must not prompt itself).
- **One event cap, never silent.** The event text keeps 1000 chars on both
routes; a cut ends in `…` plus `truncated=yes orig_len=<n>` on stderr (a plain
300-char slice once lost a gate ping's open decision unnoticed). No shorter
Expand All @@ -113,7 +115,7 @@ terminal gate) and `/close` step 1b delegation, which replaced the name-only
## Gotchas

- **Composer draft detection** (`$HERDR_COMPOSER_PY` in `herdr-agent.sh` since
work-system 1.26.0, shared with `sessions.sh refresh`) reads `herdr agent read --source visible --format
work-system 1.26.0, shared with `sessions.sh refresh` and `workers.sh`) reads `herdr agent read --source visible --format
ansi` and inspects the region between the last two horizontal rules. The UPPER
rule carries a label (CC prints the session name into it), so a rule is "starts
with ten `─`", not "consists only of `─`". Dim and **mid**-gray cells are not a
Expand Down
113 changes: 113 additions & 0 deletions .claude/knowledge/features/manager-worker-status.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
---
title: "Manager Worker Status and Status-Change Events"
createdAt: 2026-11-03
updatedAt: 2026-11-04
createdFrom: "branch: task/add-manager-status-and-events"
updatedFrom: "branch: task/add-manager-status-and-events"
pluginVersion: 1.9.0
prime: false
---

# Manager Worker Status and Status-Change Events

`scripts/workers.sh` (logic in `workers.py`) is the status slice of
`/work-system:manager`: `list`, `screen`, `question` (read-only) and `events`
(one-shot, deduplicated). Dispatch/collect are a later slice
(`add-manager-watch-loop`).

## Decisions

- **No second classifier, no copies.** `workers.sh` follows the `sessions.sh`
pattern: it sources `herdr-agent.sh` and exports `$HERDR_MATCH_PRELUDE`,
`$HERDR_NAME_PRELUDE` and `$HERDR_COMPOSER_PY`; `workers.py` `exec`s the
preludes and dies with exit 3 when run without the wrapper. Roles come from
`classify_cwd` against the main root that `main-repo-path.sh` finds for each
agent cwd (the prototype's `.git`-dir check was a second classifier).
- **Read-only by construction.** The only write path is `manager.sh prompt`.
`test_workers.py` proves it through the fake herdr's argv log across every
run (`agent send-keys|prompt|send-text`, `pane send-*` never appear). There is
no `answer` subcommand: questions are handed up to the human, never answered.
- **Own-pane guard lives in `manager.sh prompt`**, not in `events`: a Manager
running `events` would resolve itself as the lane's Manager. The root fix
(`reason=manager-is-self`) protects every caller; `test_manager.py` now calls
`prompt` from a worker pane because its fixture's caller pane IS the Manager.
- **Dedupe identity.** `blocked` = pane + session UUID (an `/exit` successor in
the same pane starts fresh; no UUID → no event). `ready-for-merge` = lane
realpath + head (a new reviewed head re-arms). A busy worker keeps its ready
record instead of resetting it, so a working spell does not re-fire.
- **ready-for-merge without a run record** (Robert, 2026-11-03): open PR from
`task-status.sh assess`, then `gh pr view`: same-repo, non-draft, head == lane
HEAD (pushed), `MERGEABLE`, no `CHANGES_REQUESTED`, at least one check and all
green (zero checks is NOT green: right after a push CI may not have registered),
plus `mandate.sh covered`, and `mergeStateStatus` CLEAN/HAS_HOOKS (round 2:
`mergeable` ignores branch protection, so a fast green lint before the
required check registered read as ready; BLOCKED/BEHIND now say no). One
rule, `ready_for_merge()`, behind both `events` and `workers.sh ready` — the
skill calls the latter instead of deriving it.
Tri-state yes/no/unknown: `unknown` (gh blip, lazy `mergeable`, assess's
`pr_state=none`, which is also what a failing gh looks like) keeps the record,
so a transient error never re-arms a delivered event. A `no` keeps the head
it last fired for (round 3: a re-run check going red then green re-fired the
same head), and a fired head with nothing left to deliver is not re-checked
(saves the gh chain per tick). Only checked once EVERY agent in the lane
is idle/done without `verify` (round 3: the first-listed pane decided); round
4 moved that check into `ready` too (`lane-busy`), so skill and events agree;
round 5: over every workspace (a `--workspace` run saw only its own panes)
and every non-self agent in the lane or a sub-directory of it (an agent in
`<lane>/src` classifies as `other`); `ready` reads only that lane's screens.
Switch to the run record once it exists.
- **The command never comes from a repo** (Robert): `on-blocked` only from the
CLI flag or `~/.config/work-system/events.toml` (O_NOFOLLOW, owner == uid, not
group/world-writable). The schema deliberately has no command key; a repo's
`.work-system.toml` gets only `events.enabled` and `events.escalate_after`,
read at the MAIN checkout (`SETTINGS_PROJECT_ROOT`), never the lane's own
worker-writable copy — otherwise a worker could silence its own events.
- **Dedupe edges found in review:** herdr's `unknown` status is no observation
(keeps the blocked record); records carry their workspace and are pruned only
within the run's scope (a `--workspace w2` run must not wipe the `--all`
timer's w1 records); an empty agent list prunes nothing (herdr repopulating,
as lanes.sh treats it), and only a record whose pane, session or lane was seen
gone is pruned (round 4: a role that fell to `other` on a main-repo-path
timeout, or a missing UUID, deleted the record and re-fired next tick); a
re-sent ping of an open event is a `retry=` line, not an event; a failing
hook is retried on at most 3 runs, and its output goes to /dev/null (a captured pipe held by a background notifier ran
into the 30 s timeout and re-fired); a ping whose reason cannot change
(`manager-is-self`, non-claude Manager, `bad-pane`, `no-main-repo`, all after
a unique resolve) is `final`, not retried each tick.
- **A failed read is not an empty screen** (`read_text` returns None): an idle
row whose screen could not be read gets `verify`, `screen` exits 4, `question`
exits 4 for a blocked pane and prints `asks=unknown` for an idle one.
The state file is validated before any delivery: a lost state re-sends every
event, so unreadable JSON is exit 2, only a missing file starts empty.
- **Dialog options** come from one 1..k run near the bottom; description lines
between options are allowed (AskUserQuestion), and a question or `[y/n]` line
below the run voids it (an older list above a later prompt is not the dialog).
- **A typed ping slugs task and path** (round 6): `manager.sh prompt` types the
sender prefix into the Manager's composer, so `task=` and `worktree=` are
slugs there (a worktree name with spaces read as a sentence outside the
bracket); the SendMessage `body` keeps the real names. `ping_text` carries no
task name at all, so the slug rule lives in one place.
- **Raw cwd for paths, `clean()` for display.** The prototype stored the
display-cleaned cwd and then used it for git/settings/ping: a whitespace fold
or truncation pointed them at another (or no) worktree.
- **Ping in the worker's workspace.** `manager.sh resolve` scopes its fallback by
`$HERDR_WORKSPACE_ID`; `events` sets it to the worker's workspace for the ping
(the caller may be a timer outside herdr or a Manager elsewhere) and leaves
`$HERDR_PANE_ID` as the caller's, so the own-pane guard still holds.
- **No loop ships.** herdr 0.9.5 has no status-change hook (`agent wait` only),
so `events` is a one-shot for a user timer; a `<state>.lock` flock makes an
overlapping tick print `summary: skipped=locked` instead of double-pinging.
A herdr failure exits 3/4/5 before the state file is touched.

## Residuals

- Composer may change between `manager.sh prompt`'s draft read and the send
(herdr has no atomic check-and-prompt), as for every `prompt` caller.
- Flags `asks`/`verify` are screen heuristics; `verify` only says "read it".
- The hook payload carries `last_line` and the cwd (the spec requires them); a
screen line can hold a secret, so the README tells users to forward it only
where they would show the screen.
- `ready_for_merge` costs two gh calls per idle lane per tick (assess + view):
kept, because assess is the agreed source; fold into one `gh pr view` when the
run record replaces it.
- cron's minimal env: herdr/gh/Python 3.11 must be on the entry's `PATH`.
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.32.0 — 2026-11-03
- `/work-system:manager`, first slice (status): `scripts/workers.sh` lists the herdr agents of a space with role (`manager`/`worker`/`other`/`self`, from the shared `classify_cwd`), task, status and flags (`question` blocked dialog, `asks` trailing question, `verify` idle while the screen shows activity — herdr #4376/#3090, `draft` someone types), reads a pane (`screen`, `--lines` clamped to 20..200 for herdr #5462, joined `※ recap:`) and a pending dialog (`question`, the live dialog's options and preselection, read-only), and answers `ready <worktree | pane>`. The skill shows questions and ready-for-merge PRs to the human; it never answers, types, merges or approves.
- `workers.sh events --state <file>`: one-shot, deduplicated status-change events for a user timer (no loop ships). `blocked` per pane + session UUID and transition (an `/exit` successor starts fresh), `ready-for-merge` per lane + head (open, non-draft, same-repo PR at the lane's HEAD, merge state clean, no changes requested, at least one check and all green, `mandate.sh covered`; checked only once every agent in the lane or a sub-directory of it is idle; an unreadable answer is `unknown` and never re-arms an event, nor does a check that goes red and green again on the same head). Stage 1 pings the Manager through `manager.sh prompt`; stage 2 runs an `on-blocked` command (argv, JSON on stdin, for either event) only when the ping did not land or after `escalate_after`. The command comes only from a CLI flag or the user-scope `~/.config/work-system/events.toml`, never from a repo; the main checkout's `.work-system.toml` gets `[events] enabled` and `escalate_after` (`events` needs Python 3.11+ to read them, else exit 3). A ping whose reason cannot change (own pane, non-claude Manager, bad pane, no main repo) is not retried, and a retried ping prints `retry=`, not a new `event=`; a record is forgotten only when its pane, session or lane was seen gone; `ready` also answers `lane-busy` while another agent of the lane works; `events` judges a lane busy over every workspace and counts any agent in the lane or a sub-directory of it; the screen and dialog blocks blank bidi and leftover control characters (C1, unterminated escapes); the lock file is opened without following symlinks; a typed ping carries the task and worktree only as slugs (the SendMessage body keeps them as they are); the command's output is discarded, so a background child it starts does not hold the run; an empty agent list forgets no record; the state is saved after every ping or hook run, so a run killed mid-loop does not re-send what it delivered. Ignoring a lane's own `.work-system.toml` is defense in depth: a worker under the same user could still edit the main checkout.
- `manager.sh prompt` never types into the caller's own pane (`reason=manager-is-self`).

### 1.31.1 — 2026-11-03
- `/sessions` recognises a Claude session started by the native installer: herdr reports its foreground process with no `argv0`, `argv[0]` `~/.local/share/claude/versions/<ver>` and the version as its name, so `sessions.py` read it as no claude, every plugin as `unknown`, and never reloaded it. `is_claude_path()` accepts a `claude` basename or a path ending in `claude/versions/<ver>` with a version-shaped `<ver>`; a bare version string (`2.1.316`, `/usr/bin/2.1.316`) or another file under `versions/` (a `.partial` download) still is not claude.

Expand Down
2 changes: 1 addition & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ This is a **Claude Code plugin marketplace** (monorepo) containing plugins that
## Current Plugins

- **knowledge-system** (v1.9.x) — Knowledge management with three layers: Rules, Knowledge, Memory. Skills: `/init`, `/query`, `/curate`, `/reindex`, `/backfill-knowledge`, `/migrate`, `/statusline`
- **work-system** (v1.31.x) — Task and worktree workflow; backlog in GitHub Issues or `tasks/` files via `task-store.py` (`tasks.backend`); workers: Claude/codex/grok/kimi or PATH-detected cc-harness; `/kickoff` records the lane's autonomy mandate in `MANDATE.md`, `/continue` works from it; a worktree `/close` can delegate its teardown to the Manager session, and `/continue` pings it at milestones; `/land` composes `/pr-flow:merge` + `/close --delegate`; the Manager runs, asks or records (ROADMAP) a closed lane's follow-ups and owns deploys; with the optional `insights` plugin, `/continue` reports at its terminal gate, `/land` when landing, and `/close` before teardown; `/sessions` reloads idle herdr sessions running stale plugin code; `herdr-shell.sh` drives interactive programs in temp herdr tabs. Skills: `/define`, `/kickoff`, `/adopt`, `/continue`, `/status`, `/close`, `/land`, `/list`, `/work-system:migrate`, `/sessions`, `/statusline`
- **work-system** (v1.32.x) — Task and worktree workflow; backlog in GitHub Issues or `tasks/` files via `task-store.py` (`tasks.backend`); workers: Claude/codex/grok/kimi or PATH-detected cc-harness; `/kickoff` records the lane's autonomy mandate in `MANDATE.md`, `/continue` works from it; a worktree `/close` can delegate its teardown to the Manager session, and `/continue` pings it at milestones; `/land` composes `/pr-flow:merge` + `/close --delegate`; the Manager runs, asks or records (ROADMAP) a closed lane's follow-ups and owns deploys; with the optional `insights` plugin, `/continue` reports at its terminal gate, `/land` when landing, and `/close` before teardown; `/work-system:manager` hands worker questions up, `workers.sh events` pings on blocked/ready-for-merge; `/sessions` reloads idle herdr sessions running stale plugin code; `herdr-shell.sh` drives interactive programs in temp herdr tabs. Skills: `/define`, `/kickoff`, `/adopt`, `/continue`, `/status`, `/close`, `/land`, `/list`, `/work-system:migrate`, `/work-system:manager`, `/sessions`, `/statusline`
- **pr-flow** (v1.9.x) — PR review feedback loop; mandate-aware and review-bot-aware (route remembered per repo); defers deploys to the Manager inside a lane unless `deploy.by = "worker"`. Skills: `/open`, `/cycle`, `/check`, `/fix`, `/rebase`, `/merge`
- **swarm** (v0.17.x) — Local mixture-of-agents code review (external `codex`/`grok` CLIs or `cc-harness:<id>` gateway voices, `kimi` opt-in, plus lenses on the session model: 11 in 5 clusters; consensus family follows the session model). Profile-based cluster/lens fan-out; externals get read + web research under an OS secret-jail. P2: `/swarm:review` pipeline (scope→fan-out→merge→verify); P5: `--fix`/`--loop` apply the findings you agreed with. Skills: `/swarm:review`, `/swarm:agents`
- **settings** (v0.1.x) — Per-plugin TOML config resolved over schema defaults; each plugin owns its `schema/settings.schema.json`. Skill: `/settings` (list/show/get/set/validate). First consumer: pr-flow `review.route`.
Expand Down
Loading
Loading