Repository navigation
feat(50)!: addressable monitor liveness without a PID (SUPV-01) - #225
Conversation
- tests/common/mod.rs: shared helpers (HermeticHome under /tmp, FakeBin, wait_for, kill_and_reap, LaunchedRunReaper, entries_under); the hermetic_devflow builder is a stub that sets nothing yet - tests/test_home_isolation.rs: R-13 test with a plain-Command negative control, and the permanent source scan with planted controls; the spawn and env-mutation counters are stubs - RED: both tests fail on their planned assertions (HOME not set; planted raw spawn counted 0) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0182ocSM2E4fb2zu52jxGLzi
- hermetic_devflow sets HOME, XDG_CACHE_HOME, XDG_CONFIG_HOME and DEVFLOW_CACHE_DIR under a /tmp HermeticHome and removes every repo-local and redirecting GIT_* variable - code_only strips // comments outside string, raw-string and char literals; raw_devflow_spawns and env_mutations (whole identifiers) count the scanned tokens - GREEN: a_child_writing_outside_its_tempdir_home_fails and suites_using_common_never_spawn_devflow_raw_or_mutate_the_environment pass; three mutants (HOME override dropped, set_var planted in common, R-13 control removed) each fail on the intended assertion Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0182ocSM2E4fb2zu52jxGLzi
…lder - both suites declare mod common; and spawn devflow only through common::hermetic_devflow with a per-test HermeticHome bound before the spawn - start_lock_e2e drops its local devflow_bin, FakeBin, fake_bin_dir, wait_for, kill_and_reap and LaunchedRunReaper for the common ones; resume_command takes the hermetic home instead of a cache dir - the four start tests that launch a run now bind a LaunchedRunReaper - stop_e2e keeps its own init_repo, wait_for and write_live_lock; its kill_and_reap is replaced by the common one - test counts unchanged (12 and 22); under a sentinel HOME the three suites leave no devflow file, while the pre-migration start_lock_e2e leaves three registry entries there Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0182ocSM2E4fb2zu52jxGLzi
- phase7_cli, gate_wedge_e2e and gate_notify_persist_e2e declare `mod common;` and spawn devflow only through common::hermetic_devflow - gate_notify_persist_e2e: isolated_cache/ReapOnDrop replaced by a test-level HermeticHome and a ParkedChild guard on common::kill_and_reap - LaunchedRunReaper bound right after every spawn that launches a run - no assertion, argument list or test count changed (28/5/9) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0182ocSM2E4fb2zu52jxGLzi
- gate_sweep_e2e, start_reachability_e2e, log_format_env, doctor_antigravity and auto_chain_leak_repair_e2e declare `mod common;` and spawn devflow only through common::hermetic_devflow - auto_chain_leak_repair_e2e: Fixture owns the HermeticHome (dropped after its reaper); LaunchedRunReaper bound after the __monitor spawn - gate_sweep_e2e: no_suite_on_the_common_builder_runs_a_real_reap_strays_pass source scan with planted controls (T-50-05) - no assertion, env var or argument list changed (counts 4+1/2/3/2/2) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0182ocSM2E4fb2zu52jxGLzi
- acquire_reclaims_a_lock_whose_live_pid_has_a_different_start_time fails today: the pid-only predicate returns Contended (RED) - controls: a live identity-matched holder and a live holder with no recorded start time both stay Contended (pass before the change) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0182ocSM2E4fb2zu52jxGLzi
- acquire_after_existing now classifies the holder with classify_holder_record (pid liveness + recorded start time) inside the coordination flock it already holds - NoHolder and Recycled are reclaimed; Live and Unconfirmable stay Contended (Phase 48 D-02 one-writer rule) - the reclaim warning names the observed and recorded start times when it reclaims a recycled pid (999.128, D-08) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0182ocSM2E4fb2zu52jxGLzi
- rename clean_phase_returns_without_cleanup_for_a_recycled_pid_lock to clean_phase_reclaims_a_recycled_pid_lock_and_cleans and invert it - add clean_phase_refuses_a_live_pid_lock_and_cleans_nothing, a hand-written live record so the refusal comes from classification - recover_clean_e2e: declare mod common, spawn devflow hermetically, add explicit_clean_at_a_recycled_lock_holder_reclaims_and_cleans (R-13) - recover_cmd: correct the explicit-phase comment (comment only) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0182ocSM2E4fb2zu52jxGLzi
The audit reads process-wide counters that the other phase7_cli tests bump, so it only holds when they share its process (cargo test, which scripts/check.sh runs). nextest gives each test its own process, where REAPED stays 0 and the audit could only time out after 60s. Return early when NEXTEST is set. Resolves WINDOWS.md #18 (operator decision 2026-10-17). Verified: nextest -E 'test(=suite_reap_audit)' 1 passed in 0.008s; cargo test --exact suite_reap_audit alone still runs the audit and fails at its deadline (rc 101); cargo test --test phase7_cli 28 passed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0182ocSM2E4fb2zu52jxGLzi
- supervisor module: SocketPath, publish via .part + hard_link, protocol framing - registry::socket_dir_in; path_digest widened to pub(crate) - probe is a stub returning NoReply, so V-01..V-03 fail at their probes Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0182ocSM2E4fb2zu52jxGLzi
- probe: lstat, socket type and uid before connect; only ECONNREFUSED is Stale; a connect counts only after a valid ping reply; reads no pid - peer_is_same_user: SO_PEERCRED on Linux, getpeereid on macOS, false elsewhere - protocol wire-shape and line-cap tests - DEV-SETUP-CHECKLIST: aarch64-apple-darwin target for the macOS compile gate Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0182ocSM2E4fb2zu52jxGLzi
- V-05: regular file, directory, symlink, unstat-able path and mode-000 socket are probe errors, never Stale; the same socket made readable is Stale - V-04: six fixtures, incl. a SIGKILLed holder child, each give one outcome; Gone, Stale and Alive are distinct - test_support::spawn_test_in_child shares run_test_in_child's command - cargo mutants on probe: 6 caught, 2 timeouts, 0 missed Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0182ocSM2E4fb2zu52jxGLzi
- V-06: 106 and 107 bytes publish with a .part name of equal length; 108 is
TooLong { len: 108, max: 107 } before any bind; new() checks .part too
- V-11: final name reads Gone inside the publish window; a second publish is
AlreadyExists and the first listener still answers; raw bind without
listen reads Stale (control)
- sock/ is 0700 (tightened if looser), socket 0600; symlinked or non-dir
sock/ refused
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0182ocSM2E4fb2zu52jxGLzi
- sidecar_round_trips_and_is_owner_only: read-back, 0600, truncated is Json - a_published_socket_is_found_by_root_and_phase_without_state (tracer) - a_socket_whose_sidecar_will_not_parse_is_still_found_by_its_name (V-36) - Sidecar, SidecarError, SweepEntry types; sweep/read/write bodies stubbed Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0182ocSM2E4fb2zu52jxGLzi
- write_sidecar: 0600 temp in sock/, renamed onto <stem>.json - read_sidecar, Sidecar, SidecarError; created_at is RFC 3339 UTC - sweep_in lists .sock/.json stems, probes and reads; acts on nothing - sweep_for_in: sidecar wins; else name digest+phase, else ping phase - ship::civil_from_days widened to pub(crate) for the timestamp Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0182ocSM2E4fb2zu52jxGLzi
- sweep_reports_without_removing_or_signalling (V-22) with removal control - request_shutdown_reads_accepted_then_stopped: stop, hand-off, refusal, close, timeout and malformed final line - remove_launch_files, ShutdownOutcome, request_shutdown stubbed Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0182ocSM2E4fb2zu52jxGLzi
- remove_launch_files: removes .sock and sidecar, NotFound is success - request_shutdown: probe's pre-connect checks, then accepted within IO_TIMEOUT and the final line within final_timeout; only a stopped line is Stopped, handed_off is HandedOff, other refusals Refused - probe's lstat/socket/uid checks factored into own_socket_at Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0182ocSM2E4fb2zu52jxGLzi
- supervisor_record_round_trips_and_absent_reads_as_none via save/load - SupervisorRecord and State::supervisor added; field stubbed serde(skip) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0182ocSM2E4fb2zu52jxGLzi
- serde(default) Option<SupervisorRecord>: saved records reload equal, older state files without the key load with None Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0182ocSM2E4fb2zu52jxGLzi
- auto_chain_flag_e2e, yes_ship_config, release_check spawn devflow only through common::hermetic_devflow; their local devflow_bin() is deleted - auto_chain_flag_e2e binds a LaunchedRunReaper after each foreground __monitor run, covering a regressed stop_until halt - new hermetic_spawn_sites.rs: permanent source scan (R6-G4) with planted controls and a real-data control per suite Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0182ocSM2E4fb2zu52jxGLzi
- reap_strays_e2e and help_snapshot spawn devflow only through common::hermetic_devflow; reap_strays_e2e still runs no real stray reap - hermetic_spawn_sites.rs covers all five suites and adds only_common_names_the_devflow_binary: a recursive walk of tests/ that allows only common/mod.rs (and recover_clean_e2e.rs) to name the binary - help snapshot unchanged Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0182ocSM2E4fb2zu52jxGLzi
- Child-process supervisor: socket shutdown must kill the agent group and report Stopped 143; control: an agent exiting 7 completes with its code - Source scan for process exit / signal mask / signal wait calls with a planted-positive control - Runtime types, pure decision fns and a ping-only control thread; shutdown, stop-aware wait_agent and the scanner are stubs - Drop the dead_code expectations the runtime now satisfies Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0182ocSM2E4fb2zu52jxGLzi
- Control thread takes shutdown under the owner lock: SIGTERM the agent group while the exit is unobserved, park the caller, SIGKILL after TERMINATE_VERIFY_WAIT if the main thread has not seen the exit - wait_agent observes the exit with waitid WNOWAIT, decides under the lock, SIGKILLs leftover members while the zombie leader holds the pgid, then reaps - forbidden_calls source scan implemented Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0182ocSM2E4fb2zu52jxGLzi
…-14) - Five interleavings against the real StopToken and a real agent process, each window held open with barriers: stop before exit, exit observed then stop before the decision, stop during a gate wait, stop before the next spawn, stop between spawn and publish - Control: the same machine with no stop advances - Table tests over on_stop_request, on_agent_exit, at_safe_point and the token's request_stop and safe_point for every phase Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0182ocSM2E4fb2zu52jxGLzi
A run that ended before its stop window left the stopper thread blocked on the barrier and the test hung on the join (seen with a hand mutant that made every exit a stop). Assert the window was reached first. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0182ocSM2E4fb2zu52jxGLzi
- SIGTERM and SIGINT to a child supervisor must end as a stop with the agent group and socket gone; the agent's SigBlk has no TERM/INT bit and its SigCgt no TERM bit; control: without the pipe the supervisor dies on TERM and its agent group lives on - A member that ignores TERM must be gone within the grace window after a socket shutdown - install_signal_pipe is a no-op stub Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0182ocSM2E4fb2zu52jxGLzi
- install_signal_pipe: once per process, a close-on-exec non-blocking pipe (pipe2 on Linux, pipe plus fcntl elsewhere) and an SA_RESTART handler that only checks getpid and writes one byte - The control thread polls the pipe and turns each byte into a stop with StopSource::Signal, acted on like a socket shutdown - The test child disarms its own group guard once wait_agent returns, so a member the supervisor failed to kill is no longer hidden Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0182ocSM2E4fb2zu52jxGLzi
- Stopped at a safe point: stopped, socket already gone - Taken but never stopped before finish: handed_off, never stopped - Handed off before the request: handed_off at once, nothing flagged Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0182ocSM2E4fb2zu52jxGLzi
- MonitorError is #[non_exhaustive] with SocketPath, Publish, PgidMismatch and NotReady (named R-9 break; semver gate --through 50-08 verdict=match) - StageIo, StageSpec, StageRun and a stub run_supervised_stage - capture_arm_runs_the_agent_under_an_addressed_supervisor runs the supervisor in a child test process and fails on the stub Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0182ocSM2E4fb2zu52jxGLzi
- run_supervised_stage installs the signal pipe, spawns the agent with process_group(0) via hermetic_command, requires getpgid == pid (PgidMismatch kills and reaps otherwise), writes the pid file, serves the socket (sidecar first), waits through wait_agent and writes the exit file (143 on Stopped) - capture stdio: stdin null, stdout and stderr to separate files - rfc3339_now drops its expect(dead_code): the sidecar builder is its first non-test caller; STOPPED_EXIT_CODE is now pub(crate) - the stream shape returns NotReady until task 2 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0182ocSM2E4fb2zu52jxGLzi
On procps-ng, ps -g selects by session, so the leader-gone refusal's inspection command listed no member of a live process group whose id differs from its session id. The hint now runs ps -o pid,pgid,sid,lstart,args -p "$(pgrep -d, -g <pgid>)". Verified on procps-ng 4.0.6 against a group with pgid != sid: the old command printed only a header, the new one listed the member. Test pins and the doc_check fragment were changed first and failed on the old text. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0182ocSM2E4fb2zu52jxGLzi
…drops - parallel_creates_two_worktrees_and_spawns_two_monitors left /tmp/.tmpXXXXXX/.devflow/.lock-07.coord (plus .gitignore) after every run: 5 of 5 alone, and once per full `cargo test --workspace` - Mechanism: the phase-7 reaper's shutdown lands while the Plan supervisor's advance is launching Code (safe point 3). Code is spawned but never recorded and waits on lock-07; Plan gives up after SUPERVISOR_STOP_WAIT (11 s) and answers `stopped`, the reaper returns, the TempDir is removed, then Code takes lock-07 to record its flagged stop and recreates <root>/.devflow on the way - LaunchedRunReaper now also waits (bounded, 15 s) until no `devflow __monitor` naming its root and phase is alive, and ends any left after the wait (start-time checked, `spare` honoured) - Evidence: with the phase-7 reaper dropped first so every stop hits that window (about 13.4 s per run), 0 of 3 runs leaked; the same order with the old reaper leaked 2 of 3. Unmodified test: 5 of 5 before, 0 of 5 after Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HRWwk1oVv9oLFxjb9cSiCm
- doctor_reports_an_unlistable_socket_dir_instead_of_nothing (50-12) and the_launch_guard_refuses_an_unlistable_socket_dir_over_an_ unloadable_state (50-15) skipped under uid 0 since cdb6efc: root's CAP_DAC_OVERRIDE lists a 0o000 directory, so the pinned container never ran their assertions (WINDOWS.md #19) - The reads now run on a scoped thread that clears CAP_DAC_OVERRIDE and CAP_DAC_READ_SEARCH from its own effective set (capset(2) is per-thread), so root is refused by the same 0o000 directory with EACCES and reaches the same arm; the uid-0 skips are removed - Both tests now also pin the error: permission denied - Pinned image as uid 0: both pass; with socket_dir_unlistable forced to None both fail at their assertions; with the capability drop disabled both fail their 0o000 precondition. Host (uid 1000): pass, and fail with either fail-closed check removed Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HRWwk1oVv9oLFxjb9cSiCm
- ARCHITECTURE.md's gate section described only Gates::poll_response's 1s -> 60s backoff. Inside a supervisor's advance (stop token engaged) wait_for_gate_response uses poll_response_cancellable: owner gate_wait, every sleep capped at 1s, the stop token checked before each sleep, so a socket shutdown or SIGTERM/SIGINT ends the wait in about a second, clears the gate, records `stopped` and keeps the state - The uncancellable backoff poll is kept for a run with no stop token (the hidden `devflow advance` from a shell, a foreground gate) - run_gate() lives in pipeline_gate.rs, not main.rs Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HRWwk1oVv9oLFxjb9cSiCm
- cleanup's refusal for a live agent or an ALIVE supervisor told the operator to run `devflow resume --phase N`, which refuses while a run is live; since 50-14 `devflow stop --phase N` is the verb that ends it - cleanup_force_refuses_on_live_agent_unknown_monitor and cleanup_force_refuses_on_dead_monitor_live_agent now assert the refusal names `devflow stop --phase N` and no `devflow resume`; both failed on the old text first - OPERATIONS.md (50-17) no longer says the CLI text is out of step Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HRWwk1oVv9oLFxjb9cSiCm
- The preflight's launch-shape refusal said the chain-flag guard binds only inside the pipe-owning monitor, "which this launch shape never starts". Since 50-11 a capture launch runs under the same Rust supervisor; run_monitor just never engages the guard for it - The reason now ends "— so Code would launch on the capture shape, whose supervisor never engages the chain-flag guard (only a stream-shape supervisor does)", for every cause; the doc comment no longer calls the legacy arm a detached shell script - unattended_launch_shape_refusal_names_the_capture_supervisor pins the sentence for the legacy opt-out and a codex agent; it failed on the old text first Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HRWwk1oVv9oLFxjb9cSiCm
Two held-advance pairs on a --yes-ship Ship stage: a socket stop taken after safe point 1 (test hold after_safe_point_1) and one taken just before finalization (test hold before_finalization) must ship nothing, record the stop at safe point 5 and 6 respectively, and answer the caller stopped. Each has a no-stop control that ships. Both stop tests fail on the current code: the run emits gate_resolved (--yes-ship), runs every after-ship hook and emits workflow_shipped. The hold points are cfg(test) only. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RGzZZbfn3r9638yptqPL47
A stop accepted while a supervisor's advance runs (owner advancing) was read only at safe points 1-4. With --yes-ship, run_gate_with_timeout wrote the approval before the gate wait, and a response on disk wins over a cancel, so a stop landing after safe point 1 let finalization merge, bump, tag and push. Safe point 5 sits right before a pre-authorized response is written: the gate is cleared, the stop recorded and an error unwinds the advance, as at safe point 4. Safe point 6 opens finish_workflow_with_gate_timeout and clears the consumed Ship approval with the stop. Both record advance_stopped with their number, so a parked stop caller is answered stopped. Outside a supervisor's advance (ship_override) no token is engaged and both proceed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RGzZZbfn3r9638yptqPL47
- stream_wait_test_hold parks a stream supervisor after its exit file and before its stream threads are waited for; a stop or the release file ends it - compiled only under cfg(test) or the test-support feature, with its call - source scan G pins the gate, the gated call and the literal env read - DEVFLOW_TEST_STREAM_WAIT_HOLD allowlisted (doc_check failed naming it first) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RGzZZbfn3r9638yptqPL47
RED record, on the unfixed code (nextest exit codes): - A a_completed_stream_run_returns_while_a_leftover_process_holds_the_agents_stdout: 100, WR-03: supervisor still runs 20 s after agent release, owner agent_running - B a_completed_stream_run_returns_once_its_leftover_process_released_the_agents_stdout: 0 (control: holder on /dev/null) - C no_line_reaches_the_capture_after_the_stdout_wait_expires: 100, WR-03: Ok(Ok(0)) only after 20.0 s (when the holder exited) - F a_stream_run_returns_while_a_leftover_process_keeps_writing_to_the_agents_stdout: 100, WR-03: Err(Timeout) after 20 s while the leftover kept writing - HeldStdout records the holder's pid and start time from the agent itself Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RGzZZbfn3r9638yptqPL47
…ead join (WR-03) - drive_stream_io: one StreamEvent channel; an exit-watch thread sends AgentExited after waitid(WNOWAIT) (never reaps), so the loop ends at the agent's exit instead of after an idle timeout of silence - the reader tees through a shared Mutex<Option<File>>, keeps reading after the loop ends, and discards once the capture is taken (pipe stays drained) - StreamThreads::join_within(READER_DRAIN_WAIT = TERMINATE_VERIFY_WAIT, 3 s) replaces the unbounded join for Completed and Stopped; at the bound the capture is taken with a bounded try_lock and unfinished threads detach - finish_stream_threads records a stream_wait_bounded event - wait_for_exit_without_reaping is pub(crate); nothing is signalled Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RGzZZbfn3r9638yptqPL47
- D: a stop with the agent's stdout held outside the agent's group finishes within the stop's own wait (Stopped 143, advance suppressed, one bound) - E1: a completed run on the real __monitor reaches its --until halt with a held stdout; E2 (control): the hold is reachable with stdout released - E3: devflow stop sent while advancing ends the run at safe point 1; the exit file keeps the agent's 0 (999.166 item 6) - ARCHITECTURE.md: monitor step 4 names the bounded stream wait Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RGzZZbfn3r9638yptqPL47
The negative-control writer in auto_rescan_rejection_parks_for_supervised_repair read the phase state, then checked whether the re-scan response was gone. The park saves `stopped` and only then removes the response, so a writer descheduled between its two reads could miss the whole park: it saw "not stopped", then "response gone", and wrote an abort no gate had asked for. The final "writer must stay inert" assertion then failed although only the one re-scan gate had fired. Check the response first and the state second. A response the park removed is then always seen with `stopped` set, while a response removed by the pre-fix loop-back (state not stopped) still triggers the writer. Under stress-ng --cpu 12 with 16 parallel loops of the test binary the old order failed 9 of 2600 runs, the new one 0 of 2000. With a 20 ms sleep injected between the two reads the old order failed 50 of 50 and the new one passed 50 of 50. A mutant restoring the pre-fix Auto loop-back still fails 45 of 45 with the new writer, at "Auto rejection must park the phase instead of aborting it". Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0182ocSM2E4fb2zu52jxGLzi
A launch that could not read its agent's start time or the boot id records 0 or "" in their place. classify read "" as PreviousBoot and 0 as LeaderRecycled, so stop removed the socket and sidecar, signalled nothing and reported nothing running while the group could still live. Either placeholder now classifies as RefuseIdentityUnavailable, before the boot and leader rules. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HRWwk1oVv9oLFxjb9cSiCm
The lock holder is signalled after its pid and start time match the identity the lock recorded, not after a /proc cmdline check. And `stopped` is not recorded after a gate answer whose waiter still holds the phase lock: that waiter's abort path clears the state instead. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HRWwk1oVv9oLFxjb9cSiCm
The launch guard returned Ok as soon as the state file was missing, so it skipped the socket sweep that `stop` runs in the same case and let `start` launch over a live supervisor whose state `recover --clean` had removed. A missing state now sweeps this root and phase's sockets, with the same refusal for a socket directory that cannot be listed. The operator-approved no-state carve-out for the agent pid file stays: it is still not read without a state file. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HRWwk1oVv9oLFxjb9cSiCm
`recover --clean --phase N` counted only an ALIVE socket as live, so a probe error, or a STALE socket whose agent group the backstop could not prove gone, passed and the state was deleted. It now uses the launch guard's supervisor rule, factored out as possibly_live_sockets: ALIVE, STALE unless proved gone, or a failed probe refuses, as does an unlistable socket directory when the state is missing or does not load. The agent pid file is still not read. OPERATIONS.md says so. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HRWwk1oVv9oLFxjb9cSiCm
A sidecar's launch id, and the launch id segment of a socket name, are printed into copy-paste `devflow stop --phase N --launch <id>` lines (the several-sockets refusal, the launch guard's repair lines, doctor's orphan findings), but nothing checked them. One rule in supervisor.rs, is_launch_id (16 lowercase hex digits), now gates both: a sidecar with any other id does not deserialize, so it reads like an unparsable sidecar everywhere, and a socket name with one yields no launch id. The rejected id is never echoed. Test fixtures that used readable ids now use ids in that format. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HRWwk1oVv9oLFxjb9cSiCm
write_sidecar renamed over its target unconditionally and Supervisor:: serve wrote before publishing, so a launch whose socket name was taken replaced that launch's sidecar and only then failed AlreadyExists, leaving it naming this launch's agent. write_sidecar now hard-links its temp file into place, which refuses an existing sidecar, and serve's publish failure removes the sidecar it created, the taken-name case included. The monitor wrote the same sidecar just before serve wrote it again; it now publishes through serve_written after its own (RV-03) write, so the sidecar is written once. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HRWwk1oVv9oLFxjb9cSiCm
A stopped supervisor records its stop under the per-phase lock, waiting up to SUPERVISOR_STOP_WAIT (11 s), before it sends the final `stopped` line, and its kill first takes up to the 3 s SIGTERM grace. `stop` waited 11 s for that line, so a lock busy for more than about 5 s made it re-probe a still-answering supervisor and report "did not answer the shutdown ... nothing was signalled or recorded" for a stop that happened. Its wait now adds SUPERVISOR_STOP_WAIT (22 s in all). The supervisor's record-then-reply order is kept, so stop_reason stays deterministic; reordering would race it against stop's own record. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HRWwk1oVv9oLFxjb9cSiCm
A `start` launcher saves its supervisor record only after the spawned __monitor publishes. A `stop` in that window loaded a state with no record, skipped the socket sweep and signalled the lock holder: the launcher, which has no SIGTERM handler, died and left the supervisor and its agent running unrecorded while stop reported success. A loaded state with no record now sweeps this root and phase's sockets as a missing or unloadable one does: one live socket is stopped through it before any lock holder is considered, several are the existing refusal. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HRWwk1oVv9oLFxjb9cSiCm
The command table still said `--clean --phase N` clears a phase unconditionally; since 3c0aeef it refuses while the supervisor may be live, as the prose section already says. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HRWwk1oVv9oLFxjb9cSiCm
There was a problem hiding this comment.
Graphify reviewed this change.
Code graph out of date: Health delta baseline: last indexed commit 71a5192, 198 commit(s) behind this PR's base. Health delta baseline was built by an older engine whose node ids do not match this review's — delta is approximate. Impact and hotspot numbers below may be wrong.
Worth a look — the grounded gate found no coupling regressions or blocking issues, but 1 advisory finding(s) below merit a look before merge.
Not checked: tests were not run; formal verification proved 0 of 131 changed function(s) (131 not verified).
Formal verification. PR-changed functions: 0/131 verified (0 proven, 0 may-equivalent, 0 distinguished) · 131 not verified (25 unsupported, 106 over cap).
Not verified on this run: a\_deleted\_root\_contributes\_nothing\_to\_the\_reachable\_set (unsupported), agreeing\_facts (unsupported), build\_phase\_facts (unsupported), cleanup (unsupported), doctor (unsupported), doctor\_finds\_a\_real\_stray\_and\_never\_signals\_it\_across\_two\_runs (unsupported), doctor\_is\_read\_only\_on\_a\_mismatched\_project (unsupported), doctor\_json\_body (unsupported), doctor\_json\_body\_carries\_planning\_doc\_staleness\_as\_a\_third\_key (unsupported), doctor\_json\_body\_carries\_stray\_processes\_as\_a\_fourth\_key (unsupported), and 15 more; 106 changed function(s) beyond the cap and 0 skipped when the time budget ran out.
Graphify review — findings
Replaces the shell-script monitor with a detached __monitor supervisor that owns the agent in its own process group, publishes a per-launch Unix socket, answers ping and shutdown, and runs the advance in-process when the agent exits on its own. Liveness becomes ALIVE / STALE / GONE from probing that socket instead of trusting a recorded pid, and devflow stop tries a gate rejection first, then the socket (with a guarded SIGTERM/SIGKILL backstop for stale ones), then the lock holder, never signalling a pid read from state. Gate waits inside a supervisor's advance now poll about once a second and unwind within about a second of a stop, and cleanup --force and recover --clean lean on the same liveness answer to decide whether a phase is safe to remove.
Worth a look
cleanup --forceremoves the worktree when the probe failed, breaking fail-closed —crates/devflow-cli/src/commands.rs:952· Escalate · medium- NOT verified (no proof, no reproducing execution) — needs human review
Review partial — this diff was larger than one review pass covers, so later files were not reviewed; some findings may be missing.
Analysis details — impact, health, verification
Impact & health
Graphify review
Impact — 2487 functions depend on the 2414 functions this change touches.
Health — this change adds coupling hotspots:
- new:
hermetic_devflow()— 80 callers, 4 callees - new:
handle_validate_outcome()— 31 callers, 8 callees - new:
advance_with()— 7 callers, 21 callees - new:
supervise_stream_child()— 12 callers, 11 callees - new:
reconcile_phase()— 16 callers, 7 callees - new:
resume()— 15 callers, 7 callees - new:
run_preflight()— 15 callers, 5 callees - new:
clean_report()— 7 callers, 10 callees - …and 173 more — each is listed as a finding
Verification — 2487 functions in the blast radius were not formally verified this run (proofs are advisory here).
Health delta baseline: last indexed commit 71a5192, 198 commit(s) behind this PR's base.
Health delta baseline was built by an older engine whose node ids do not match this review's — delta is approximate.
Gate & verification
graphify gate
PASS — objectively clean (no health regressions, tests not run — proofs not run this pass (advisory)). Grounded, not self-assessed.
Advisory (not blocking):
- verification_scope: 2487 function(s) in the blast radius were not formally verified this run
Risky patterns (advisory)
- medium
no-stderr-to-stdoutincrates/devflow-core/src/monitor.rs:3296: stderr redirected to stdout: ( cat <&3 > /dev/null; printf 'EOF\n' > '{eof}' ) > /dev/null 2>&1 & - medium
no-stderr-to-stdoutincrates/devflow-core/src/monitor.rs:4615: stderr redirected to stdout: ( cat <&3 > /dev/null; printf 'EOF\n' > '{eof}' ) > /dev/null 2>&1 & - medium
no-stderr-to-stdoutincrates/devflow-core/src/monitor.rs:5458: stderr redirected to stdout: "{preamble}sleep 60 </dev/null >/dev/null 2>&1 &\n\ - medium
no-stderr-to-stdoutincrates/devflow-core/src/monitor.rs:5731: stderr redirected to stdout: "{preamble}sh -c 'printf %s \"${R_F_VARIABLE}\" > \"$1\"; exec sleep 60' sh '{env_out}' </dev/null >/dev/null 2>&1 &\n\ - medium
no-stderr-to-stdoutincrates/devflow-core/src/monitor.rs:5808: stderr redirected to stdout: "exec 2>&1\n" - medium
no-stderr-to-stdoutincrates/devflow-core/src/monitor.rs:6918: stderr redirected to stdout: "sleep 300 </dev/null >/dev/null 2>&1",
Docs that may be stale (advisory)
CHANGELOG.md§ 2.11.0 — 2026-08-27 (lines 40-44): references changed symbolsagent,phaseCHANGELOG.md§ Added (lines 45-51): references changed symbolsagent,phaseCHANGELOG.md§ Added (lines 66-72): references changed symbolslaunch,runCHANGELOG.md§ What's new (lines 114-129): references changed symbolsagent,gitCHANGELOG.md§ What's new (lines 174-200): references changed symbolsagent,release,runCHANGELOG.md§ 2.5.0 — 2026-08-15 (lines 207-222): references changed symbolsgit,startCHANGELOG.md§ What's new (lines 223-271): references changed symbolsphase,release,run,startCHANGELOG.md§ Fixed (lines 272-292): references changed symbolsgitCHANGELOG.md§ Changed — breaking (lines 305-333): references changed symbolsevents,git,label,phase,stateCHANGELOG.md§ Removed — breaking (lines 334-343): references changed symbolsgit
…and 10 more.
Formal verification
Could not verify: Could not verify a\_deleted\_root\_contributes\_nothing\_to\_the\_reachable\_set.
The verifier did not have enough to check a\_deleted\_root\_contributes\_nothing\_to\_the\_reachable\_set, so it is saying so rather than guessing. No false assurance is the whole point.
Guarantee: No guarantee either way, this is an honest abstention, not a pass.
Note: Reason: unit-returning functions have no value to compare
Could not verify: Could not verify agreeing\_facts.
The verifier did not have enough to check agreeing\_facts, so it is saying so rather than guessing. No false assurance is the whole point.
Guarantee: No guarantee either way, this is an honest abstention, not a pass.
Note: Reason: unsupported return type: PhaseFacts
Could not verify: Could not verify build\_phase\_facts.
The verifier did not have enough to check build\_phase\_facts, so it is saying so rather than guessing. No false assurance is the whole point.
Guarantee: No guarantee either way, this is an honest abstention, not a pass.
Note: Reason: unsupported return type: PhaseFacts
Could not verify: Could not verify cleanup.
The verifier did not have enough to check cleanup, so it is saying so rather than guessing. No false assurance is the whole point.
Guarantee: No guarantee either way, this is an honest abstention, not a pass.
Note: Reason: unsupported return type: Result<(),CliError>
Could not verify: Could not verify doctor.
The verifier did not have enough to check doctor, so it is saying so rather than guessing. No false assurance is the whole point.
Guarantee: No guarantee either way, this is an honest abstention, not a pass.
Note: Reason: unsupported return type: Result<(),CliError>
Could not verify: Could not verify doctor\_finds\_a\_real\_stray\_and\_never\_signals\_it\_across\_two\_runs.
The verifier did not have enough to check doctor\_finds\_a\_real\_stray\_and\_never\_signals\_it\_across\_two\_runs, so it is saying so rather than guessing. No false assurance is the whole point.
Guarantee: No guarantee either way, this is an honest abstention, not a pass.
Note: Reason: unit-returning functions have no value to compare
Could not verify: Could not verify doctor\_is\_read\_only\_on\_a\_mismatched\_project.
The verifier did not have enough to check doctor\_is\_read\_only\_on\_a\_mismatched\_project, so it is saying so rather than guessing. No false assurance is the whole point.
Guarantee: No guarantee either way, this is an honest abstention, not a pass.
Note: Reason: unit-returning functions have no value to compare
Could not verify: Could not verify doctor\_json\_body.
The verifier did not have enough to check doctor\_json\_body, so it is saying so rather than guessing. No false assurance is the whole point.
Guarantee: No guarantee either way, this is an honest abstention, not a pass.
Note: Reason: could not parse Rust function
Could not verify: Could not verify doctor\_json\_body\_carries\_planning\_doc\_staleness\_as\_a\_third\_key.
The verifier did not have enough to check doctor\_json\_body\_carries\_planning\_doc\_staleness\_as\_a\_third\_key, so it is saying so rather than guessing. No false assurance is the whole point.
Guarantee: No guarantee either way, this is an honest abstention, not a pass.
Note: Reason: unit-returning functions have no value to compare
Could not verify: Could not verify doctor\_json\_body\_carries\_stray\_processes\_as\_a\_fourth\_key.
The verifier did not have enough to check doctor\_json\_body\_carries\_stray\_processes\_as\_a\_fourth\_key, so it is saying so rather than guessing. No false assurance is the whole point.
Guarantee: No guarantee either way, this is an honest abstention, not a pass.
Note: Reason: unit-returning functions have no value to compare
Could not verify: Could not verify doctor\_json\_is\_a\_single\_object\_with\_environment\_and\_reconciliation.
The verifier did not have enough to check doctor\_json\_is\_a\_single\_object\_with\_environment\_and\_reconciliation, so it is saying so rather than guessing. No false assurance is the whole point.
Guarantee: No guarantee either way, this is an honest abstention, not a pass.
Note: Reason: unit-returning functions have no value to compare
Could not verify: Could not verify live\_run\_refusal.
The verifier did not have enough to check live\_run\_refusal, so it is saying so rather than guessing. No false assurance is the whole point.
Guarantee: No guarantee either way, this is an honest abstention, not a pass.
Note: Reason: signature changed ((phase: PhaseId, verb: LaunchVerb, live: &[(&str,u32)]) -> String) → ((phase: PhaseId, verb: LaunchVerb, sockets: &[LiveSocket], agent: Option<u32>) -> String) — a single call cannot feed both versions
Could not verify: Could not verify reachable\_pids\_are\_excluded\_from\_both\_the\_findings\_and\_the\_reap\_candidates.
The verifier did not have enough to check reachable\_pids\_are\_excluded\_from\_both\_the\_findings\_and\_the\_reap\_candidates, so it is saying so rather than guessing. No false assurance is the whole point.
Guarantee: No guarantee either way, this is an honest abstention, not a pass.
Note: Reason: unit-returning functions have no value to compare
Could not verify: Could not verify reconcile\_phase.
The verifier did not have enough to check reconcile\_phase, so it is saying so rather than guessing. No false assurance is the whole point.
Guarantee: No guarantee either way, this is an honest abstention, not a pass.
Note: Reason: unsupported return type: Vec<PhaseFinding>
Could not verify: Could not verify reconcile\_phase\_ordering\_is\_input\_order\_independent.
The verifier did not have enough to check reconcile\_phase\_ordering\_is\_input\_order\_independent, so it is saying so rather than guessing. No false assurance is the whole point.
Guarantee: No guarantee either way, this is an honest abstention, not a pass.
Note: Reason: unit-returning functions have no value to compare
Could not verify: Could not verify recover\_cmd.
The verifier did not have enough to check recover\_cmd, so it is saying so rather than guessing. No false assurance is the whole point.
Guarantee: No guarantee either way, this is an honest abstention, not a pass.
Note: Reason: unsupported return type: Result<(),CliError>
Could not verify: Could not verify recovery\_hints.
The verifier did not have enough to check recovery\_hints, so it is saying so rather than guessing. No false assurance is the whole point.
Guarantee: No guarantee either way, this is an honest abstention, not a pass.
Note: Reason: signature changed ((state: &State, liveness: Liveness) -> Vec<String>) → ((state: &State, liveness: &PhaseLiveness) -> Vec<String>) — a single call cannot feed both versions
Could not verify: Could not verify refuse\_launch\_over\_a\_live\_run.
The verifier did not have enough to check refuse\_launch\_over\_a\_live\_run, so it is saying so rather than guessing. No false assurance is the whole point.
Guarantee: No guarantee either way, this is an honest abstention, not a pass.
Note: Reason: signature changed ((project_root: &Path, phase: PhaseId, verb: LaunchVerb) -> Result<(),CliError>) → ((project_root: &Path, phase: PhaseId, verb: LaunchVerb, cache_dir: Option<&Path>) -> Result<(),CliError>) — a single call cannot feed both versions
Could not verify: Could not verify refuse\_resume\_over\_a\_live\_run.
The verifier did not have enough to check refuse\_resume\_over\_a\_live\_run, so it is saying so rather than guessing. No false assurance is the whole point.
Guarantee: No guarantee either way, this is an honest abstention, not a pass.
Note: Reason: unsupported return type: Result<(),CliError>
Could not verify: Could not verify refuse\_start\_over\_a\_live\_run.
The verifier did not have enough to check refuse\_start\_over\_a\_live\_run, so it is saying so rather than guessing. No false assurance is the whole point.
Guarantee: No guarantee either way, this is an honest abstention, not a pass.
Note: Reason: unsupported return type: Result<(),CliError>
Could not verify: Could not verify registry\_reachable\_pids.
The verifier did not have enough to check registry\_reachable\_pids, so it is saying so rather than guessing. No false assurance is the whole point.
Guarantee: No guarantee either way, this is an honest abstention, not a pass.
Note: Reason: unsupported return type: HashSet<u32>
Could not verify: Could not verify render\_reconciliation\_json.
The verifier did not have enough to check render\_reconciliation\_json, so it is saying so rather than guessing. No false assurance is the whole point.
Guarantee: No guarantee either way, this is an honest abstention, not a pass.
Note: Reason: unsupported return type: serde_json::Value
Could not verify: Could not verify stage\_launched\_ts\_reflects\_event\_age\_not\_phase\_started\_at.
The verifier did not have enough to check stage\_launched\_ts\_reflects\_event\_age\_not\_phase\_started\_at, so it is saying so rather than guessing. No false assurance is the whole point.
Guarantee: No guarantee either way, this is an honest abstention, not a pass.
Note: Reason: unit-returning functions have no value to compare
Could not verify: Could not verify status.
The verifier did not have enough to check status, so it is saying so rather than guessing. No false assurance is the whole point.
Guarantee: No guarantee either way, this is an honest abstention, not a pass.
Note: Reason: unsupported return type: Result<(),CliError>
Could not verify: Could not verify status\_reading\_monitor\_liveness\_writes\_no\_state\_and\_no\_event.
The verifier did not have enough to check status\_reading\_monitor\_liveness\_writes\_no\_state\_and\_no\_event, so it is saying so rather than guessing. No false assurance is the whole point.
Guarantee: No guarantee either way, this is an honest abstention, not a pass.
Note: Reason: unit-returning functions have no value to compare
· 20 grounded finding(s) anchored inline below; 113 more finding(s) on lines outside this diff (see the check run); 48 additional anchorable finding(s) not shown (cap).
| /// `DEVFLOW_CACHE_DIR` all point inside `home`, with every repo-local and | ||
| /// repo-redirecting `GIT_*` variable removed. `home` must outlive the child | ||
| /// and any monitor it launches. | ||
| pub fn hermetic_devflow(home: &HermeticHome) -> Command { |
There was a problem hiding this comment.
hermetic_devflow()
80 callers depend on it (afferent coupling).
Grounded coupling-delta finding (deterministic), not an LLM guess.
| @@ -425,12 +432,13 @@ fn dry_run_sweep_reports_no_waiter_gates_as_left_alone() { | |||
|
|
|||
| #[test] | |||
| fn start_wedge_arm_interrupted_start_leaves_a_define_gate_nothing_answers() { | |||
There was a problem hiding this comment.
start_wedge_arm_interrupted_start_leaves_a_define_gate_nothing_answers()
fans out to 7 callees (efferent coupling).
Grounded coupling-delta finding (deterministic), not an LLM guess.
| @@ -382,6 +395,7 @@ esac | |||
|
|
|||
| #[test] | |||
| fn start_defaults_to_worktree() { | |||
There was a problem hiding this comment.
start_defaults_to_worktree()
fans out to 6 callees (efferent coupling).
Grounded coupling-delta finding (deterministic), not an LLM guess.
| @@ -441,6 +457,7 @@ fn start_defaults_to_worktree() { | |||
| /// phase's worktree, which always starts at ahead=0, behind=0. | |||
| #[test] | |||
| fn start_worktree_mode_ignores_main_checkout_divergence() { | |||
There was a problem hiding this comment.
start_worktree_mode_ignores_main_checkout_divergence()
fans out to 7 callees (efferent coupling).
Grounded coupling-delta finding (deterministic), not an LLM guess.
| @@ -500,6 +519,7 @@ fn start_worktree_mode_ignores_main_checkout_divergence() { | |||
|
|
|||
| #[test] | |||
| fn start_no_worktree_uses_feature_branch() { | |||
There was a problem hiding this comment.
start_no_worktree_uses_feature_branch()
fans out to 6 callees (efferent coupling).
Grounded coupling-delta finding (deterministic), not an LLM guess.
| @@ -1893,12 +1998,14 @@ fn hermes_nonzero_exit_does_not_advance() { | |||
| /// does not falsely advance, and gates when killed. | |||
| #[test] | |||
| fn hermes_hung_process_is_detected_not_left_running() { | |||
There was a problem hiding this comment.
hermes_hung_process_is_detected_not_left_running()
fans out to 7 callees (efferent coupling).
Grounded coupling-delta finding (deterministic), not an LLM guess.
| /// fragments; the verb's common fragments and every removed sentence are | ||
| /// checked here. `refusal` is the first assertion's message and names the | ||
| /// arm. | ||
| fn assert_start_refuses_live_run( |
There was a problem hiding this comment.
assert_start_refuses_live_run()
fans out to 6 callees (efferent coupling).
Grounded coupling-delta finding (deterministic), not an LLM guess.
| } | ||
|
|
||
| #[test] | ||
| fn start_refuses_a_phase_whose_recorded_monitor_is_alive() { |
There was a problem hiding this comment.
start_refuses_a_phase_whose_recorded_monitor_is_alive()
fans out to 6 callees (efferent coupling).
Grounded coupling-delta finding (deterministic), not an LLM guess.
| } | ||
|
|
||
| #[test] | ||
| fn start_refuses_a_phase_whose_recorded_agent_is_alive() { |
There was a problem hiding this comment.
start_refuses_a_phase_whose_recorded_agent_is_alive()
fans out to 6 callees (efferent coupling).
Grounded coupling-delta finding (deterministic), not an LLM guess.
| @@ -586,6 +751,7 @@ fn start_replaces_a_leftover_state_whose_recorded_processes_are_dead() { | |||
|
|
|||
| #[test] | |||
| fn start_proceeds_when_no_state_exists_even_if_the_agent_pid_file_names_a_live_process() { | |||
There was a problem hiding this comment.
start_proceeds_when_no_state_exists_even_if_the_agent_pid_file_names_a_live_process()
fans out to 6 callees (efferent coupling).
Grounded coupling-delta finding (deterministic), not an LLM guess.
`kill(-pgid, 0)` also finds zombies. Under a PID 1 that never reaps (a GitHub Actions job container runs `tail -f /dev/null`), a killed agent's orphaned children stay zombies for the whole job, so 18 stop/shutdown tests failed on PR #225 while passing in the pre-push gate, whose PID 1 is bash and reaps. Add `test_support::live_group_members` / `process_is_live` (the non-zombie rule the backstop census already applies) and route every test-side group probe through it. The recover backstop's `group_empties_within` now uses the backstop census too, so it no longer keeps a sidecar for a group that holds only zombies. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RGzZZbfn3r9638yptqPL47
A job container's PID 1 is `tail -f /dev/null`, which never reaps orphans; the pre-push gate's and every real host's PID 1 does. Give Test and Sequential 2-CPU check a reaping init so CI runs the suite under the same PID 1 behaviour as the local gate. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RGzZZbfn3r9638yptqPL47
`kill(-pgid, 0)` also finds zombies. Under a PID 1 that never reaps (a GitHub Actions job container runs `tail -f /dev/null`), a killed agent's orphaned children stay zombies for the whole job, so 18 stop/shutdown tests failed on PR #225 while passing in the pre-push gate, whose PID 1 is bash and reaps. Add `test_support::live_group_members` / `process_is_live` (the non-zombie rule the backstop census already applies) and route every test-side group probe through it. The recover backstop's `group_empties_within` now uses the backstop census too, so it no longer keeps a sidecar for a group that holds only zombies. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RGzZZbfn3r9638yptqPL47
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RGzZZbfn3r9638yptqPL47
Phase 50 shipped to develop as PR #225 (e5239ec). Conflict in .planning/ROADMAP.md: both branches filed a 999.162 after the split. The phase branch's (gate response outliving its gate, 2026-10-19) keeps the number; the workspace's later "Adopt wsx for Repo Operations" (2026-10-20) is renumbered 999.168 with its phase directory. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RGzZZbfn3r9638yptqPL47
Phase 50: Addressable Monitor Liveness (SUPV-01)
"Is this run's monitor alive?" is now answerable from disk state alone, with no PID read, in three distinguishable states (GONE, STALE, ALIVE). A stalled unattended run is recoverable by design rather than by diagnosis.
Breaking change:
refactor(50-16)!removesState's monitorpidfield (devflow-corepublic API). This is the milestone's planned breaking change; DevFlow has no users needing state migration.Also folds backlog items 999.128, 999.134, 999.138, 999.139 and 999.141.
How this branch was cut
scripts/cut-pr-branch.shfromfeature/phase-50: 100 phase-scoped code commits replayed, 113 planning-only commits excluded, zero forbidden paths.(50-review)fix/docs commits from the external review, andtest(48): check the response before the state in the park control(a flake fix made during this phase).feature/phase-50outside workspace-only paths: only.gitignoreand the plan-bashism linter update (a pre-phase workspacechore(lint)commit, deliberately not part of this PR).CI fix in this PR
The first CI run failed 18 stop/shutdown tests on GitHub only. Cause: a job container's PID 1 is
tail -f /dev/null, which never reaps orphans, so a killed agent's orphaned children stayed zombies and the tests'kill(-pgid, 0)"group gone?" probes read them as alive. Reproduced locally: the same 18 fail undertailas PID 1 and pass under--init.fix(50): every group probe now counts only live (non-zombie) members viatest_support::live_group_members, the rule the backstop census already used;recover'sgroup_empties_withinuses the backstop census too. After the fix the full workspace suite passes undertailas PID 1 (1660 passed, 0 failed).ci(50):TestandSequential 2-CPU checkrun withoptions: --init, matching the pre-push gate's reaping PID 1.graphify-labs review triage
One advisory finding,
cleanup --forceremoving a worktree on a failed probe (commands.rs:952): deliberate per plan 50-12 (--forceoverrides ProbeFailed like STALE; ALIVE or a live agent is refused even with--force). The realistic trigger, a ping during a stop's kill grace, is tracked in backlog 999.164 item 2. Coupling notes skipped: the bot's baseline ismainat v2.12.0, 1,967 commits behinddevelop.Verification
stoppedreply says 143 (999.166 item 6).🤖 Generated with Claude Code
https://claude.ai/code/session_01RGzZZbfn3r9638yptqPL47