Skip to content
Open
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.28.1"
"version": "1.28.2"
},
{
"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
- `features/backfill-and-origin-metadata.md` — `/backfill-knowledge` significance bar + origin-reconstruction cascade
- `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` + `/adopt`) + `resume`; feature-detected dual contract (legacy placement vs 0.7.5+ `agent start --kind --pane`), registry-declared transport, rollback vs `blocked=unverified`, worker death from process state not terminal text
- `features/herdr-kickoff-automation.md` — herdr `herdr-launch.sh`: `launch` (`/kickoff` + `/adopt`) + `resume`; feature-detected dual contract (legacy placement vs 0.7.5+ `agent start --kind --pane`), registry-declared transport, rollback vs `blocked=unverified`, 0.8.2 idle-readiness timeout confirmed by detection + rename, worker death from process state not terminal text
- `features/herdr-shell-interactive-tabs.md` — `herdr-shell.sh`: temp `tmp-` tabs for screen programs + `handover`; termios gate (canonical+no-echo; `read -s` is a builtin), socket-only `send_input`, secret fetched before the gate, pid-based fg, flock across create+record vs sweep, `python3 -I` (cwd shadows stdlib), teardown-by-cwd gap, `smoke_*` not `test_*`
- `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`
Expand Down
51 changes: 47 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-08-19
updatedAt: 2026-10-26
createdFrom: "PR #17"
updatedFrom: "session: 2026-08-19 (task/offer-cc-harness-agents-at-kickoff)"
pluginVersion: 1.12.0
updatedFrom: "session: 2026-10-26 (task/fix-herdr-launch-unverified-on-0-8-2)"
pluginVersion: 1.28.2
prime: false
reindexedAt: 2026-07-12
---
Expand Down Expand Up @@ -164,7 +164,8 @@ decisions from the fix:
Definitive failures close the created tab exactly once through
`herdr-teardown.sh close-tab` (the existing close-then-verify SoT — it deliberately
does not re-issue the close, since a recycled tab id would kill an unrelated live
tab). Ambiguous outcomes — readiness timeout, an unclassifiable herdr error, a
tab). Ambiguous outcomes — a readiness timeout with no running worker detected
(see the 0.8.2 section below), an unclassifiable herdr error, a
returned pane id that isn't the requested one, a wrapper detected as the wrong kind
— leave the tab alone and return the new fail-closed `blocked=unverified` result
(exit 0, `blocked` FIRST). An *unconfirmed* rollback downgrades to the same result:
Expand All @@ -185,6 +186,48 @@ decisions from the fix:
"the verified worker is in its dedicated tab", and changing the key would break
every caller for a cosmetic gain.

### herdr 0.8.2: `agent start` waits for an IDLE agent (work-system 1.28.2)

From 0.8.2, `agent start` succeeds only once the agent is *ready for input* — idle.
A claude worker handed `/work-system:continue` starts working immediately and stays
busy for the whole task, so every native `claude:*` start ran into its `--timeout`
(`code=timeout`) while the worker ran fine, and herdr then **never registers the
requested name** — not even once the agent later goes idle (`agent get <label>` →
`agent_not_found`). Measured live: `pane get` reports `agent=claude` 0.6s in, the
start times out at 10.4s; a start whose turn finished in 16s returned at 17s. The
pane-run path (cc-harness, kimi) never hit this: it confirms by detection, not by
`agent start`'s readiness.

- **A `timeout` — that code only — is confirmed from process state.** A bounded poll
(`WORK_SYSTEM_HERDR_CONFIRM_SECONDS`, 10s) of `pane get`'s `agent` + `agent_status`
in that exact pane: the expected kind while `working`/`idle`/`done` → `moved=yes`;
`blocked` (held at a prompt), a different kind or nothing → `blocked=unverified` as
before. `unknown` is herdr's first seconds of detection and is polled through.
Nothing is retried. A timeout is genuinely two-sided: live, a 3s start into a pane
whose shell was still busy typed **nothing** and left a bare prompt.
- **Then name it**: `agent rename <pane> <label>` (idempotent, so bounded unlike the
launch mutations). Best-effort — `agent_name_taken` (a stale tab holding the label)
is reported on stderr, the launch stays confirmed.
- **Not every startup error is the busy case.** `agent_not_ready` ("blocked during
startup") is CC's trust dialog in an untrusted cwd — the agent is stuck, not
working, so it stays ambiguous → `unverified`.
- herdr's own error line is withheld on a confirmed launch and printed only when
confirmation fails.

### Gotcha: a killed probe shifts a sequenced test stub

`test_herdr_launch.py`'s herdr stub answers the Nth call to a subcommand with the
Nth canned reply, counting on ENTRY. A bounded read that a loaded runner stalls past
its bound is killed after it was counted, so every later answer shifts by one — the
suite then failed on a different assertion each run (`stops immediately` sees 2
polls; a ready→busy shell reads busy at once). Measured: a single 2s `pane get` under
a 1s bound turns 1 poll into 2. Hence every time bound is a seam
(`WORK_SYSTEM_HERDR_PROBE_SECONDS`, `WORK_SYSTEM_AGENT_PROBE_SECONDS`,
`HA_CALL_TIMEOUT_SECS`) pinned to 120s in `Env.__init__`, together with every poll
window (`…_CONFIRM/DETECT/READY/ALIVE/BUSY_SECONDS`): a read that stalls without
being killed would otherwise end a window after one poll. Production stays 10s. A
new bounded call or window needs the same seam, or the flake returns.

### Gotcha: a supervisor cannot take its signal from terminal text

Deciding "did this worker die?" from what the pane SHOWS was attempted three times
Expand Down
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,10 @@ entries are grouped per plugin, newest first.

## work-system

### 1.28.2 — 2026-10-26
- `herdr-launch.sh launch` confirms a native `claude:*` worker whose `agent start` hit herdr's readiness `timeout`: herdr 0.8.2 waits for an idle agent, and a worker handed `/work-system:continue` is busy from the start, so every such kickoff ended `blocked=unverified` while it ran. On that error code only, a bounded poll (`WORK_SYSTEM_HERDR_CONFIRM_SECONDS`, 10 s) for the expected kind in the pane, in a running state (`working`/`idle`/`done`), turns it into `moved=yes`; a worker held at a prompt (`blocked`), nothing or a different kind stays `unverified`, and nothing is retried. The confirmed worker then gets the herdr agent name the timed-out start never registered (`agent rename`, best-effort), and herdr's timeout error is printed only when the confirmation fails.
- Test seams for the probe time bounds (`WORK_SYSTEM_HERDR_PROBE_SECONDS`, `WORK_SYSTEM_AGENT_PROBE_SECONDS`; production default unchanged at 10 s), pinned with `HA_CALL_TIMEOUT_SECS` and every poll window in `test_herdr_launch.py`. A stalled call killed by the bound had already advanced the herdr stub's per-call sequence, which is why the test failed on a different assertion each loaded run.

### 1.28.1 — 2026-10-22
- The merge gate's lane diff id hashes only the lane's own lines: a `-U0` diff, whose place is the file, the hunk order and git's function context. A rebase over main edits next to a lane hunk keeps `covered=yes` (#89 asked to re-review lines the lane never touched). A lane line that changes or moves (under another header, or past another lane hunk) still flips it. A main edit to the hunk's function-context line still asks for a review. Accepted residuals: a lane line moved within one function past no other lane hunk keeps the id, and so does a main edit that changes what a lane hunk means (deleting a guard right above it). Reviews recorded by 1.28.0 (`diff_id_form=context`) or earlier (`legacy`; `context` when no manifest is touched, the same text) keep matching their unchanged lane, also under a non-default diff config, and `delta` still shows context. `delta`'s `diff_form=masked` is now `context-free` (or `context`/`legacy` for an older record).
- `mandate.sh round` (and the other record writers) no longer exit 4 when a concurrent booker releases the lock between a failed `mkdir` and the check: only a `mkdir` error other than "File exists" is a lock that cannot be created. The lock's pid is written atomically, so a partial pid never marks a live lock stale, and a file or symlink at the lock path exits 4 at once with a hint instead of waiting 5 s and blaming another process.
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. 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.28.1",
"version": "1.28.2",
"author": {
"name": "gering"
},
Expand Down
4 changes: 4 additions & 0 deletions plugins/work-system/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -596,6 +596,10 @@ keeps the original branch name rather than renaming it to `task/<name>`:
a bounded window. On **0.7.0–0.7.4** the old `agent start --workspace --cwd -- …`
plus `pane move --new-tab` sequence is kept unchanged. If neither contract is
recognizable, nothing is created and you get the manual command instead.
- On herdr **0.8.2**, `agent start` only returns once the agent is idle, so a
claude worker already busy with `/work-system:continue` runs into herdr's startup
timeout. The launcher then confirms the worker from herdr's own detection (the
expected kind, running, in that pane) and gives it the tab's agent name.
- If a launch can't be **confirmed** (the worker never became detectable, an
unclassifiable herdr error, a pane-id mismatch), the tab is left in place and the
skill tells you to look at it rather than launching a second worker onto the same
Expand Down
11 changes: 7 additions & 4 deletions plugins/work-system/scripts/agent-registry.sh
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,8 @@
# WORK_SYSTEM_CC_HARNESS_AGENTS path to the optional cc-harness-agents
# helper; default: `cc-harness-agents` on PATH.
# Point it at a stub to test without PATH munging.
# WORK_SYSTEM_AGENT_PROBE_SECONDS time bound of each CLI availability probe
# (default 10); a test seam for loaded runners.
# No global state and no shipped fallback — a repo with no default gets the
# picker instead.
#
Expand Down Expand Up @@ -346,7 +348,7 @@ harness_list_raw() {
# from "configured but this provider is logged out". pipefail is not POSIX, so
# dash-as-sh cannot express this.
set +e
run_bounded 10 bash -c 'set -o pipefail; "$1" list 2>/dev/null | head -c "$2"' _ \
run_bounded "$PROBE_SECONDS" bash -c 'set -o pipefail; "$1" list 2>/dev/null | head -c "$2"' _ \
"$HARNESS_BIN" "$HARNESS_MAX_BYTES" >"$tmp"
rc=$?
set -e
Expand Down Expand Up @@ -693,6 +695,7 @@ row_for_selector() {
# lib-bounded.sh (herdr-launch.sh sources the same file): two private copies had
# already drifted apart, and a sibling-script dependency is the norm here anyway.
_LIB_BOUNDED="$SCRIPT_DIR/lib-bounded.sh"
PROBE_SECONDS="${WORK_SYSTEM_AGENT_PROBE_SECONDS:-10}"
# shellcheck source=lib-bounded.sh
. "$_LIB_BOUNDED" 2>/dev/null || {
echo "agent-registry.sh: cannot source $_LIB_BOUNDED (it ships alongside this script)" >&2
Expand All @@ -717,7 +720,7 @@ _GROK_RAW_RC=0
grok_models_load() {
[ -n "$_GROK_RAW_LOADED" ] && return 0
_GROK_RAW_LOADED=1
_GROK_RAW="$(run_bounded 10 grok models 2>/dev/null)" || _GROK_RAW_RC=$?
_GROK_RAW="$(run_bounded "$PROBE_SECONDS" grok models 2>/dev/null)" || _GROK_RAW_RC=$?
}
grok_models_raw() {
grok_models_load
Expand Down Expand Up @@ -767,7 +770,7 @@ grok_resolve_latest() {
# model alias against the raw JSON rather than parsing it — no jq/python
# dependency, and a reshaped config document can't yield a wrong token.
kimi_models_raw() {
run_bounded 10 kimi provider list --json 2>/dev/null
run_bounded "$PROBE_SECONDS" kimi provider list --json 2>/dev/null
}

entry_status() {
Expand All @@ -787,7 +790,7 @@ entry_status() {
local crc=0
if ! command -v codex >/dev/null 2>&1; then note="not installed"
else
run_bounded 10 codex login status >/dev/null 2>&1 || crc=$?
run_bounded "$PROBE_SECONDS" codex login status >/dev/null 2>&1 || crc=$?
if [ "$crc" -eq 0 ]; then avail=yes
elif [ "$crc" -eq 124 ]; then avail=yes; note="codex login status timed out — availability assumed"
else note="run: codex login"; fi
Expand Down
Loading
Loading