Skip to content

feat(50)!: addressable monitor liveness without a PID (SUPV-01) - #225

Merged
denniyahh merged 112 commits into
developfrom
feature/phase-50-pr
Oct 22, 2026
Merged

denniyahh merged 112 commits into
developfrom
feature/phase-50-pr

Conversation

@denniyahh

@denniyahh denniyahh commented Oct 22, 2026 •

Copy link
Copy Markdown
Owner

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)! removes State's monitor pid field (devflow-core public 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.sh from feature/phase-50: 100 phase-scoped code commits replayed, 113 planning-only commits excluded, zero forbidden paths.
  • Cherry-picked on top, because the script's scope filter does not match them: the nine (50-review) fix/docs commits from the external review, and test(48): check the response before the state in the park control (a flake fix made during this phase).
  • Diff against feature/phase-50 outside workspace-only paths: only .gitignore and the plan-bashism linter update (a pre-phase workspace chore(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 under tail as PID 1 and pass under --init.

  • fix(50): every group probe now counts only live (non-zombie) members via test_support::live_group_members, the rule the backstop census already used; recover's group_empties_within uses the backstop census too. After the fix the full workspace suite passes under tail as PID 1 (1660 passed, 0 failed).
  • ci(50): Test and Sequential 2-CPU check run with options: --init, matching the pre-push gate's reaping PID 1.

graphify-labs review triage

One advisory finding, cleanup --force removing a worktree on a failed probe (commands.rs:952): deliberate per plan 50-12 (--force overrides 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 is main at v2.12.0, 1,967 commits behind develop.

Verification

  • Phase verification: passed, 8/8 roadmap success criteria (re-verified 2026-10-21 after gap closure).
  • Known residuals, filed in the backlog, not fixed here:
    • Ship finalization retry loop checks for a stop only once before the loop. A stop accepted in the same second the operator approves a retry gate after a failed first merge/tag/push lets that finalization run (999.166 item 7, deferred by operator). Unattended runs cannot reach this gate alone.
    • A stop arriving after the agent has already exited kills no group (999.164 item 5); the exit file keeps the agent's code 0 while the stopped reply says 143 (999.166 item 6).
  • Not established: stop paths were exercised by tests, not by a live run; timing-based tests were run once on one machine (flake rate unknown); macOS is compile-only.

🤖 Generated with Claude Code

https://claude.ai/code/session_01RGzZZbfn3r9638yptqPL47

denniyahh and others added 30 commits October 22, 2026 08:02
- 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
denniyahh and others added 22 commits October 22, 2026 08:02
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

@graphify-labs graphify-labs Bot left a comment •

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 --force removes 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-stdout in crates/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-stdout in crates/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-stdout in crates/devflow-core/src/monitor.rs:5458: stderr redirected to stdout: "{preamble}sleep 60 </dev/null >/dev/null 2>&1 &\n\
  • medium no-stderr-to-stdout in crates/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-stdout in crates/devflow-core/src/monitor.rs:5808: stderr redirected to stdout: "exec 2>&1\n"
  • medium no-stderr-to-stdout in crates/devflow-core/src/monitor.rs:6918: stderr redirected to stdout: "sleep 300 </dev/null >/dev/null 2>&1",

Docs that may be stale (advisory)

…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 {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Health regression — 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() {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Health regression — 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() {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Health regression — 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() {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Health regression — 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() {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Health regression — 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() {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Health regression — 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(

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Health regression — 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() {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Health regression — 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() {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Health regression — 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() {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Health regression — 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.

denniyahh and others added 2 commits October 22, 2026 11:45
`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
@denniyahh
denniyahh merged commit e5239ec into develop Oct 22, 2026
12 checks passed
@denniyahh
denniyahh deleted the feature/phase-50-pr branch October 22, 2026 18:30
denniyahh added a commit that referenced this pull request Oct 23, 2026
`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
denniyahh added a commit that referenced this pull request Oct 23, 2026
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RGzZZbfn3r9638yptqPL47
denniyahh added a commit that referenced this pull request Oct 23, 2026
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
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