Skip to content

fix(claude): ship launcher scripts and detect 2.1.179 tmux ready prompt - #623

Merged
ivankuznetsov merged 9 commits into
mainfrom
fix-claude-tmux-ready-detector-260629-50cc
Jul 1, 2026
Merged

fix(claude): ship launcher scripts and detect 2.1.179 tmux ready prompt#623
ivankuznetsov merged 9 commits into
mainfrom
fix-claude-tmux-ready-detector-260629-50cc

Conversation

@ivankuznetsov

@ivankuznetsov ivankuznetsov commented Jun 29, 2026

Copy link
Copy Markdown
Owner

Summary

On a clean release-gem install with Claude Code 2.1.179, hive run in Claude tmux mode never reached WAITING — it died at launch with claude_launch_failed / "can't find pane". Two independent defects had to line up for a launch to work, and both were broken.

First, the gem shipped no shell scripts. spec.files globbed lib/**/*.rb only, so the wrappers ClaudeLauncher shells out to (interactive_claude_wrapper.sh, stop_hook.sh) were absent from the built gem. bash <missing-file> exits instantly, the tmux session collapses, and Hive reports the launch failure. Second, even with the wrapper present, the 2.1.179 idle prompt was not recognized: that build renders a separator / caret / separator / footer box and paints a non-breaking space around the caret, which the old detector — a shallow 2-line tail scan tolerating only an ASCII space — read as "not ready" forever.

After this change a release build packages the scripts and the detector recognizes the 2.1.179 prompt (and tolerates future TUI tweaks) without misreading trust, permission, or menu states as ready.

What changed

Area Change
Packaging hive.gemspec now globs lib/hive/scripts/**/*.sh so every script ClaudeLauncher invokes ships in the gem.
Packaging guard New built-gem integration test enumerates the script references in claude_launcher.rb / stop_hook_installer.rb, builds the real .gem, and fails if any referenced script is missing — a future script reference can't silently drop out.
Detector Ready detection now scans the current input region (12 lines), tolerates \p{Zs} (incl. NBSP) on both sides of the caret, and accepts a caret only when every line below it is terminal chrome (box/separator/footer/blank) rather than assuming a fixed footer distance.
Tests Regression cases for the 2.1.179 NBSP/separator shape and the prior caret shapes; rejection cases for permission, trust, and numbered-menu states plus carets with real output below them.

Design notes

  • Chrome-anchored, not distance-anchored. Widening the scan window alone would have flipped two existing rejection guards green-to-red (a caret with build output or scrollback below it would read as ready). Instead the detector finds the last caret in the window and accepts it only when everything beneath it is footer/separator/blank. This drops the fragile "caret within last N lines" rule while keeping those rejections intact.
  • Negative guards stay first and unchanged. Trust and permission markers and the banner/footer gate remain hard return falses — misreading a trust/permission prompt as ready is the dangerous case.
  • strip does not normalize NBSP. Ruby's String#strip leaves \p{Zs} separators in place, so the prompt regex matches the separator class explicitly with anchored \A/\z rather than relying on stripping.

Out of scope

No version bump or gem publish, and no fix for the /usr/bin/hive vs ~/.local/bin/hive daemon binary drift — that drift is documented as a validation caveat in wiki/operating.md only.

Test plan

  • rake coverage green: 100.00% (28895/28895) line coverage, 6869 runs, 73404 assertions, 0 failures, 0 errors.
  • Focused suites green: gemspec_test, gem_package_scripts_test, claude_launcher_test, wiki_log_test.
  • RuboCop clean on the touched Ruby files.
  • The packaging guard skips cleanly when gem is unavailable, so it never hard-fails a build-chain-less runner.
  • End-to-end repro against a sandbox GEM_HOME (build → install → hive run reaching WAITING, hive doctor green) is documented as a manual check in wiki/operating.md.

Review summary

Five review passes (two ce-code-review, two pr-review-toolkit, two codex passes) found no High defects. The Medium findings — a non-UTF-8 locale crash in the packaging guard's source File.read, and an over-loose ⏵⏵ chrome clause that could mark a stale caret with real footer-prefixed output as ready — were both fixed (read source as UTF-8; require the bypass-permissions footer copy). Nits (unreachable blank-line chrome branch, coupled-constant reconciliation comment, an explicit require "tmpdir", and a U+202F narrow-no-break-space regression test) were also addressed. Remaining no-fix items (the intentional ?/+/- chrome looseness per plan R2, and the invariant-lock trust/permission rejection tests) were reviewed and accepted. All plan units U1–U5 are implemented; no version bump or publish, consistent with the OUT scope.

Linked task

  • Task: fix-claude-tmux-ready-detector-260629-50cc
  • Acceptance: the reproduced task add-local-hive-web-install-260629-f4ca reaches WAITING instead of claude_launch_failed, with hive doctor green (manual e2e check per wiki/operating.md).

Compound Engineering
Claude Code

@ivankuznetsov

Copy link
Copy Markdown
Owner Author

Reviewer: claude-ce-code-review - Pass 01

Code Review — fix-claude-tmux-ready-detector (pass 1)

Reviewed git diff origin/main..HEAD. All plan units (U1–U5) are
implemented and the touched unit suites pass:
claude_launcher_test.rb (78 runs green), gemspec_test.rb (4 green),
and the new gem_package_scripts_test.rb passes under a UTF-8 locale.
The NBSP fixtures genuinely contain U+00A0 (verified by byte inspection),
so they really guard the detector fix. Findings below.

High

(none)

Medium

  • test/integration/gem_package_scripts_test.rb:43 crashes with ArgumentError: invalid byte sequence in US-ASCII under a non-UTF-8 default locale: File.read tags the UTF-8 source of claude_launcher.rb (box-drawing glyphs + ) as US-ASCII and String#scan then raises. Reproduces on this worktree's default (Encoding.default_external == US-ASCII), so the plan's documented rake test verification errors locally for the maintainer; passes only because GitHub CI defaults to a UTF-8 locale. The guard errors rather than skips/passes — read with File.read(path, encoding: "UTF-8") (or binread/force_encoding).

Nit

  • lib/hive/claude_launcher.rb:639 — the line.empty? branch in claude_prompt_chrome_line? is unreachable: current_lines is built with .reject(&:empty?) (line 620), so the slice passed below the caret never contains an empty line. Harmless and line-coverage-safe, but it's defensive dead logic that could mislead a future reader about what input shapes the predicate sees.
  • lib/hive/claude_launcher.rb:627current_lines.last(CLAUDE_PROMPT_TAIL_LINES) is redundant now that both CLAUDE_PROMPT_TAIL_LINES and CLAUDE_PROMPT_CONTEXT_LINES are 12: current_lines is already capped at CONTEXT_LINES inside current_prompt_text, so the second .last(12) is a no-op. Fine as a defensive bound, but the two coupled constants now encode the same window and could drift apart silently; a brief comment that they must stay reconciled would help.
  • lib/hive/claude_launcher.rb:72CLAUDE_PROMPT_CHROME_LINE treats lines made entirely of ?, +, or - as chrome. This is intentional (matches the ?─── rendered separator) and only applies to lines below the accepted caret, so a stale caret with a trailing markdown rule (---) could in principle read as ready; the chrome predicate's tightness is acceptable per plan R2, but worth noting the +/? inclusions are the loosest part of the gate.

@ivankuznetsov

Copy link
Copy Markdown
Owner Author

Reviewer: pr-review-toolkit - Pass 01

PR Review — fix-claude-tmux-ready-detector (pr-review-toolkit, pass 1)

The pr-review-toolkit:review-pr skill is not installed as an invocable
skill, so I ran its six persona lenses manually against
git diff origin/main..HEAD: code-reviewer, silent-failure-hunter,
type-design-analyzer, pr-test-analyzer, code-simplifier,
comment-analyzer.

Verification performed: claude_launcher_test.rb (78 runs, 0 failures),
gemspec_test.rb (4 runs), and gem_package_scripts_test.rb passes under a
UTF-8 locale. NBSP fixtures confirmed to contain U+00A0 by byte inspection.
Security-critical ordering is preserved — the trust/permission return false
guards still run first and unchanged (silent-failure-hunter: no new error
suppression introduced; the detector and gem-build guard both fail loud).

High

(none)

Medium

  • [code-reviewer] test/integration/gem_package_scripts_test.rb:43 raises ArgumentError: invalid byte sequence in US-ASCII when Encoding.default_external is not UTF-8: File.read reads the UTF-8 source of claude_launcher.rb (box-drawing glyphs + ) as US-ASCII, then String#scan blows up. This worktree's default IS US-ASCII, so the U2 guard errors under the plan's documented rake test verification locally and only passes because GitHub CI defaults to a UTF-8 locale — read the source with File.read(path, encoding: "UTF-8").

Nit

  • [pr-test-analyzer] The U2 guard's own crash (above) means the central release-representative test does not actually run on a non-UTF-8 dev machine — it errors instead of producing a pass/fail signal, weakening the "would a release build pass" guarantee the test exists to provide.
  • [pr-test-analyzer] No regression test for the narrow-no-break-space (U+202F) separator the plan's U3 explicitly calls out — \p{Zs} covers it, but only U+00A0 is exercised by fixtures, so a future regex narrowing wouldn't be caught for the narrow variant.
  • [code-simplifier] lib/hive/claude_launcher.rb:627current_lines.last(CLAUDE_PROMPT_TAIL_LINES) is a no-op now that current_prompt_text already caps current_lines at CLAUDE_PROMPT_CONTEXT_LINES and both constants are 12; the second slice can be dropped or the two constants collapsed/cross-referenced.
  • [code-simplifier] lib/hive/claude_launcher.rb:639 — the line.empty? operand in claude_prompt_chrome_line? is unreachable: the slice passed in comes from current_lines, which is built with .reject(&:empty?) (line 620). Defensive but dead.
  • [comment-analyzer] lib/hive/claude_launcher.rb:~46/:638 — the doc comment and the chrome rule describe "blank" as an accepted below-caret line, but empty lines are filtered out before the predicate ever sees them, so the "blank" case the comment documents cannot occur at that point. Comment slightly overstates the input shapes the predicate handles.
  • [code-reviewer] lib/hive/claude_launcher.rb:72CLAUDE_PROMPT_CHROME_LINE admits lines made entirely of ?, +, or - as chrome; this is the loosest part of the below-caret gate (a trailing markdown rule --- or diff markers +++ would count). Intentional per plan R2 and only applies below an already-matched caret, but worth a tightening note.
  • [comment-analyzer] lib/hive/claude_launcher.rb:74-78 — the CLAUDE_PROMPT_TAIL_LINES comment doesn't note that it must stay reconciled with CLAUDE_PROMPT_CONTEXT_LINES (both 12); the plan flagged keeping them in sync, and a future edit to one without the other would silently narrow the scan vs. context window.

(type-design-analyzer: no new types introduced — changes are constants,
regexes, and method bodies only; nothing to rate. silent-failure-hunter: no
findings — guards fail loud, the gem-build guard surfaces stdout/stderr on
failure, and skip on missing gem is appropriate rather than a silent pass.)

@ivankuznetsov

Copy link
Copy Markdown
Owner Author

Reviewer: codex-ce-code-review - Pass 02

High

Medium

  • claude_prompt_chrome_line? accepts any ⏵⏵ line as chrome: line 652 treats every line starting with ⏵⏵ as footer chrome, so a stale caret followed by real output such as ⏵⏵ running build step 1/2 is incorrectly reported ready, weakening the plan-required rejection of caret-with-real-output-below cases.

Nit

@ivankuznetsov

Copy link
Copy Markdown
Owner Author

Reviewer: pr-review-toolkit - Pass 02

PR Review — fix-claude-tmux-ready-detector (pass 2)

Diff: git diff origin/main..HEAD (hive.gemspec, lib/hive/claude_launcher.rb,
test/integration/gem_package_scripts_test.rb, test/unit/claude_launcher_test.rb,
test/unit/gemspec_test.rb, wiki/operating.md, wiki/log.d fragment).

Verification performed during review (read-only):

  • claude_launcher_test.rb → 79 runs, 0 failures; gemspec_test.rb → 4 runs, 0 failures.
  • gem_package_scripts_test.rb integration guard → ran (not skipped), 9 assertions, builds a real .gem and finds both referenced scripts.
  • Full rake coverage → 6872 runs, 0 failures, 0 errors, 7 (pre-existing env) skips; 100% line gate held (R5 satisfied).
  • rubocop on all 4 changed Ruby files → no offenses.
  • Traced every accept/reject case (incl. rejects_caret_above_the_input_box_tail, rejects_stale_prompt_marker_in_scrollback) against the new chrome-aware logic — all behave as the plan intends (R1 mitigation holds).
  • Confirmed the U2 reference-scan regex resolves exactly both real source literals (scripts/interactive_claude_wrapper.sh, scripts/stop_hook.sh).
  • Confirmed the NBSP test caret is a genuine U+00A0 (and the narrow variant U+202F), so the regression tests are non-vacuous.

All Requirements-Trace rows (U1–U5) are implemented; no plan-required item is missing. No High or Medium defects found.

High

(none)

Medium

(none)

Nit

  • [simplicity] CLAUDE_PROMPT_TAIL_LINES (12) duplicates CLAUDE_PROMPT_CONTEXT_LINES (12) and current_lines.last(CLAUDE_PROMPT_TAIL_LINES) at claude_launcher.rb:636 is a self-documented no-op: a single constant would eliminate the hand-synced invariant the comment itself warns can drift (deliberate per pass-1 reconciliation, behavior-neutral).
  • [test-quality] test_claude_ready_prompt_rejects_permission_prompt_with_footer and test_claude_ready_prompt_rejects_trust_prompt_with_footer short-circuit at the pre-existing trust/permission return false guards, so they pass even with the U3 chrome rule reverted — they lock an invariant but are not regression guards for this change (the ❯ 1. menu rejection is the test that actually exercises caret-with-footer).
  • [robustness] claude_prompt_chrome_line?'s line.start_with?("⏵⏵") clause (claude_launcher.rb:652) is never the deciding branch in any test because the footer always contains "for agents" (caught first); harmless defensive redundancy, not dead-but-broken.
  • [robustness] The CLAUDE_PROMPT_CHROME_LINE class treats a line composed purely of ? + - (plus box glyphs/separators) as chrome, so a caret followed only by such a line — with a banner/footer present elsewhere in the pane — could read as ready; low risk and explicitly accepted by plan R2, but worth a guard test if Claude ever prints dash-only output below a stale caret.
  • [robustness] test/integration/gem_package_scripts_test.rb calls Dir.mktmpdir without require "tmpdir", relying on transitive loading (works today via open3/test_helper); an explicit require would make the guard self-contained.

Read claude_launcher.rb with explicit UTF-8 encoding in the gem-package
script guard so it no longer raises ArgumentError (invalid byte sequence)
under a non-UTF-8 default locale. Add a regression fixture for the narrow
no-break space (U+202F) caret separator the U3 plan calls out, alongside
the existing U+00A0 NBSP coverage.

Hive-Task-Slug: fix-claude-tmux-ready-detector-260629-50cc
Hive-Fix-Pass: 01
Hive-Fix-Findings: 2
Hive-Triage-Bias: courageous
Hive-Reviewer-Sources: claude-ce-code-review,codex-ce-code-review,pr-review-toolkit
Hive-Fix-Phase: fix
…stants

Remove the dead `line.empty?` branch in claude_prompt_chrome_line? — callers
pass `.reject(&:empty?)`-filtered lines, so blank input never reaches the
predicate. Correct the readiness doc comment that listed "blank" among
accepted below-caret chrome shapes, and note that CLAUDE_PROMPT_TAIL_LINES
and CLAUDE_PROMPT_CONTEXT_LINES must stay reconciled (both 12) so a future
edit to one cannot silently narrow the scan vs. context window.

Hive-Task-Slug: fix-claude-tmux-ready-detector-260629-50cc
Hive-Fix-Pass: 01
Hive-Fix-Findings: 4
Hive-Triage-Bias: courageous
Hive-Reviewer-Sources: claude-ce-code-review,codex-ce-code-review,pr-review-toolkit
Hive-Fix-Phase: fix
`claude_prompt_chrome_line?` treated any line starting with `⏵⏵` as
footer chrome, so a stale caret followed by real output such as
`⏵⏵ running build step 1/2` was incorrectly reported ready. Tighten the
clause to require the `bypass permissions` hint-footer copy (the real
footer also carries "for agents", so accept tests stay green) and add a
regression test. Also make the gem-package integration guard
self-contained by requiring "tmpdir" explicitly.

Hive-Task-Slug: fix-claude-tmux-ready-detector-260629-50cc
Hive-Fix-Pass: 02
Hive-Fix-Findings: 2
Hive-Triage-Bias: courageous
Hive-Reviewer-Sources: claude-ce-code-review,codex-ce-code-review,pr-review-toolkit
Hive-Fix-Phase: fix
Hive-Task-Slug: fix-claude-tmux-ready-detector-260629-50cc
Hive-Stage: 8-finalize
Hive-Auto-Commit: residue
Hive-Auto-Commit-Reason: finalize_entry_backstop
@ivankuznetsov
ivankuznetsov force-pushed the fix-claude-tmux-ready-detector-260629-50cc branch from 3fede60 to 40667fa Compare June 30, 2026 14:32
@ivankuznetsov
ivankuznetsov marked this pull request as ready for review June 30, 2026 14:32
@ivankuznetsov
ivankuznetsov merged commit 9015322 into main Jul 1, 2026
14 checks passed
@ivankuznetsov
ivankuznetsov deleted the fix-claude-tmux-ready-detector-260629-50cc branch July 1, 2026 09:07
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