Verified that all cleanup mechanisms are properly implemented:
- Orphan cleanup on startup (10-minute threshold)
- CleanupGuard for automatic RAII cleanup
- Global cleanup before process::exit()
- Idempotent cleanup with retry logic
All exit paths covered:
- Normal exit (success/error)
- Timeout exit
- Signal interruption (SIGINT/SIGTERM)
- Watchdog timeout
- Panic
- Early returns
All tests passing. No orphaned temp directories found.
Bead-Id: bf-2w7
- Verify comprehensive cleanup on all exit paths
- Document all cleanup mechanisms and their locations
- Confirm all 90 tests pass including cleanup-specific tests
- Exit path matrix shows all paths covered
Co-Authored-By: Claude <noreply@anthropic.com>
Bead-Id: bf-2w7
The watchdog mechanism was complete but had an inconsistency:
main.rs used exit code 3 for timeout errors while ClaudePrintError::Timeout.exit_code()
returned 124 (GNU timeout convention). Now uses the proper exit code from the error type.
This ensures timeout errors exit with the standard code 124, matching GNU timeout
behavior and making error handling consistent for callers (marathon loop/NEEDLE).
This improvement ensures that when a watchdog timeout occurs, the event
loop wakes up immediately (via self-pipe write) rather than waiting for
the poll timeout. This allows for faster and more responsive cleanup on
timeout, ensuring temp dirs and FIFOs are removed promptly.
Co-Authored-By: Claude <noreply@anthropic.com>
- Add cleanup_performed flag to HookInstaller for idempotent cleanup
- Add Drop implementation to HookInstaller for automatic cleanup
- Enhance cleanup() to explicitly remove both FIFO and temp directory
- Ensure temp dirs are cleaned up on normal exit, error, timeout, signals, and panic
- cleanup_orphans() already called at startup to sweep stale temp dirs
Co-Authored-By: Claude <noreply@anthropic.com>
Implement a complete watchdog timeout system that ensures hung child
processes are terminated cleanly with proper diagnostics and cleanup.
Features:
- PTY first-output timeout (default 90s): detects if child produces no PTY output
- Stream-json first-output timeout (default 90s): detects if child produces no stream-json events
- Overall session timeout (default 3600s): prevents indefinite hangs
- Stop hook watchdog timeout (default 120s): detects if Stop hook doesn't fire after prompt injection
Timeout handling:
- Sends SIGTERM to child process when timeout fires
- kill_child() ensures SIGTERM → SIGKILL sequence (2s grace period)
- Writes clear diagnostic to stderr indicating timeout type
- Emits stream-json error event for downstream consumers
- CleanupGuard ensures temp dir/FIFO cleanup on all exit paths
- Returns Error::Timeout and exits non-zero (code 3) for retry loop
Fixes:
- Pass temp_dir_path to Watchdog so stream-json monitoring works correctly
- Remove unused constants (duplicates of watchdog module defaults)
- Improve mock-claude binary path resolution for workspace builds
This prevents the indefinite hang that occurs when Claude Code wedges
during session initialization or tool use, ensuring marathon loops and
NEEDLE can retry cleanly instead of blocking forever.
Bead-Id: bf-2f5
- Confirm all cleanup mechanisms are in place and working
- All 90 tests pass
- Orphan sweeping on startup, Drop guard for normal paths, global cleanup for process::exit()
- All exit paths covered: normal, error, watchdog timeout, signal interruption
Co-Authored-By: Claude <noreply@anthropic.com>
Root cause: Child claude hangs at startup when global settings containing
hooks (SessionStart, SessionEnd, etc.) are inherited despite creating a
temp settings.json with only a Stop hook.
When --settings=<temp_path> is passed without --setting-sources=, Claude Code
merges temp settings with global settings. Global hooks fire and may hang,
causing the child to never produce output and the first-output timeout to fire.
Fix: Always pass --setting-sources= to child claude (src/session.rs:127-129)
to prevent global settings inheritance. This ensures ONLY the temp settings.json
is loaded, preventing any global hooks from causing hangs.
Evidence: Documented in notes/bf-2u1-findings.md and notes/bf-2u1-investigation.md
Related beads:
- bf-2w7: temp dir and FIFO cleanup
- bf-3ag: session implementation
Add test execution step to claude-print-ci WorkflowTemplate.
This ensures watchdog regression tests (silent child timeout)
run before creating GitHub releases.
Co-Authored-By: Claude <noreply@anthropic.com>
Update all call sites to include the new first_output_timeout_secs parameter:
- src/main.rs: pass None for default first-output timeout
- tests/watchdog.rs: pass None in both watchdog tests
The prior commit added the 5th parameter but missed updating the callers,
causing compilation errors.
Co-Authored-By: Claude <noreply@anthropic.com>
Bead-Id: bf-2w7
- Add cleanup_orphans() to HookInstaller: sweeps stale claude-print-* dirs on startup
- Add cleanup() method to HookInstaller: explicitly removes FIFO and temp dir artifacts
- Add CleanupGuard struct in session.rs: ensures cleanup via Drop on all exit paths
- Call cleanup_orphans() in HookInstaller::new() on each invocation
This prevents orphaned temp directories from accumulating after crashes,
timeouts, or signal interruptions.
Co-Authored-By: Claude <noreply@anthropic.com>
Bead-Id: bf-2w7
Update billing description to present tense (Agent SDK split is now
active, not upcoming); tighten the tagline.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
These flags were parsed by the CLI but never added to claude_args,
so headless callers (e.g. marathon-coding) would hang on interactive
tool-use approval prompts. Also forward allowedTools/disallowedTools
for completeness.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
These crates were used in main.rs but absent from Cargo.toml, causing
CI build failures (exit 101) in fresh environments that lack the
local shared target/ dir where transitive deps were available.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
CI now ships glibc-linked binaries (cargo build --release on debian:bookworm,
same as forge-ci/needle-ci). Asset names: claude-print-x86_64-linux and
mock_claude-x86_64-linux. musl cross-compilation dropped — exceeded 3600s
workflow deadline.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Bug 1 (event_loop.rs): poll(-1) blocked forever when TUI went silent;
startup timer never fired so prompt was never injected. Fixed: poll(50ms)
+ empty-slice tick so poll_timers() runs on every iteration.
Bug 2 (session.rs): read_fd from open_fifo_nonblock() was dropped
immediately after as_raw_fd(), closing the fd the event loop was polling.
Fixed: store both (_fifo_read, _fifo_keeper) to keep both alive.
Bug 3 (pty.rs): child inherited CLAUDE_CODE_SESSION_ID from parent, so
it wrote events into the parent transcript and skipped Stop hook dispatch.
Fixed: unsetenv(CLAUDE_CODE_SESSION_ID) in child after fork; preserve
CLAUDECODE=1 and CLAUDE_CODE_ENTRYPOINT=cli.
Bug 4 (session.rs): empty-slice timer ticks were fed to startup.feed()
which reset last_output_at, preventing idle timer from ever firing.
Fixed: guard startup.feed() and terminal.feed() from empty slices.
Bug 5 (session.rs): handle.join() blocked main thread for up to
cli.timeout (default 3600s) on any early exit, because the timeout thread
sleeps for the full duration. Also, waitpid blocked forever if child
ignored SIGTERM. Fixed: drop(timeout_thread) to detach; add kill_child()
helper (SIGTERM → 2s wait → SIGKILL) used on all cleanup paths.
All five confirmed fixed: claude-print "what is 2+2?" → "4", exit 0,
cc_entrypoint=cli in session JSONL (subscription billing verified).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Add SessionResult struct with transcript, claude_version, duration_ms
- Implement Session::run() with full signature and PTY session orchestration
- Add resolve_claude_version() using Command::new(claude_bin).arg("--version").output()
- Add pub mod session to lib.rs
- Include unit tests for version resolution with mock binaries
Co-Authored-By: Claude <noreply@anthropic.com>
All 13 emitter tests pass. Implementation completed in commit bfb50da.
Verified output formatting for text, json, and stream-json formats.
Co-Authored-By: Claude <noreply@anthropic.com>
Complete implementation of error.rs with:
- Full Error enum covering all error types (PTY spawn failures, hook
setup, parse failures, timeout, interruption, binary resolution,
version checks, terminal errors, and child process errors)
- Proper error propagation with user-friendly messages
- ClaudePrintError for user-facing errors with exit codes and JSON
subtypes
- 18 unit tests covering all error variants and conversions
Fixes bead bf-46v
- Add SIGINT_RECEIVED static flag and sigint_handler signal handler
- Install SIGINT handler in relay() to catch Ctrl-C presses
- Forward SIGINT to child process in relay loop using kill()
- Restore default SIGINT handler on exit
- Update error types to use specific PTY errors (OpenptyFailed, ForkFailed, SignalHandlerFailed, WaitpidFailed)
This implements key invariant #3 from AGENTS.md: pressing Ctrl-C must reach
the claude child process, not just terminate claude-print.
Co-Authored-By: Claude <noreply@anthropic.com>
Documents the root cause of the bf-40i loss (claude-sonnet PTY fallback
in resolve_adapter), the consequences, and the mitigations (atomic label,
NEEDLE fixes bf-14w/bf-2wi).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Documents build commands, test structure, module map, key invariants,
and bead workflow for AI coding agents working in this repo.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
bf-4r6 (Write AGENTS.md) was blocked on bf-40i (Wire main()), but
documentation writing does not require main() to be functional.
Removing this dependency makes bf-4r6 ready for a worker to claim.
bf-52c's dependency on bf-40i remains correct — binary E2E tests
genuinely require a working binary.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
All module-level deliverables for phases 1-11 are committed. Checked all
[ ] boxes to [x], added a Status section summarizing what is done vs.
pending (main() orchestration, E2E tests, billing verification, CI release),
and noted that the Phase 11 install.sh end-to-end download test is blocked
on a release binary (which requires main() completion first).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>