Skip to content

Harden herdr /close teardown against silent orphan tabs - #20

Merged
gering merged 5 commits into
mainfrom
task/fix-close-herdr-orphan-tab
Jun 30, 2026
Merged

Harden herdr /close teardown against silent orphan tabs#20
gering merged 5 commits into
mainfrom
task/fix-close-herdr-orphan-tab

Conversation

@gering

@gering gering commented Jun 30, 2026

Copy link
Copy Markdown
Owner

Summary

  • Fixes /close leaving an idle orphan herdr tab (observed closing archive-tasks-on-close, PR Archive task file on /close instead of deleting it #19): the task's tab stayed open and idle even with the 1.4.1+ teardown active.
  • Root cause pinned to Scenario B (close run from inside the worktree tab): the session went idle but /exit was never delivered — the old 30s poll window timing out, a finished status settling to unknown, or a dropped /exit.
  • Diagnosis verified empirically in a live herdr session: a nohup … & disown poller survives past the Bash-tool boundary (so the detached mechanism is sound), and herdr's agent_status is one of idle|working|done|unknown (the injector's idle|done targets do exist).
  • Durable principle: a teardown must verify its own effect or name the target for a manual fallback — never report a close it didn't observe.

Changes

  • herdr-teardown.sh: new tab_status/tab-status helper (present|gone|unverified); close-tab now closes and re-checks the tab is gone (one retry) and prints closed|still-open|unverified; __delayed-inject poll window raised 30s→120s plus a post-inject confirm with one re-inject.
  • close/SKILL.md step 12: Scenario A branches on the close-tab result and names the tab on failure; Scenario B always appends an explicit "close by hand: <tab>" fallback line (a self-close can't be confirmed in-turn).
  • README + knowledge (herdr-close-automation.md) updated to document the verify-or-name behavior and the empirical findings.
  • Bump work-system 1.5.0 → 1.5.1 (plugin.json + marketplace.json).

Readiness

  • ✅ Structure checks (check-structure.py): 0 errors
  • ✅ Shell syntax (bash -n) clean
  • ✅ Version bumped & in sync (1.5.1)
  • ✅ Knowledge + README updated
  • ➖ Changelog (none in repo), build/test manifests (declarative plugin repo)
  • Live-validated: tab-status + full close-tab path on a throwaway tab (present → closed → gone)

Test plan

  • Scenario A: /close a merged task from the main herdr session → tab closes; if it can't, an explicit "close by hand: <tab>" line appears
  • Scenario B: /close from inside the worktree tab → session exits cleanly and the tab auto-closes; the fallback line names the tab in case it lingers
  • Non-herdr /close is unaffected

🤖 Generated with Claude Code

gering and others added 5 commits June 30, 2026 10:17
close-tab now closes AND verifies the tab is gone (one retry), printing
closed|still-open|unverified via a new tab_status/tab-status helper, so a
close that silently didn't take is never reported as success.

The self-exit poller window is raised 30s->120s (a closing turn that
archives+commits+pushes can outlast the old guess, and a timeout injects
nothing -> an idle orphan), and after injecting /exit on idle it now
confirms the pane vanished and re-injects once.

close/SKILL.md step 12: Scenario A branches on the close-tab result and
names the tab on failure; Scenario B always appends an explicit
'close by hand: <tab>' line, since a self-close fires asynchronously
after the turn and cannot be confirmed in-turn.

Bump work-system 1.5.0 -> 1.5.1; update herdr-close-automation knowledge.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HSMdpBst3jMCb47Nuq4ueg
- close-tab: drop require_herdr (exit 1 + empty stdout broke its own
  'always exit 0 / closed|still-open|unverified' contract, leaving a silent
  orphan when herdr is unresolvable at step 12); herdr-absent now yields
  'unverified'. Poll the status a few times (re-close while present, retry on
  transient 'unverified') instead of a single 0.3s retry to cut false alarms.
- self-exit poller: drop the speculative second /exit re-injection — it can't
  distinguish a dropped first /exit from a user who reopened the tab and is
  momentarily idle, and would kill that live session. Inject once; the
  always-printed manual-close line + SessionEnd hook are the backups.
- close/SKILL.md: fix the step-11 summary advertising status words the script
  never emits; resolve the duplicated Ctrl+D directive (B-hook now names the
  tab; item 5 is scoped to the B-inject path).
- Sync the herdr-close-automation knowledge entry.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HSMdpBst3jMCb47Nuq4ueg
- extract_tab_present: a valid-but-empty panes array now reads 'unverified',
  not 'gone' — a transiently empty list (e.g. just after a herdr restart) would
  otherwise let close-tab falsely report a live tab as 'closed' (the silent
  orphan this feature prevents). Verified with a fake herdr.
- close-tab: short-circuit to 'unverified' when herdr/python3 is missing instead
  of spinning the verify loop ~1.2s on a condition that can't change; re-read the
  status once after the loop so a close landing on the final iteration is
  verified, not misreported as 'still-open'.
- Remove the dead 'tab-status' subcommand (no production caller; it was a third
  copy of the present|gone|unverified vocabulary) — the internal tab_status
  helper that close-tab uses stays.
- close/SKILL.md: fix 'one retry' prose to match the actual poll-a-few-times loop.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HSMdpBst3jMCb47Nuq4ueg
- extract_status: apply the same empty-but-valid panes guard the round-2 fix
  added to extract_tab_present. The __delayed-inject poller used extract_status,
  which still returned __gone__ for a transiently empty list and bailed without
  injecting /exit — re-creating the silent orphan on the path the wider window was
  meant to protect. Now __gone__ fires only for a POPULATED list missing the pane;
  an empty list prints nothing so the poller keeps polling.
- close-tab: close ONCE then poll until gone, instead of re-issuing the close on
  every 'present' read — a re-issue could land on a tab id herdr recycled onto a
  new tab and kill it. A genuinely-failed close still surfaces as 'still-open' for
  the manual-close fallback. Drops the off-by-one final re-read too.
- extract_tab_present: str()-coerce tab_id so a numeric id from herdr still matches
  (avoids a false 'gone' -> false 'closed').
- README: distinguish 'still-open' (close not taken) from 'unverified' (couldn't
  re-query) instead of collapsing both into 'didn't take'.
- Sync the herdr-close-automation knowledge entry.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HSMdpBst3jMCb47Nuq4ueg
- Revert the speculative str() tab_id coercion added last round: herdr ids are
  'wN:tM'/'wN:pM' strings (never numeric), so no snippet needs it — and coercing
  only extract_tab_present left it inconsistent with extract_tab's --exclude
  compare. Document that all id compares are as-is.
- Fix doc drift: the script header and close/SKILL.md still described close-tab as
  're-issuing the close while still present', but it deliberately closes once and
  polls. Match the shipped behavior.
- close-tab: skip the sleep after the final status read (its result is the one
  reported), so the verify loop does 5 reads / 4 waits instead of a wasted trailing
  0.3s.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HSMdpBst3jMCb47Nuq4ueg
@gering
gering merged commit 9bed9e1 into main Jun 30, 2026
1 check passed
@gering
gering deleted the task/fix-close-herdr-orphan-tab branch June 30, 2026 15:34
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