Skip to content

Add /continue reopen mode to recover /exit-closed herdr tabs - #22

Merged
gering merged 10 commits into
mainfrom
task/continue-reopen-herdr
Jul 2, 2026
Merged

Add /continue reopen mode to recover /exit-closed herdr tabs#22
gering merged 10 commits into
mainfrom
task/continue-reopen-herdr

Conversation

@gering

@gering gering commented Jul 2, 2026

Copy link
Copy Markdown
Owner

Summary

  • A kickoff tab runs Claude as its root pane, so a bare /exit (even one only meant to restart Claude Code) closes the whole tab — the worktree and resumable session persist, but you lose your place.
  • Adds a reopen path: /continue <task> from the main session reopens the task's herdr tab at its worktree and resumes the existing Claude session (claude -c), then focuses it.
  • Reopened tabs are hardened against /exit: Claude runs inside a shell pane, so a later /exit drops back to the shell and the tab survives.

Changes

  • scripts/herdr-launch.sh — split into two subcommands sharing one precondition/parse/output contract: launch (kickoff, argv-exec, root-pane, unchanged) and resume (tab create + pane run "claude -c" in a shell pane, then tab focus). Same pane=/tab=/moved= contract.
  • skills/continue/SKILL.md — branch by location: inside a worktree = in-session resume (unchanged); main session + <task> = reopen via the helper; outside herdr = manual cd <worktree> && claude -c block. Description updated for the new mode.
  • skills/kickoff/SKILL.md — call the helper's new launch subcommand.
  • Design decisions: keep kickoff argv-exec (race-freedom is verified, and /close's teardown depends on the root-pane model); resume uses claude -c (worktree cwd is a 1:1 proxy for the task session, so no session-id stash is needed). A race-free prevention variant is documented as deferred pending live herdr verification.
  • Docs: bump work-system 1.5.1 → 1.6.0; update root + plugin READMEs and the herdr knowledge entry (+ index).

Readiness

  • ✅ README freshness — root + plugin README updated
  • ✅ Version bump — 1.6.0 (plugin.json + marketplace.json in sync)
  • ➖ Changelog — N/A (no changelog in repo)
  • ✅ Knowledge — herdr entry + index updated
  • ✅ Structure check — 0 errors (bash -n, JSON, version sync, refs, description budget)
  • ✅ Rebase — 0 commits behind main

Test plan

  • Inside herdr: /exit a kickoff tab, then /continue <task> from the main session → tab reopens at the worktree, session resumes, tab is focused.
  • In the reopened tab, /exit again → drops to the shell, tab survives.
  • Outside herdr: /continue <task> prints the manual cd <worktree> && claude -c block.
  • Inside a worktree: /continue still does in-session resume unchanged.

🤖 Generated with Claude Code

gering and others added 10 commits July 2, 2026 14:06
A kickoff tab runs Claude as its root pane, so a bare /exit (even to
just restart Claude Code) closes the whole tab. Recover it with
`/continue <task>` from the main session.

- herdr-launch.sh: split into `launch` (kickoff, argv-exec, unchanged)
  and `resume` (tab create + `pane run "claude -c"` inside a SHELL pane,
  then focus). Same pane=/tab=/moved= contract. Because resume runs
  Claude in a shell pane, a later /exit drops back to the shell and the
  tab survives.
- continue/SKILL.md: branch by location — worktree = in-session resume
  (unchanged); main session + <task> = reopen via the helper; outside
  herdr = manual `cd <worktree> && claude -c` block. Update description.
- kickoff/SKILL.md: call the helper's new `launch` subcommand.
- Decisions: keep kickoff argv (race-freedom + /close teardown depend on
  root-pane Claude); resume uses `claude -c` (worktree cwd is 1:1 with
  the task session, so no session-id stash needed).
- Bump work-system 1.5.1 -> 1.6.0; update READMEs + knowledge entry.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AQaQpf2Y8hn2aUV6SuLfFF
- Guard against a duplicate session: resume now reuses herdr-teardown.sh's
  worktree-tab (cwd→tab) lookup and, if a tab is already open at the
  worktree, just focuses it (reused=yes) instead of starting a second
  `claude -c` on the same working tree.
- Report resumed=no when the `pane run "claude -c"` send fails, so the
  skill never claims a resume that didn't happen; SKILL branches on
  reused/resumed and softens the success wording (adopted worktrees may
  have no session to resume).
- Route `/continue <other-task>` from inside a worktree to reopen mode for
  that task instead of silently ignoring the argument.
- Parse the tab-create response in one python3 pass (was two spawns).
- Restore the "pick up where I left off" trigger phrase; tighten the
  description to stay within budget.
- Trim the over-long knowledge index line back to a terse pointer.
- Document the resume-vs-/close teardown asymmetry (shell-launched Claude
  may not be a registered agent; /close falls back to its manual-close
  line) as a known, gracefully-degrading limitation.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AQaQpf2Y8hn2aUV6SuLfFF
Address xhigh review findings on the duplicate-session guard added last round:

- Retry the worktree-tab lookup (~3x): it returns empty for both "no tab"
  and a transiently empty `herdr pane list` (e.g. just after a herdr
  restart), so a single empty read failed open and could still duplicate.
- Don't assert a live resume on reuse: a cwd match can't tell a live
  Claude from a bare shell that survived a prior /exit, so the reuse
  branch emits `resumed=` (empty) and the skill tells the user to run
  `claude -c` if the focused tab is just a shell — no false "resumed".
- Parse the tab-create response pipe-delimited (`<pane>|<tab>`) so an
  empty pane id can't be mis-read as the tab id (would have targeted a
  tab id as a pane id).
- Emit `focused=<yes|no>`; skill no longer claims a focus that failed,
  and reorders its branches so an empty `tab=` with resumed=yes still
  gets the manual-focus hint.
- Document the workspace-scoped lookup as an accepted limitation
  (a same-worktree tab in another herdr workspace is out of scope),
  consistent with /kickoff and /close.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AQaQpf2Y8hn2aUV6SuLfFF
Address xhigh round-2 findings; the duplicate-session guard is redesigned
to fail closed instead of open:

- Add `herdr-teardown.sh worktree-tab-state`: a tri-state cwd->tab lookup
  returning <tab>|none|unverified. resume now creates only on `none`
  (populated pane list, no match), FAILS CLOSED on `unverified` (herdr
  unreachable or an empty/repopulating pane list) by exiting 1 so the
  skill shows the manual block — a plain empty-return guard fails open
  and could still duplicate. This also drops the retry-with-sleep, so the
  common no-tab path pays no latency.
- Search all workspaces (empty workspace arg) so a still-live tab for the
  same worktree in a different herdr workspace is found; the new tab is
  still created in $HERDR_WORKSPACE_ID.
- Fix a dispatch regression: inside a worktree, an argument that doesn't
  resolve to another task's worktree (typo/alias) now resumes the current
  task in-session instead of refusing with "no worktree — /kickoff".
- Header said "two keys", lists three (reused/resumed/focused).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AQaQpf2Y8hn2aUV6SuLfFF
Round-3 review returned only PLAUSIBLE findings; two were already-
documented, gracefully-degrading tradeoffs (the shell-startup keystroke
race and reopened tabs not being registered agents). Addressed:

- Remove the duplicated norm()/cwd-match logic: worktree-tab-state now
  reuses the canonical extract_tab matcher plus a trivial
  extract_panes_populated check, so path/realpath matching is defined
  once and the reopen guard and /close's worktree-tab lookup can't drift.
  Guard an empty target as unverified (fail closed).
- Word the reopen success message as "Sent `claude -c`", not "is
  running" — the helper delivers the keystrokes but can't confirm Claude
  came up (shell-startup race); tell the user to re-run it if they land
  on a bare shell.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AQaQpf2Y8hn2aUV6SuLfFF
Round-4 findings centered on the mode-selection prose (which had drifted
across edits). Make it mechanical:

- Normalize the argument through task-status.sh before comparing/looking
  up the worktree, so a `task/`-prefixed or aliased name matches the
  prefix-stripped worktree dir name instead of misrouting to in-session
  resume of the wrong task.
- Compare the resolved arg name to the current worktree's task name first,
  so `/continue <current-task>` from its own worktree resumes in place
  rather than no-op-focusing the tab you're already in.
- Mode-aware usage string: resume no longer advertises [session-name]
  (only launch takes one).
- Knowledge: resume is called from continue's reopen path (main session,
  OR a different task's name from inside a worktree) — not "main-session
  only"; document the exact-cwd match gap (a shell wandered into a subdir
  isn't matched) as an accepted limit of the shared matcher.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AQaQpf2Y8hn2aUV6SuLfFF
Final review-loop round. Fixes the four surviving guard-path findings:

- unverified now surfaces as `blocked=unverified` (exit 0), and the skill
  tells the user to CHECK herdr for an already-open tab before reopening
  by hand. Previously the fail-closed exit fell through to the plain
  manual block ("cd && claude -c") with no cue, which could recreate the
  duplicate the guard exists to prevent.
- Collapse worktree-tab-state into ONE python pass (was two: populated
  check + extract_tab), removing the double parse.
- The single tri-state extractor now fails closed (`unverified`) on a
  malformed/errored pane list AND when a pane sits in a SUBDIRECTORY of
  the worktree (a tab may have wandered in) — both previously read as
  `none` and could duplicate. norm() mirrors extract_tab (kept in sync by
  comment) since /close's exact-match matcher must not inherit the
  subtree/tri-state behavior.

Unit-tested the extractor across exact/none/subtree/empty/malformed/
sibling cwd cases.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AQaQpf2Y8hn2aUV6SuLfFF
Round 6 verified the round-5 fixes and caught a regression they
introduced. Corrections:

- Revert the subtree-cwd matching: it was deterministically failing
  closed whenever ANY unrelated pane sat in a worktree subdir (e.g. a
  shell in <worktree>/logs), permanently blocking auto-reopen. Back to
  exact-match; the wandered-own-tab gap it targeted is re-documented as
  an accepted narrow limitation.
- Fail closed on a tab pane whose cwd is empty/unreadable: it was being
  skipped and falling through to `none` (fail open -> possible duplicate
  claude -c). Now yields `unverified`.
- blocked outcome now prints a lone `blocked=unverified`, matching the
  header contract (was three keys).
- Factor norm() into a shared prelude string concatenated into both
  extract_tab and extract_tab_state, so path normalization is defined
  once (the two can no longer drift).
- Document that reopening a different task from inside a worktree can
  place its tab in the current workspace, which a later /close may not
  locate (falls back to its manual-close line; no data loss).

Unit-tested both extractors across exact/none/subtree/empty/null/
non-string/no-tab cwd cases.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AQaQpf2Y8hn2aUV6SuLfFF
Round 7 (verification of round 6). Its top finding was self-inflicted:
the round-6 subtree revert changed the code but left comments claiming
subdir -> unverified. Fixes:

- Sweep the stale subtree=fail-closed comments (herdr-launch header +
  teardown) to match the code: subdir cwd -> `none` (accepted narrow
  gap), documented as such.
- extract_tab_state: check cwd BEFORE tab_id, so a worktree-cwd pane
  momentarily missing its tab_id fails closed (`unverified`) instead of
  being skipped -> `none` (a fail-open duplicate risk).
- resume sends `cd <worktree> && claude -c` (shell-quoted): the pane is
  created with --cwd, but an rc that drifts the cwd (direnv/zoxide) would
  make `claude -c` resume the wrong task's session. Re-anchor first.
- Factor the herdr-down/empty-list guard chain into a `pane_query`
  helper shared by tab_status() and worktree-tab-state (were duplicated).
- /continue reopen step 1 reuses the ARG_TASK the dispatcher already
  resolved instead of re-running task-status.sh.
- Correct the "all workspaces ... globally unique" overclaim: the search
  spans the current herdr SERVER only (a separate server is invisible);
  document that and the unreadable-cwd over-block as accepted fail-safe.

Re-unit-tested both extractors (8 + 3 cases); structure check green.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AQaQpf2Y8hn2aUV6SuLfFF
- continue/SKILL.md: correct the blocked=unverified cause list — a subdir
  pane now yields `none` (creates), not `unverified` (a stale claim the R7
  sweep missed in the skill, matching the exact-match/subtree-revert design)
- continue/SKILL.md: the resumed=yes template no longer tells a never-hosted
  worktree to re-run `claude -c` (which errors); start fresh `claude` instead
- continue/SKILL.md: on reuse focused=no, cue reopening by hand if the tab
  closed between snapshot and focus (no auto-create — that risks a duplicate)
- continue/SKILL.md: defer MAIN_REPO off the hot in-session-resume path;
  resolve it only in the linked+switch branch that needs it
- herdr-launch.sh: close an orphaned tab (parsed tab id, no pane id) before
  bailing, so a schema-drift response can't leak a blank tab on every resume
- Knowledge: note the unscoped-pane-list-spans-workspaces assumption + the
  orphan-close; bash -n + structure check green

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AQaQpf2Y8hn2aUV6SuLfFF
@gering
gering merged commit 8a7e1a5 into main Jul 2, 2026
1 check passed
@gering
gering deleted the task/continue-reopen-herdr branch July 2, 2026 20:45
gering added a commit that referenced this pull request Jul 16, 2026
Second dogfooding round at the deepest profile (/swarm:review --max --fix:
per-lens split live — gate pruned style, 10 xhigh finders + codex
gpt-5.6-sol@xhigh + grok-4.5 + composer; 24 findings, 10 consensus).
Fixes the agreed findings:

- Never auto-accept an all-untagged consensus cluster: no tagged lens
  backs it, so it is verified like a solo. Verify/auto-accept now derive
  from ONE needsVerify predicate (structural exactly-once partition);
  kind vote in clearer every-form (#1, consensus critical; #21, #22)
- Design verifier sees the finding's recommendation — the proposal the
  applicability rubric actually tests — and carries an escape hatch:
  a genuine defect mis-filed under a design lens is not refuted away
  (#12, #3); "solo" dropped from verifier prompts (#4)
- Untagged findings from multi-lens cluster finders fall back to
  'unspecified' (safe defect bucket), not lenses[0] (#7); merge-agent
  free-text lens validated, majority-member fallback (#9)
- Improvement invitation scoped to design finder units — defect-lens
  finders stay defect-only (#8); merge prompt clusters by issue, not
  only defect (#13); schema descriptions generalized for design
  findings (#14)
- LENS_BRIEF startup assertion (#6); gate prompt interpolates
  LENS_CLUSTERS.design (#19); new test_lens_sync.py guards all lens
  mirrors: SKILL HDR prompt, LENS_BRIEF, pr-post DESIGN_LENSES (#5)
- Workflow assigns stable finding num (defects first, shared sequence);
  presenter/pr-post render it verbatim (#20)
- pr-post.py: design lens is the backup kind signal when the handoff
  drops kind (explicit defect still wins); single-pass partition (#17, #24)
- Balance: REFUTED is its own segment (refuted ⊄ solo since design
  consensus can be refuted) (#2); LOCKED design-table column precedence
  in --loop rounds clarified (#11); "verifies solos" doc sweep across
  manifests/README/knowledge (#4); cluster failure-isolation trade-off
  documented (#10); knowledge index line trimmed (#15)

Declined: #16 (JS test harness for sandbox code; lens-sync test covers
the drift class), #18 (gate-fail under --max runs all lenses — the
documented never-silently-narrower degrade), #23 (micro-opt vs readability).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0126cGxsBYxqgEFH4AcpriNk
gering added a commit that referenced this pull request Jul 17, 2026
Second dogfooding round at the deepest profile (/swarm:review --max --fix:
per-lens split live — gate pruned style, 10 xhigh finders + codex
gpt-5.6-sol@xhigh + grok-4.5 + composer; 24 findings, 10 consensus).
Fixes the agreed findings:

- Never auto-accept an all-untagged consensus cluster: no tagged lens
  backs it, so it is verified like a solo. Verify/auto-accept now derive
  from ONE needsVerify predicate (structural exactly-once partition);
  kind vote in clearer every-form (#1, consensus critical; #21, #22)
- Design verifier sees the finding's recommendation — the proposal the
  applicability rubric actually tests — and carries an escape hatch:
  a genuine defect mis-filed under a design lens is not refuted away
  (#12, #3); "solo" dropped from verifier prompts (#4)
- Untagged findings from multi-lens cluster finders fall back to
  'unspecified' (safe defect bucket), not lenses[0] (#7); merge-agent
  free-text lens validated, majority-member fallback (#9)
- Improvement invitation scoped to design finder units — defect-lens
  finders stay defect-only (#8); merge prompt clusters by issue, not
  only defect (#13); schema descriptions generalized for design
  findings (#14)
- LENS_BRIEF startup assertion (#6); gate prompt interpolates
  LENS_CLUSTERS.design (#19); new test_lens_sync.py guards all lens
  mirrors: SKILL HDR prompt, LENS_BRIEF, pr-post DESIGN_LENSES (#5)
- Workflow assigns stable finding num (defects first, shared sequence);
  presenter/pr-post render it verbatim (#20)
- pr-post.py: design lens is the backup kind signal when the handoff
  drops kind (explicit defect still wins); single-pass partition (#17, #24)
- Balance: REFUTED is its own segment (refuted ⊄ solo since design
  consensus can be refuted) (#2); LOCKED design-table column precedence
  in --loop rounds clarified (#11); "verifies solos" doc sweep across
  manifests/README/knowledge (#4); cluster failure-isolation trade-off
  documented (#10); knowledge index line trimmed (#15)

Declined: #16 (JS test harness for sandbox code; lens-sync test covers
the drift class), #18 (gate-fail under --max runs all lenses — the
documented never-silently-narrower degrade), #23 (micro-opt vs readability).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0126cGxsBYxqgEFH4AcpriNk
gering added a commit that referenced this pull request Jul 17, 2026
Second dogfooding round at the deepest profile (/swarm:review --max --fix:
per-lens split live — gate pruned style, 10 xhigh finders + codex
gpt-5.6-sol@xhigh + grok-4.5 + composer; 24 findings, 10 consensus).
Fixes the agreed findings:

- Never auto-accept an all-untagged consensus cluster: no tagged lens
  backs it, so it is verified like a solo. Verify/auto-accept now derive
  from ONE needsVerify predicate (structural exactly-once partition);
  kind vote in clearer every-form (#1, consensus critical; #21, #22)
- Design verifier sees the finding's recommendation — the proposal the
  applicability rubric actually tests — and carries an escape hatch:
  a genuine defect mis-filed under a design lens is not refuted away
  (#12, #3); "solo" dropped from verifier prompts (#4)
- Untagged findings from multi-lens cluster finders fall back to
  'unspecified' (safe defect bucket), not lenses[0] (#7); merge-agent
  free-text lens validated, majority-member fallback (#9)
- Improvement invitation scoped to design finder units — defect-lens
  finders stay defect-only (#8); merge prompt clusters by issue, not
  only defect (#13); schema descriptions generalized for design
  findings (#14)
- LENS_BRIEF startup assertion (#6); gate prompt interpolates
  LENS_CLUSTERS.design (#19); new test_lens_sync.py guards all lens
  mirrors: SKILL HDR prompt, LENS_BRIEF, pr-post DESIGN_LENSES (#5)
- Workflow assigns stable finding num (defects first, shared sequence);
  presenter/pr-post render it verbatim (#20)
- pr-post.py: design lens is the backup kind signal when the handoff
  drops kind (explicit defect still wins); single-pass partition (#17, #24)
- Balance: REFUTED is its own segment (refuted ⊄ solo since design
  consensus can be refuted) (#2); LOCKED design-table column precedence
  in --loop rounds clarified (#11); "verifies solos" doc sweep across
  manifests/README/knowledge (#4); cluster failure-isolation trade-off
  documented (#10); knowledge index line trimmed (#15)

Declined: #16 (JS test harness for sandbox code; lens-sync test covers
the drift class), #18 (gate-fail under --max runs all lenses — the
documented never-silently-narrower degrade), #23 (micro-opt vs readability).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0126cGxsBYxqgEFH4AcpriNk
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant