Skip to content

Automate /close herdr tab teardown - #18

Merged
gering merged 3 commits into
mainfrom
task/automate-close-in-herdr
Jun 24, 2026
Merged

Automate /close herdr tab teardown#18
gering merged 3 commits into
mainfrom
task/automate-close-in-herdr

Conversation

@gering

@gering gering commented Jun 24, 2026

Copy link
Copy Markdown
Owner

Summary

  • Inside a herdr session, /close now tears down the finished task's herdr tab on top of the usual worktree/branch/task-file cleanup. Outside herdr it is unchanged.
  • Companion to the merged /kickoff herdr automation (Automate /kickoff worktree launch inside herdr #17): kickoff opens the tab, close tears it down — same HERDR_* env + cwd-matching conventions.
  • All herdr logic lives in one tested helper (scripts/herdr-teardown.sh) plus a plugin-shipped SessionEnd hook; the SKILL.md stays thin.

Changes

  • skills/close/SKILL.md — step 7 captures the worktree tab by cwd before removal (+ a SELF discriminator); new step 12 runs Scenario A (close the worktree tab directly from the main session) or Scenario B (self-close from inside the tab).
  • scripts/herdr-teardown.sh (new) — worktree-tab/main-tab (cwd lookup), close-tab/focus-tab, inject-exit, self-exit (detached delayed /exit), arm-self-close/on-session-end (marker + hook handler).
  • hooks/hooks.json (new) — conditional SessionEnd hook closing only a per-pane-marked tab; ships with the plugin (no settings.json injection).
  • READMEs + knowledge — herdr section extended; features/herdr-close-automation.md curated.
  • Version — work-system 1.4.0 → 1.4.1 (plugin.json + marketplace.json).

Readiness

  • ✅ README freshness — root + work-system READMEs updated
  • ✅ Version bumped to 1.4.1 (plugin.json + marketplace.json in sync)
  • ➖ Changelog — N/A (repo has none)
  • ✅ Knowledge — herdr-close-automation.md + index curated
  • ✅ Structure check — check-structure.py: 0 errors
  • ➖ Lint / Build — N/A (declarative markdown plugins)

Test plan

  • Live-verified (workspace w6, throwaway tabs): send-text "/exit" + Return cleanly exits Claude's TUI; pane run "/exit" and send-keys ctrl+d do not; a root-pane clean exit auto-closes the tab; detached self-exit exits the idle session after the turn.
  • End-to-end SessionEnd hook firing is provable only after installing work-system 1.4.1 (cache currently has 1.3.1 without hooks.json); the hook handler is unit-tested.
  • /close from the main session closes a different worktree tab (Scenario A).
  • /close outside herdr behaves exactly as before.

🤖 Generated with Claude Code

gering and others added 3 commits June 24, 2026 15:33
- Inside a herdr session, /close now closes the finished task's herdr tab
  on top of the worktree/branch/task-file cleanup; outside herdr it is
  unchanged. The tab is found by cwd before the worktree is removed.
- Scenario A (run from the main session): close the worktree tab directly.
- Scenario B (run from inside the worktree tab): focus the main tab, then
  self-exit via a detached, delayed /exit that lands on the idle prompt
  after the turn ends, so Claude exits cleanly and its root-pane tab
  auto-closes. Falls back to a Ctrl+D prompt when injection is unavailable.
- Ship a conditional SessionEnd hook (hooks/hooks.json) that closes only a
  per-pane-marked tab, as the backup for shell-hosted sessions.
- Keep all herdr logic in the tested scripts/herdr-teardown.sh.
- Verified live: `pane run "/exit"` and `send-keys ctrl+d` do not exit
  Claude's TUI; `send-text "/exit"` + Return does, and a root-pane exit
  auto-closes the tab.
- Bump work-system to 1.4.1; document in the work-system + root READMEs and
  curate herdr-close-automation knowledge.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TnbQUD8guQaB8UoX52iwiK
Address the xhigh code-review findings on the Scenario-B self-close path:

- Decide self-close by pane id (own-tab), not the possibly-empty $HERDR_TAB_ID,
  so an unset tab id can no longer misclassify a self-close into a mid-turn
  `close-tab` against the live session's own tab. If the own tab can't be
  resolved, skip the auto teardown instead of guessing.
- Match the worktree/main cwd by realpath on both sides, so a symlinked path
  (macOS /tmp→/private/tmp, symlinked /Users) no longer silently skips teardown.
- main-tab excludes the dying tab from its --first fallback, so focus never
  lands on the tab that is about to self-close.
- self-exit polls the pane until it leaves `working` (turn ended) before
  injecting, replacing the fixed 4s timer that could fire /exit mid-turn; it
  passes args positionally to an internal handler (no bash -c double-eval).
- inject-exit dismisses the slash-command menu if Return fails after the text
  was typed, so a half-delivered exit doesn't leave the TUI stuck.
- Marker: fixed $HOME/.cache (not $XDG_CACHE_HOME, which can diverge between the
  /close shell and the hook env), sanitized pane id, and a timestamp + TTL so a
  stale marker is dropped without closing an unrelated reused tab.
- check-structure.py also scans hooks/*.json for ${CLAUDE_PLUGIN_ROOT} refs, so
  renaming the script can't break the SessionEnd hook with CI still green.
- Gate step 12 on completed cleanup; fix the README "no state file" wording;
  refresh the knowledge entry.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TnbQUD8guQaB8UoX52iwiK
Tighten the Scenario-B self-close path against the failure modes the first
hardening pass introduced or left open:

- __delayed-inject now injects ONLY on a confirmed idle/done status. A transient
  `herdr pane list` failure yields empty output, which is retried, not mistaken
  for idle (which would have injected /exit mid-turn — the exact invariant the
  path promises). A vanished pane or a never-idle timeout injects nothing.
- extract_tab no longer false-matches: an empty/whitespace target (realpath("")
  resolves to the process cwd → would match the main-repo pane) never matches,
  and an all-slashes path stays root instead of collapsing to "". lookup_tab
  rejects an empty target. --exclude is honored on the primary cwd match too, so
  main-tab can't return the dying tab when its cwd matches the main repo.
- on-session-end closes only a verifiably fresh marker: a stamp/now of 0 (date
  unavailable) or a negative/over-TTL age is dropped WITHOUT closing, instead of
  being treated as permanently fresh. Close now runs before the marker rm, with
  one retry, so a transient hiccup doesn't silently leak the tab.
- marker_dir fails closed when $HOME is empty; empty-pid guards in the pane
  matchers; arm-self-close creates the marker dir from the resolved path.
- /close reports when it's in herdr but couldn't locate the tab by cwd, instead
  of silently implying the tab was handled.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TnbQUD8guQaB8UoX52iwiK
@gering
gering merged commit 8ef27f3 into main Jun 24, 2026
1 check passed
@gering
gering deleted the task/automate-close-in-herdr branch June 24, 2026 17:09
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
Swarm loop round 3 — 3 findings, all in the probe. Rather than patch a probe
that had a security jail bolted on (rounds 1-3 all fixed jail-adjacent bugs),
this removes the jail: a readiness check passes no untrusted diff, so it never
needed one — the sibling `codex login status` in ready_check is unjailed too.

- Run `grok models` directly, not through sandboxed(). This roots out the
  round-3 CRITICAL (#18): the jail's `_init_sandbox` builds its deny profile
  with python3, so routing the probe through it made python3 a hard dependency
  of the formerly-local `ready`/`list` paths — and a missing python3 then
  misreported as "grok models failed (rc=65)". No jail, no python3 dep, honest
  reasons.
- Bound with `timeout -k 3` (#17): plain `timeout` only SIGTERMs, so a grok
  that ignores SIGTERM (or forks a stdout-inheriting child) kept the command
  substitution blocking past the deadline — the "must never hang" hole. `-k`
  SIGKILLs after a grace period. rc 124 (SIGTERM at the deadline) reports as
  "timed out"; rc 137 (SIGKILL) reports as "killed" — almost always our own
  `-k`, but an OOM/external kill shares the code, so the message doesn't assert
  a timeout that may not have happened.
- Take only the FIRST id per bullet line (#19): scanning the whole line also
  matched a grok-4.5 mentioned in prose on another model's line
  ("* grok-5 (successor to grok-4.5)"), reporting a retired model as offered.
- Fold _build_jail back into sandboxed(). It existed ONLY to share the jail
  with the probe; with the probe unjailed, the split — and the shared-warning
  (#12) and per-backend-memo (#15) fixes it forced last round — are moot.
  sandboxed() is back to its pre-split shape; _init_sandbox keeps the backend
  key (harmless, strictly more correct).
- Fix a stale swarm-review.js input-contract comment ("subset of the three" →
  codex, grok).
- Docs (header/README/CHANGELOG/knowledge) drop the jail claim. The knowledge
  entry records the real lesson: five rounds of patching a jailed probe ended
  the moment the jail was deleted — a feature added to be safe that generates
  every round's bugs is a shape problem; cut it, don't harden it. The composer
  removal (the PR's subject) drew zero findings across all five rounds.

Verified: live grok ready; SIGTERM-ignoring grok bounded by -k in ~5s, reported
as killed; SIGTERM-obeying grok reported as timed out; model-gone → honest hint,
no warning; probe rc=1 → warn + trust-auth; NO python3 invoked on the ready
path; review path still fully jailed (sandbox-exec + AWS/GH secret stripping);
end-to-end review returns a schema-valid finding.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
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