Repository navigation
fix(tui): Linux follow-ups for the Rust agent runtime removal (plan r8, run on WSL2) - #9
Merged
Merged
Conversation
… keep a bad meta description from breaking the session (audit follow-up 8, F7-01, P7-17) F7-01: <project>/.pi-python/workflows/*.py became slash commands in any project, trusted or not, with a description of the repository's choosing, and could take the place of the user's own saved workflow of the same name, or of /workflows. They are now one more thing project trust covers: - trust_fingerprint.gated_project_resources lists .pi-python/workflows (when it holds *.py files and is not the user's own <pi home>/workflows), so the ACP question names it, the saved answer is bound to its contents, and the "skipped" notice lists it. Headless runs and default_project_trust = "never" leave it out. - ExtensionAPI.project_trusted (HarnessBridge.project_trusted; only an actual True counts, anything else is no) tells an extension whether the project is trusted. The workflow extension leaves the project's directory out of WorkflowStore(include_project=False) unless it is. - A saved workflow can no longer take the place of /workflows or a built-in workflow name, trusted project or not. P7-17: a non-string meta.description reached AvailableCommand, which refused it, and the agent re-sends the command list with every prompt until a send succeeds, so every session/prompt failed. Three layers, each enough on its own: WorkflowStore drops a non-string description / when_to_use; ExtensionAPI.register_command raises TypeError; PiAcpAgent._advertise_commands leaves out a command the schema rejects (with a warning that names it) and sends the rest. Docs: Phase 7 spec (3.2, 4.4, 16), both AGENTS.md (new invariant 9, project trust), README, TUI guide, agent.example.toml, audit tracker (batch 8). The review doc of the other session is not kept. CHANGELOG: the [0.5.0] section is condensed (about 7,300 words down to 2,800): one entry per change, the details stay in the specs and the audit tracker, and related entries (project trust, DeepSeek thinking, home resolution, <git_status>, fetch_url, pi-goal-x, sub-agent routing) are merged. Sections for 0.4.0 and earlier are untouched. Tests: written red first (90 of the new tests failed before the change). 33 hand-made mutants, all killed (two survived the first round; tests added for them). Windows 2151 passed / 14 skipped; WSL 3.12 2150 / 15; WSL 3.11 and 3.13 2131 / 34; ruff 0.16.10 and 0.16.0 clean. Not changed: F7-02 to F7-09 (low impact, and documentation notes) stay open, see the tracker. No tag, no push.
`zypi -p` runs the Python agent as a plain child on the user's terminal. The TUI's agent learns that its client is gone from EOF on its stdin; here stdin is the terminal, so that signal does not exist. Killing only zypi (kill -9, kill <pid>, an IDE's stop button) left the agent and the tools it was running going on the terminal: a `sleep 300` started by the agent was still there afterwards. - zypi names itself to the agent in PI_AGENT_PARENT_PID (print_mode::agent_command). - pi_agent_cli removes the variable first thing in main(), so nothing it starts inherits it. It is used only if the pid is this process's parent, or a process that no longer exists (the parent died while the agent was starting). A live process that is not the parent is a wrapper script in between: the watch stays off. POSIX only. - In -p mode a task polls os.getppid() every 0.5 s; once the parent is not the one named it asks for the same stop as SIGTERM. The turn is cancelled and the process group of every running tool is reaped. -p now also stops on SIGTERM and SIGHUP, with exit status 128 + the signal number. - _StopRequest is the piece both paths share; serve() (the stdio agent) behaves as before. An environment variable rather than a flag, so a zypi and a pi_agent_cli of different versions still work together. Not PR_SET_PDEATHSIG: it is Linux-only, tied to the thread that started the child, and after a SIGKILL it would leave the tools orphaned. Limits: a PI_AGENT_COMMAND that points at a wrapper script keeps the watch off, so killing only zypi still leaves that agent behind. Windows has no counterpart. Tests: 15 pytest cases in packages/pi-agent-cli/tests/test_print_shutdown.py (POSIX only; disabling the watch makes the two that depend on it fail) and one Rust test in pi-pager-bin. Against a built zypi (scripts/tui_pty/print_exit.py, added later in this series), SIGTERM, SIGKILL, SIGHUP and SIGINT sent to zypi alone leave nothing running.
With --sandbox on Linux zypi re-executes itself under bubblewrap. A plain bwrap only waits for its child, so killing bwrap alone (kill $!, timeout, an IDE's stop button, a supervisor that signals just the process it started) left zypi, the Python agent and the agent's running tools on the terminal. Measured with bwrap 0.9.0 on WSL2: SIGKILL, SIGTERM and SIGHUP sent to bwrap all left them alive. bwrap now gets --die-with-parent: the sandboxed child is SIGKILLed when bwrap dies (and bwrap itself when its parent dies). The agent then sees EOF on its stdin and reaps its tools on the way out; all of them are gone within a second. It is a bwrap option, so it goes before `--`; a test pins the position. Trade-off: the child dies by SIGKILL, so when only the outer bwrap is signalled with SIGTERM or SIGHUP zypi cannot restore the terminal (alternate screen, raw mode). Before, the whole tree stayed up instead. Easy to take back: this commit is the flag and its test.
SandboxManager::apply() degrades on purpose: where the platform cannot enforce the profile (on Linux, a kernel without Landlock) or applying it fails, it logs, returns Ok and the run goes on with none of the profile's file or network limits. The user was never told: the status bar label is drawn from profile_name(), which is None both for "no sandbox asked for" and for "a sandbox that did not take". - pi-sandbox keeps the reason (SandboxManager::not_enforced; pi_sandbox::not_enforced once installed) and builds the texts: not_enforced_warning (the profile, the reason, what is lost) and not_enforced_label (`sandbox:workspace (not enforced)`). A build without the `enforce` feature reports it as well. - pi-shell's apply_sandbox prints the warning on stderr at start-up. - The welcome top bar and the session status bar draw the label in the error colour where the label of an applied profile goes. The stderr line is behind the alternate screen by then. Checked under a pseudo-terminal on WSL2 (kernel 5.10, no Landlock): the warning and the label appear. Not exercised: a kernel with Landlock, where the profile is applied and no label may show. Only unit tests cover that side, because apply() would confine the test process for good. Tests: 5 new in pi-sandbox (87 in the crate together with the previous commit).
The TUI's agent child inherited zypi's stderr, which app::run points at /dev/null once the TUI is up, so a Python traceback or warning from the agent went nowhere. The child's stderr is now <home>/logs/agent.stderr.log: - appended to across runs, with one `--- agent started <rfc3339> (zypi pid N) ---` line per spawn so that a run's output can be told apart; - owner-only (0600) on Unix, since a traceback can quote prompts, paths and keys; - rotated at spawn: past 1 MiB it becomes agent.stderr.log.1 (replacing the one before) and a new file starts. The size is not enforced while an agent runs; - when it cannot be opened (the `logs` path is a file, a read-only home) the agent still starts and its stderr is discarded as before, with a tracing warning. `-p` is not affected: there the agent keeps the user's terminal as its stderr. Tests: 6 in acp::spawn::agent_stderr_tests (location, start marker, append, rotation at the limit, file mode, a log that cannot be opened).
Neither change touches production code. - pi-pager, tool_media_same_length_same_mtime_rewrite_retries_failed_load: the stamp that tells a rewrite from an untouched file includes ctime, which comes from the kernel's coarse clock. At CONFIG_HZ=100 (WSL2's 5.10 kernel) a tick is 10 ms, and a rewrite in the same tick as the first stamp carries the same ctime and looks unchanged. The test now lets 25 ms pass between the two writes; a real slow write is never that fast. - pi-pager-minimal, committed_edit_keeps_diff_line_backgrounds: Theme::current() follows a process-wide switch that the terminal-native tests turn on while they hold theme_cache::test_lock(). This test did not take the lock, so with the default thread count it could run in that window, get the native palette and find no diff backgrounds. It takes the lock now. 40 runs each with the default threads, --test-threads=1, the test alone, and (default threads and the test alone) with every CPU busy: no failure.
…fest scripts/tui_baseline.toml is the manifest that scripts/tui_baseline.py, and with it the TUI CI job, runs. Two suites join it and doctor_cmd:: is no longer skipped: - pi-pager: drop skip = ["doctor_cmd::"]. It was there because the voice probe queried the audio input device, which hangs on macOS CoreAudio without microphone access. The probe is gone (plan section 10.10) and the 12 tests pass without an audio device. reference_passed 5552 -> 5577: the 5,559 of the last CI run, plus those 12, plus the 6 agent_stderr tests. - New suite pi-pager-render (--lib; 1,092 passed, 2 ignored on Linux). One test is listed in known_failures: tmux_probe::tests::successful_near_deadline_exit_still_returns_captured_output spends 1.2 s of a 1.5 s budget in a shell script, so it fails when the machine stalls. It failed twice on WSL2, both times on the first run after the binary was linked, and not once in the 94 runs after that, idle or with every core busy. known_failures only reports: the test is named in the report when it fails, and the run is not blocked. - New suite pi-pager-minimal (--lib; 79 passed). - pi-pager-bin: 20 -> 21, for the test that names zypi as the agent's parent in `-p` mode. The numbers are WSL2's, not the runner's. The first CI run with the unskipped doctor_cmd tests and the two new suites decides whether they hold there; if doctor_cmd does not, put the skip back. Whole manifest on WSL2 (python scripts/tui_baseline.py test): exit 0, 10 suites, 8,633 passed, 1 known failure, 22 ignored, 1,469 s.
…l terminal scripts/tui_pty/ runs a built zypi under a pty (a pyte screen reads what it draws) and looks at the process tree it leaves. They are the Linux counterpart of the macOS PTY runs in the plan (sections 10.7 and 10.10). They need a built binary, so they are not part of pytest or CI. - smoke.py: welcome page, one turn, /model, /exit, a restart in the same home with /resume, the agent's stderr log, and with --sandbox PROFILE what the status bar says about the profile. 23 checks on WSL2 (26 with --sandbox workspace). - exit_matrix.py: while the agent runs a long bash tool call, leave zypi six ways (/exit twice, kill -9, SIGTERM, SIGHUP, closing the terminal, Ctrl-C) from two launch styles; nothing may be left running. 12/12 on WSL2 (14/14 with --sandbox workspace). - print_exit.py: `zypi -p` while only zypi gets SIGTERM, SIGKILL, SIGHUP or SIGINT; the agent and its tool call must stop too. 4/4 (the tool survived before the -p fix). - pty_term.py (the driver and the /proc helpers) and zypi_env.py (finds the binary, builds a throw-away PI_HOME, HOME and working directory and an environment from scratch, so what the caller's shell exports, PI_HOME included, never reaches zypi and a real ~/.pi-python is not touched). scripts/tests/test_tui_pty.py covers the helpers: 10 tests on Linux; on Windows 5 pass and 5 are skipped.
The plan's macOS side is merged (#6 to #8); this is the Linux counterpart, run in WSL2 (Ubuntu 24.04, kernel 5.10 without Landlock, bubblewrap 0.9.0). - docs/PLAN/PLAN-RUST-AGENT-RUNTIME-REMOVAL.md: section 10.10 (what was run on Linux, the findings and what was done about each, what is not verified, what is left for the user to decide), the r8 revision-log line and the validation tables. - docs/baselines/tui.md: 10 suites, the numbers measured on WSL2 (not yet confirmed by CI), the tmux known failure, and a section on the PTY drivers. - CHANGELOG.md, [Unreleased]: the agent's stderr log, the not-enforced notice, the `-p` orphan fix, --die-with-parent and its trade-off. - AGENTS.md: how the Rust tests and the PTY drivers are run. packages/pi-agent-cli/AGENTS.md: the agent's stderr and the `-p` parent watch. docs/TUI-AND-CODE-AGENT.md: where the stderr log is and what --sandbox says when it is not in force. Verification of the whole series: scripts/tui_baseline.py test exits 0 (10 suites, 8,633 passed / 1 known failure / 22 ignored); pytest 2,160 passed / 30 skipped on Linux (WSL) and 2,156 passed / 34 skipped on Windows; ruff check and ruff format --check clean. The numbers of the new suites and of the unskipped doctor_cmd tests are WSL2's; the first TUI CI run on the pull request confirms or corrects them.
… microphone fake_standalone_facts_compose_through_shared_view asserts issue_count() == 1, but collect_report_with also runs apply_voice_probe, which on Linux looks for pw-record, parec or arecord on PATH. A host without one, as the CI runner is, gets a voice.no-input-device issue on top: 2 instead of 1. The first TUI CI run of PR #9 failed on exactly that; WSL2 has all three recorders, which is why it passed there. The test now leaves that finding out of the count (it checks the facts it composes, not the machine it runs on). Verified by running the test binary directly with PATH=/nonexistent: before the change 11 passed and 1 failed with left: 2, right: 1; after it 12/12, and 12/12 with the recorders on PATH as well. The docs said doctor_cmd:: needs no audio device and that r5 had removed apply_voice_probe. Neither is true (the function is still there and is called on every report), so the manifest comment, the baselines page and plan section 10.10 are corrected and now carry the numbers of that CI run: 8,633 passed, every suite equal to the WSL2 count except this one test.
zy1233
added a commit
that referenced
this pull request
Oct 10, 2026
#9 went into main as one squash commit, 60033a4, so main no longer shares #9's commits with this branch, and GitHub could not merge #10 into it: files that both sides changed near each other conflict, although they changed them the same way. The tree of 60033a4 is the tree of d84d98a, #9's last commit and the base of this branch, so main holds nothing this branch lacks. The merge keeps this branch's tree as it is (-s ours); the tree stays the one that CI ran on for eef675b.
zy1233
added a commit
that referenced
this pull request
Oct 10, 2026
…ates (plan r9) (#10) ## Summary **Stacked on #9** (base `codex/linux-wsl-r8`). It adds sixteen commits on top of `d84d98a`: the three phase 0 gates of `docs/PLAN/PLAN-RUST-AGENT-RUNTIME-REMOVAL.md` that were still open after r8 (0.3 on the Rust side, 0.7, 0.8), and the parts of phase 1 that need no decision. Section 10.11 and appendix A of the plan have the full record. **No behaviour-changing decision is made here.** Where a decision is needed (the permission-mode mapping, the `--session-id` contract, what happens to `zypi export`, ...) the plan writes a proposal and says what it would cost. Section 10.11 ends with twelve of them, each with my preference; none is implemented. What the branch does: - **0.3, e2e against the real agent.** `python_agent_e2e.rs`: 10 tests drive the pager's ACP client against the real Python agent, and `tui-ci.yml` installs the agent first and sets `PI_E2E_REQUIRED=1` so a missing Python fails instead of passing silently. Its first run found two product bugs, both fixed here: the reason an agent failed to start was swallowed into `channel closed`, and `connect()` hung when the agent exited at once. - **0.7, can the pager lose its own disk reads (ADR1)?** Measured: with 1,000 sessions on Windows the first page of `session/list` went from 2.0 s to 0.11 s and `session/load` from 2.0 s to 0.12 s (a header is read without reading the file, and the repo lists a page at a time). ADR1's override condition did not trigger. The plan inventories the pager's 13 disk-read call sites in 8 files, adds a 13-case PTY matrix of the resume flags (`scripts/tui_pty/resume_matrix.py`: 9 PASS / 4 GAP, the GAPs are the work ADR1 would do) and a landing design with the sandbox ordering problem and a PR slicing. - **0.8, capability matrix and ACP version pairing** (appendix A): every row marked decided / proposed / open, the upgrade rules, and a per-method table of what each end supports. - **1.P1:** `session/list` is paged with an opaque cursor, the pager's picker follows `nextCursor`, and a session is looked up by id without reading every file. - **1.P4:** the pager sends no `mcp_servers`; the agent says which ones a client passed and that it ignored them. - **1.P7:** the permission round trip and `session/cancel` are covered over real stdio. - **Agent exits on its own:** the pager says so and why, instead of a bare `channel closed`. - **1.P3 input, not a change:** `scripts/tui_pty/mode_probe.py` records which permission modes the Python agent honours (12 scenarios on WSL2). Plan mode restricts nothing, Auto is always-approve in the agent, the settings "Default" is dropped, and of the six `--permission-mode` values only `bypassPermissions` does anything. ## Decisions that were open Plan section 10.11, last list. The three that unblock the most, and what became of them: 1. Start ADR1 with PR 1: the Python side only (`_meta.sessionId` honoured by `session/new`, the session's cwd in the `session/load` response). No TUI change, least risk. **Done in #11.** 2. 1.P3: hide Plan, Auto and Default (Shift+Tab becomes Normal <-> Always-Approve) and accept only the `--permission-mode` values that do something. Today Plan gives users a false sense of safety. **Done in #12.** 3. The sandbox ordering (the first of the plan's three options): drop "restore the sandbox profile a session saved", which never applies to a Python session. **Still open.** ## Not verified - The Rust e2e tests and the PTY scripts ran on Linux (WSL2) only; the latency numbers on Windows measure the Python side only. Cold disk, network file systems and more than 1,000 sessions were not measured. - Cross-directory `--resume <id>` (which directory the TUI uses for `@` search, diffs and git info), the settings "Default" path under a pty, and the `-m` / `--model` flag. - The landing design is on paper; everything uses the mock LLM. ## Review guide | Commit | What | |---|---| | `28fed94` | 1.P7: permission round trip and cancel over real stdio | | `54a38fe`, `1b9fb47` | harness: read a header without the file; list a page at a time; find by id | | `e944ed5` | `scripts/session_list_bench.py` | | `c1f02c8`, `9f7bc35` | 1.P1: Python pages `session/list`; the pager follows `nextCursor` | | `935e4b8`, `7becb49`, `9798e1d` | 1.P4: the agent names the ignored MCP servers; the pager sends none; docs | | `affda9e` | the pager notices an agent that exits, and says why a start failed | | `3fc182b` | 0.3: `python_agent_e2e.rs`, `tui-ci.yml`, manifest | | `babd75f` | 0.7: `resume_matrix.py` | | `bee3a36` | manifest: `pi-pager` `reference_passed` 5,602 | | `679cccb`, `d426253` | 1.P3 input: `mode_probe.py` | | `eef675b` | docs: plan 10.11, appendix A, B.4, `docs/baselines/tui.md`, CHANGELOG, AGENTS files | #9 was squash-merged first and this PR's base then moved to `main`. The PR still lists #9's commits, and GitHub could not merge it because files that both sides changed near each other conflict. `b0d001b` merges `main` into the branch with the `ours` strategy: the tree of `main` is the tree of #9's last commit, the base of this branch, so `main` holds nothing the branch lacks, and the branch's tree is unchanged. ## Verification - **CI.** The run on `eef675b` is green (TUI CI [run 38061734062](https://github.com/zy1233/pi-python/actions/runs/38061734062)). The new install step in `tui-ci.yml` ran for the first time, `python_agent_e2e`'s 10 tests really ran (a missing Python would fail them), and the 10 suites add up to **8,659 passed / 0 failed / 22 ignored** with `pi-pager` at 5,602: the numbers measured locally. `cargo test` took 23 m 19 s (32 m 40 s on #9), `Gates + cargo check` 7 m 9 s, Ruff 9 s, pytest on 3.11 / 3.12 / 3.13 about 2 minutes each. `tui-ci.yml` now also runs when `pi_agent_cli/**` changes. After #9 was squash-merged, `main` was merged into this branch (`b0d001b`, keeping the branch's tree, which is the tree of `eef675b`) and CI ran again: green too ([run 38074449935](https://github.com/zy1233/pi-python/actions/runs/38074449935); `cargo test` 21 m 48 s, 8,659 passed again). - Whole manifest on WSL2 (`python scripts/tui_baseline.py test`, scrubbed environment, `PI_E2E_PYTHON` and `PI_E2E_REQUIRED=1`): exit 0, 10 suites, **8,659 passed, 0 failed, 22 ignored** = #9's CI result (8,634) + 25 new `pi-pager` tests (10 e2e, 8 agent-exit, 6 paging, 1 for `mcp_servers`). `pi-pager`: 5,602. - pytest: 2,352 passed / 35 skipped on Linux (WSL2, Python 3.12.3), 2,351 passed / 36 skipped on Windows (Python 3.12.7); `scripts/tests/test_tui_pty.py` with `pyte`: 19 passed. - `ruff check .` and `ruff format --check .`: clean (226 files) with the project's Ruff 0.16.10 and with CI's pinned 0.16.0. - `resume_matrix.py`: 9 PASS / 4 GAP / 0 FAIL. `mode_probe.py`: 12 scenarios.
zy1233
added a commit
that referenced
this pull request
Oct 10, 2026
…ds the merge order The ADR1 PR 1 branch and the mode_a branch were local when the plan and the baseline report were written, and said "not pushed" in eight places. They are pushed now: #11 (codex/adr1-pr1-python) on top of #10, #12 (codex/phase1-mode-a) on top of #11. Those places name the PRs instead; the r9 statements that were true when they were written stay, as the r10 entry already says. The r10 entry in the revision log gets the last step. The user asked to sync main to origin and merge the processed PRs. The stack has four (#9, #10, #11, #12), and the upper ones have nothing to merge into until the lower ones are in, so all four are merged, bottom-up, one squash each (the convention of #7 and #8), with the base moved to main before each merge and the tree of main compared with the branch's afterwards. Local main is 9 commits ahead of origin/main; all of them except one older docs commit (superseded by the revised one in #9) are already in #9's branch, so main is not pushed: after the merges it is reset to origin/main, and the old main stays on a backup branch. Docs only: the plan, docs/baselines/tui.md.
zy1233
added a commit
that referenced
this pull request
Oct 10, 2026
…rged (plan r10) PR #12's CI run counted what the local WSL2 run had: 8,683 passed / 0 failed / 22 ignored over the ten suites, pi-pager 5,626, each suite equal to its reference_passed. The baseline report and the comment in the manifest said the next CI run would confirm that; they now say it did, with the run. #10 (after main was merged into its branch) and #11 were green too, 8,659 over the ten suites. The r10 entry of the revision log gets what happened when the stack was merged. #9 was a squash, so #10's branch still held #9's original commits and GitHub reported conflicts in files both sides had changed near each other, in the same way. main was merged into #10's branch with the ours strategy (the tree of main is the tree of #9's last commit, so nothing is dropped), #11 and #12 were each rebased onto the new main before their squash, and after every merge the tree of main was compared with the branch's. A squash of the lower PR of a stack makes the next one conflict unless it is brought up to main first. Docs and a comment in the manifest only; the code is the tree CI already ran on.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
The macOS half of
docs/PLAN/PLAN-RUST-AGENT-RUNTIME-REMOVAL.mdis merged (#6 to #8). This is the Linux half, run in WSL2 (Ubuntu 24.04, kernel 5.10 without Landlock, bubblewrap 0.9.0): build the TUI, run its Rust suites, drive a realzypiunder a pseudo-terminal, and look at what it leaves behind. That turned up six things, and this PR fixes them. The plan's section 10.10 has the full record (what was run, what was found, what is not verified).What this PR is for in CI. The numbers below were measured on WSL2, not on the runner. This PR's first TUI CI run is what confirms or corrects them:
doctor_cmd::is no longer skipped inpi-pager(12 tests). The skip existed because the voice probe hung on macOS CoreAudio; the probe is gone and the tests pass here without an audio device. If they do not hold on the runner, put the skip back (one line inscripts/tui_baseline.toml).pi-pager-render(1,092 passed, 2 ignored) andpi-pager-minimal(79 passed).pi-pagerreference_passed5552 -> 5577;pi-pager-bin20 -> 21.tmux_probe::tests::successful_near_deadline_exit_still_returns_captured_output, is listed inknown_failures. It spends 1.2 s of a 1.5 s budget in a shell script, so it fails when the machine stalls (twice on WSL2, both on the first run after linking, never in the 94 runs after). It is reported when it fails and does not block.CI result. The runs on
d84d98aare green (TUI CI run 38052332014): 10 suites, 8,634 passed / 0 failed / 22 ignored, each suite equal to itsreference_passed. That is the local 8,633 plus the one known failure, which passed on the runner, soknown_failuresstays as a safety net and was not needed this time.doctor_cmd::held on the runner without the skip.cargo testtook 32 m 40 s,Gates + cargo check8 m 21 s, Ruff 8 s, pytest on 3.11 / 3.12 / 3.13 about 2 minutes each; the release build baseline is skipped by design.The six findings
zypi -pruns the Python agent as a plain child on the user's terminal, so the agent never sees EOF when only zypi is killed (kill -9,kill <pid>, an IDE's stop button). The agent and asleep 300it had started kept running.PI_AGENT_PARENT_PID;pi_agent_clipollsos.getppid()every 0.5 s in-pmode and stops as it does on SIGTERM (cancel the turn, reap every tool's process group).-palso stops on SIGTERM / SIGHUP now (exit status 128 + signal).--sandbox, zypi re-executes itself under bubblewrap, and a plain bwrap only waits for its child. SIGKILL / SIGTERM / SIGHUP to bwrap alone left zypi, the agent and its tools running.bwrap --die-with-parent, as its own commit.SandboxManager::apply()degrades on purpose (no Landlock, or applying fails): it logs, returnsOk, and the run goes on with none of the profile's limits. The user was never told.sandbox:<profile> (not enforced)in the error colour./dev/nullonce the TUI was up, so a Python traceback went nowhere.<home>/logs/agent.stderr.log: appended, one marker line per spawn,0600, rotated at 1 MiB at spawn, and a log that cannot be opened does not stop the agent.-pkeeps the terminal.theme_cache::test_lock()in a test that raced the terminal-native tests.doctor_cmd::and had nopi-pager-render/pi-pager-minimal.-pshutdown) had no repeatable form.scripts/tui_pty/drives a builtzypiunder a pty (apytescreen reads what it draws) and inspects the process tree. They need a built binary, so they are not part of pytest or CI.Decision for you
--die-with-parenthas a cost. The sandboxed child is SIGKILLed when bwrap dies, so when only the outer bwrap gets SIGTERM or SIGHUP, zypi cannot restore the terminal (alternate screen, raw mode). Before the flag, the whole tree stayed up instead. I judged an unrestorable terminal better than a running agent nobody can see, but it is a trade, and the commit is only the flag and its test, so it is easy to revert. If you revert it, the CHANGELOG line and the plan's section 10.10 need the same edit.Not verified
-pwith a wrapper script asPI_AGENT_COMMAND. The watch stays off when the process that started the agent is not the one named, so killing only zypi still leaves that agent behind. Windows has no counterpart for the watch.Review guide
Nine commits. The first is not part of the Linux work: it was on local
mainand never pushed, so it rides along. If you squash-merge, it goes in under this PR's title; a rebase-merge keeps the nine as they are.eafc578c13f7cc-pparent watch (Rust passes the pid; 15 pytest cases, POSIX only; 1 Rust test)d14e6f8--die-with-parentand the test that pins its position before--2d499adpi-sandbox,pi-shell, two render sites; 5 tests)91b5f629489975478aa8a15d2d85scripts/tui_pty/andscripts/tests/test_tui_pty.py4cf989fdocs/baselines/tui.md, CHANGELOG, AGENTS filesVerification
On WSL2, the whole manifest (
python scripts/tui_baseline.py test): exit 0, 10 suites, 8,633 passed, 1 known failure, 22 ignored. pytest: 2,160 passed / 30 skipped on Linux (WSL), 2,156 passed / 34 skipped on Windows.ruff checkandruff format --checkare clean. With a builtzypiunder a pty:smoke.py23 checks (26 with--sandbox workspace),exit_matrix.py12/12 (14/14 with the sandbox),print_exit.py4/4 (the tool survived before the-pfix).