Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
112 commits
Select commit Hold shift + click to select a range
0710033
test(50-02): add failing 999.141 home-isolation and scan tests
denniyahh Oct 17, 2026
d48c95e
feat(50-02): implement hermetic devflow builder and source scan
denniyahh Oct 17, 2026
2d4fbf5
test(50-02): run start_lock_e2e and stop_e2e through the hermetic bui…
denniyahh Oct 17, 2026
4f18d28
test(50-03): run phase7_cli and gate e2e suites hermetically
denniyahh Oct 17, 2026
3213f53
test(50-03): migrate five more e2e suites and pin dry-run stray pass
denniyahh Oct 17, 2026
98bb5cc
test(50-04): add failing test for recycled-pid lock reclaim
denniyahh Oct 17, 2026
06db94c
feat(50-04): reclaim a lock whose live pid was recycled
denniyahh Oct 17, 2026
3fedc45
test(50-04): pin the repair at a recycled-pid lock end to end
denniyahh Oct 17, 2026
811d6f5
test(50-03): skip suite_reap_audit under cargo-nextest
denniyahh Oct 17, 2026
c13feca
test(50-05): add failing probe tests for Gone, Stale and Alive
denniyahh Oct 17, 2026
dc98124
feat(50-05): probe a recorded socket into Gone, Stale or Alive
denniyahh Oct 17, 2026
7931f64
test(50-05): pin probe totality and the per-fixture liveness table
denniyahh Oct 17, 2026
861ed1f
test(50-05): pin the sun_path bound and the atomic socket publish
denniyahh Oct 17, 2026
18e6c95
test(50-06): add failing tests for sidecar and state-less sweep
denniyahh Oct 17, 2026
103354c
feat(50-06): sidecar and read-only sweep by root and phase
denniyahh Oct 17, 2026
3ec0637
test(50-06): add failing tests for read-only sweep and shutdown client
denniyahh Oct 17, 2026
002fc02
feat(50-06): acting launch-file removal and shutdown client
denniyahh Oct 17, 2026
cfe873b
test(50-06): add failing test for persisted supervisor record
denniyahh Oct 17, 2026
dd2f130
feat(50-06): persist State::supervisor
denniyahh Oct 17, 2026
29a7660
test(50-21): migrate three e2e suites to the hermetic builder
denniyahh Oct 17, 2026
820956c
test(50-21): finish hermetic e2e migration and pin binary naming
denniyahh Oct 17, 2026
71201d9
test(50-07): add failing tracer tests for the supervisor runtime
denniyahh Oct 17, 2026
12396cb
feat(50-07): stop the agent group on a socket shutdown and report it
denniyahh Oct 17, 2026
ff46146
test(50-07): prove a stop racing natural completion never advances (V…
denniyahh Oct 17, 2026
7f7305c
test(50-07): fail V-14 instead of hanging when a window is unreached
denniyahh Oct 17, 2026
d234bb7
test(50-07): add failing signal-pipe and TERM-ignoring member tests
denniyahh Oct 17, 2026
629c55b
feat(50-07): deliver SIGTERM and SIGINT as stop events via a self-pipe
denniyahh Oct 17, 2026
c484f8d
test(50-07): pin the final line a parked shutdown caller hears
denniyahh Oct 17, 2026
98209aa
test(50-08): add failing capture-shape tracer for run_supervised_stage
denniyahh Oct 17, 2026
5c5c86d
feat(50-08): run the capture shape under an addressed supervisor
denniyahh Oct 17, 2026
c5480fd
refactor(50-08): share the stream I/O loop with run_supervised_stage
denniyahh Oct 17, 2026
805a759
test(50-08): add failing stream-shape, own-group and liveness tests
denniyahh Oct 17, 2026
223550a
feat(50-08): run the stream shape on the same supervisor body
denniyahh Oct 17, 2026
1956ebf
test(50-08): prove stop semantics and the R-A..R-M table for both shapes
denniyahh Oct 17, 2026
60ba2a7
test(50-08): gate the V-08 constants to Linux with their test
denniyahh Oct 17, 2026
69f2a06
test(50-09): add failing tracer test for the stale-socket backstop
denniyahh Oct 17, 2026
fb44315
feat(50-09): tear down a validated group behind a STALE socket
denniyahh Oct 17, 2026
cf25c8b
test(50-09): add failing tests for the refusal text and unsound targets
denniyahh Oct 17, 2026
b460408
feat(50-09): describe every backstop decision for the CLI
denniyahh Oct 17, 2026
b437bb0
test(50-09): pin the classifier table and the kernel-capability check
denniyahh Oct 17, 2026
d481b01
test(50-10): add failing tracer for the in-crate monitor harness entry
denniyahh Oct 17, 2026
26fdc5b
feat(50-10): re-enter in-crate stream monitors at a test harness entry
denniyahh Oct 17, 2026
6633b34
test(50-10): run every in-crate child test with a hermetic cache dir
denniyahh Oct 17, 2026
1306fc7
test(50-24): add failing tracer for the publish wait's teardown
denniyahh Oct 17, 2026
5712478
feat(50-24): end an unpublished supervisor and the agent group it sta…
denniyahh Oct 17, 2026
3bafbd4
test(50-24): add failing tests for the pre-publish agent teardown
denniyahh Oct 17, 2026
4e68909
feat(50-24): end a pre-publish agent group in the stage and launcher
denniyahh Oct 17, 2026
1b810f9
test(50-11): add failing tests for the published supervisor launch
denniyahh Oct 17, 2026
1a6e59f
feat(50-11): launch both shapes under the published Rust supervisor
denniyahh Oct 17, 2026
033bd34
test(50-11): end launched runs through the supervisor socket
denniyahh Oct 17, 2026
b82f138
test(50-11): prove the cache-dir socket, CLI SIGTERM stop and capture…
denniyahh Oct 17, 2026
ab5845f
test(50-12): add failing tests for probe-answered status liveness
denniyahh Oct 17, 2026
c4b6e6c
feat(50-12): answer status and cleanup liveness from the supervisor s…
denniyahh Oct 17, 2026
6a0f0a6
test(50-12): add failing tests for doctor's supervisor check
denniyahh Oct 17, 2026
b96771c
feat(50-12): report supervisor liveness in doctor and doctor --json
denniyahh Oct 17, 2026
7ccc11d
test(50-12): add failing tests for the orphan sweep and 2.x census
denniyahh Oct 17, 2026
d9248b5
feat(50-12): add doctor's read-only orphan supervisor sweep
denniyahh Oct 17, 2026
3e7a3af
test(50-13): add failing tests for the publish-before-unlink handoff
denniyahh Oct 17, 2026
8a97aeb
feat(50-13): publish the next supervisor before the old one unlinks
denniyahh Oct 17, 2026
f2f8a6d
test(50-13): add failing tests for advance safe points and stopped state
denniyahh Oct 17, 2026
08d130e
feat(50-13): honour a stop at the advance safe points
denniyahh Oct 17, 2026
d19be85
test(50-13): add failing tests for the cancellable gate wait
denniyahh Oct 17, 2026
978a63e
feat(50-13): make the advance gate wait cancellable by a stop
denniyahh Oct 17, 2026
7641c39
test(50-12): add failing tests for a ping closed under the probe
denniyahh Oct 18, 2026
37fa4e2
fix(50-12): re-probe a socket whose ping connection closes under it
denniyahh Oct 18, 2026
f6bf3d3
test(50-14): add failing tests for stop through the supervisor socket
denniyahh Oct 18, 2026
d0c3dfe
feat(50-14): stop a live run through its supervisor socket
denniyahh Oct 18, 2026
3618631
test(50-14): add failing tests for stop on a STALE or unprobeable socket
denniyahh Oct 18, 2026
7fa533b
feat(50-14): tear a STALE supervisor socket down through the backstop
denniyahh Oct 18, 2026
7f02a05
test(50-14): add failing tests for stop --launch and recover's live r…
denniyahh Oct 18, 2026
59c7940
feat(50-14): add stop --launch and refuse recover --clean on a live run
denniyahh Oct 18, 2026
bbc9fa9
test(50-14): prove --launch on a deleted root and the recorded launch
denniyahh Oct 18, 2026
85e1dc7
test(50-23): add failing tests for clearing the record on a failed la…
denniyahh Oct 18, 2026
91d252c
feat(50-23): clear the supervisor record on a refused or failed launch
denniyahh Oct 18, 2026
be9bd7a
test(50-23): add failing tests for the publish save and its stops
denniyahh Oct 18, 2026
cd9235c
feat(50-23): shut down an unrecorded successor and record a stop it took
denniyahh Oct 18, 2026
695d483
test(50-23): add failing tests for forwarding a late stop to N+1
denniyahh Oct 18, 2026
d005710
feat(50-23): forward a stop that reaches N after safe point 3 to N+1
denniyahh Oct 18, 2026
8545c11
test(50-15): add failing tests for the launch guard over a corrupt state
denniyahh Oct 18, 2026
f5989de
feat(50-15): find a live run through its socket when the state will n…
denniyahh Oct 18, 2026
9bd3714
test(50-15): prove two racing starts yield one live supervisor (V-12)
denniyahh Oct 18, 2026
e97d5fc
refactor(50-22): record only the supervisor on a launch
denniyahh Oct 18, 2026
e3b3613
refactor(50-22): drop the CLI's last monitor pid reads
denniyahh Oct 18, 2026
b46ece1
refactor(50-16)!: remove State's monitor pid field
denniyahh Oct 18, 2026
8b291c4
fix(50-16): skip the 0o000 socket-dir tests when running as root
denniyahh Oct 18, 2026
e5cc48d
docs(50-17): document run liveness and the socket-based stop arms
denniyahh Oct 18, 2026
55046bb
docs(50-17): state socket supervision is unverified on macOS
denniyahh Oct 18, 2026
ac8a8a5
docs(50-17): show the in-process advance in the agent lifecycle diagram
denniyahh Oct 18, 2026
5b6e218
fix(50-09): point the backstop's group hint at pgrep -g, not ps -g
denniyahh Oct 18, 2026
70014bf
test(50-02): wait out a run's unrecorded supervisors before the root …
denniyahh Oct 19, 2026
e3493ea
test(50-16): run the unlistable socket-dir tests as root too
denniyahh Oct 19, 2026
c1ee968
docs(50-13): describe the supervisor's cancellable gate poll
denniyahh Oct 19, 2026
28b57c5
fix(50-14): name `devflow stop` in cleanup's live-run refusal
denniyahh Oct 19, 2026
0c66b16
fix(50-11): name the capture supervisor in the auto-mode refusal
denniyahh Oct 19, 2026
f090a5e
test(50): reproduce a stop lost to the --yes-ship merge (CR-01)
denniyahh Oct 21, 2026
8d16e3f
fix(50): honour a stop before yes-ship approval and finalization (CR-01)
denniyahh Oct 21, 2026
d6a7335
test(50-26): add a test-support hold before the stream-thread wait
denniyahh Oct 21, 2026
7af56e2
test(50-26): reproduce a supervisor stuck on a held stdout (WR-03)
denniyahh Oct 21, 2026
010f4f9
fix(50-26): end the stream wait at the agent's exit and bound the thr…
denniyahh Oct 21, 2026
d3dfe5c
test(50-26): pin the stream wait fix for stopped and real-binary runs
denniyahh Oct 21, 2026
ce86f30
test(48): check the response before the state in the park control
denniyahh Oct 18, 2026
6ce762a
fix(50-review): refuse a placeholder identity in the backstop
denniyahh Oct 19, 2026
f1fcff1
docs(50-review): correct the stop row's lock and recording claims
denniyahh Oct 19, 2026
22596f2
fix(50-review): sweep for live sockets when the state file is missing
denniyahh Oct 19, 2026
0eb2eba
fix(50-review): make recover --clean use the launch guard's liveness
denniyahh Oct 19, 2026
3f94335
fix(50-review): accept only new_launch_id's format from disk
denniyahh Oct 19, 2026
1046865
fix(50-review): never replace an existing sidecar at a launch's name
denniyahh Oct 19, 2026
566e3ee
fix(50-review): wait out a stopped supervisor's lock wait in stop
denniyahh Oct 19, 2026
3062c27
fix(50-review): stop an unrecorded supervisor through its socket
denniyahh Oct 19, 2026
2635dbf
docs(50-review): say recover --clean --phase refuses a live run
denniyahh Oct 19, 2026
a265527
fix(50): count only live members when probing whether a group is gone
denniyahh Oct 22, 2026
673e9fb
ci(50): run the suite's container jobs with --init
denniyahh Oct 22, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,14 @@ jobs:
runs-on: ubuntu-24.04
container:
image: mcr.microsoft.com/devcontainers/rust:2.0.13-1-bookworm
# --init: a job container's PID 1 is otherwise `tail -f /dev/null`, which
# never reaps orphans, so a killed agent's orphaned children stay
# zombies for the whole job. scripts/check-in-container.sh's PID 1 is
# bash, which reaps, and so does any real host. Without this the suite
# runs under a PID 1 neither of them has (PR #225: 18 failures, all
# zombies). The tests' liveness probes skip zombies too; this keeps CI
# matching the local gate rather than relying on that alone.
options: --init
timeout-minutes: 30
steps:
- uses: actions/checkout@v7
Expand Down Expand Up @@ -96,6 +104,8 @@ jobs:
runs-on: ubuntu-24.04
container:
image: mcr.microsoft.com/devcontainers/rust:2.0.13-1-bookworm
# --init: see the Test job.
options: --init
timeout-minutes: 45
# REQUIRED, not stylistic. A `run:` step in a CONTAINER job defaults to
# `sh -e {0}`, not `bash -e {0}` as it would on a bare runner — and this
Expand Down
84 changes: 61 additions & 23 deletions ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -220,12 +220,27 @@ files per gated stage, all written atomically (temp file + rename):
(`Gates::ack()`) once the response is consumed, so a poller knows to clean
up.

`run_gate()` (`crates/devflow-cli/src/main.rs`) writes the gate, fires the
notify hook (`fire_gate_notify()` — runs `$DEVFLOW_GATE_NOTIFY_CMD` via
`sh -c` with gate metadata as environment variables, never interpolated
`run_gate()` (`crates/devflow-cli/src/pipeline_gate.rs`) writes the gate,
fires the notify hook (`fire_gate_notify()` — runs `$DEVFLOW_GATE_NOTIFY_CMD`
via `sh -c` with gate metadata as environment variables, never interpolated
into the command string; a silent no-op when the variable is unset), then
blocks in `Gates::poll_response()` with exponential backoff (1s → 2s → …
capped at 60s) until a response appears or `DEVFLOW_GATE_TIMEOUT_SECS`
waits in `wait_for_gate_response()` (`crates/devflow-cli/src/pipeline_launch.rs`),
which polls the response file one of two ways:

- **Inside a supervisor's advance** (its stop token is engaged, 50-13): the
run's owner reads `gate_wait` in a `ping` reply for the length of the wait,
and `Gates::poll_response_cancellable()` reads the response file about once
a second (every sleep is capped at 1s) and checks the stop token before each
sleep. A stop — a `shutdown` request on the supervisor's socket, or
SIGTERM/SIGINT to the supervisor — ends the wait within about a second: the
gate's files are removed, `stopped` is recorded in the state, which is kept
for `devflow resume` (not an abort), and the advance unwinds without acting
on the gate. A response already on disk wins over a stop.
- **Anywhere else** (no stop token: the hidden `devflow advance` run from a
shell, or a gate a foreground command fires): `Gates::poll_response()` with
exponential backoff (1s → 2s → … capped at 60s), which nothing cancels.

Either wait ends when a response appears or `DEVFLOW_GATE_TIMEOUT_SECS`
(default 259200 = 3 days, held equal to
`DEVFLOW_GATE_MAX_UNATTENDED_AGE_SECS` so `devflow gate sweep` cannot reap a
gate this poll is still watching) elapses. `GateAction::from_response()` turns an
Expand All @@ -237,24 +252,47 @@ skipping unparsable entries rather than failing.
## Monitor daemon

`crates/devflow-core/src/monitor.rs` is the core automation primitive — no
cron, no scheduler, no agent cooperation. `spawn_monitor()` spawns a
**detached child process that owns the agent**:

1. Launches the agent (`program` + `args`), redirecting stdout/stderr to
separate phase capture files (stderr is kept out of the stdout capture so
it can never corrupt the `DEVFLOW_RESULT` parse) and recording the agent
PID.
2. Waits for the agent to exit and records its exit code to
`.devflow/phase-NN-exit`.
3. Runs `devflow advance <project> --phase N` to advance the stage machine.
The phase is threaded in at spawn time, so `advance`'s identity never
depends on a shared state singleton — under `devflow parallel`, each
phase's monitor advances exactly its own stage.

Because the monitor outlives the `devflow start`/`devflow advance`
invocation that spawned it, agent stdout keeps flowing into the capture
file and the exit code is still reaped after the CLI process returns.
`spawn_monitor()` is the single way an agent process is spawned (23d).
cron, no scheduler, no agent cooperation. `spawn_monitor()` re-execs the CLI
as its hidden `__monitor` subcommand, **a detached supervisor process in its
own process group that owns the agent**, for both launch shapes:

- **capture** (`MonitorLaunch::Legacy`): the agent's stdin is `/dev/null` and
its stdout and stderr go straight to the phase capture files (stderr is kept
out of the stdout capture so it can never corrupt the `DEVFLOW_RESULT`
parse). Non-Claude adapters other than Antigravity, the legacy Claude launch
opt-out and a checkpoint resume use it.
- **stream** (`MonitorLaunch::PipeOwning`): the supervisor owns the agent's
stdin and stdout pipes, delivers the prompt as the first turn and applies the
idle timeout. Claude and Antigravity `stream-json` launches use it.

The supervisor (`run_supervised_stage()`):

1. Spawns the agent (`program` + `args`) in the agent's own process group and
records its PID.
2. Publishes a per-launch Unix socket under `registry::cache_dir()/sock/`,
with a JSON sidecar beside it naming the launch (project root, phase,
agent process group, start time, boot id). The socket path is recorded in
the phase's `state.json` as `state.supervisor`; liveness (ALIVE / STALE /
GONE) is answered by probing that path, never from a pid.
3. Answers `ping` and `shutdown` on the socket, and treats SIGTERM and SIGINT
sent to it as a `shutdown`: the agent's group is ended and no advance runs.
4. Waits for the agent to exit and records its exit code to
`.devflow/phase-NN-exit`. A stream supervisor stops reading when the agent
exits, not when its stdout closes: when a process the agent started still
holds that stdout, it waits at most 3 s for its pipe threads, closes the
capture file, records a `stream_wait_bounded` event, and signals nothing.
5. On the agent's natural exit, runs the advance in-process. The phase is
threaded in at spawn time, so the advance's identity never depends on a
shared state singleton — under `devflow parallel`, each phase's supervisor
advances exactly its own stage. When the advance launches the next stage,
that stage's supervisor publishes its socket before this one unlinks its
own, so the hand-off leaves no gap with neither socket published.

`spawn_monitor()` returns once the new supervisor answers a ping with this
launch's id. Because the supervisor outlives the CLI process that launched
it, agent stdout keeps flowing into the capture file and the exit code is
still reaped after that process returns. `spawn_monitor()` is the single way
an agent process is spawned (23d).

## Worktree model

Expand Down
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading