Repository navigation
feat: phase 1 contract work that needs no decision, and the phase 0 gates (plan r9) - #10
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.
…al stdio (plan 1.P7) The stdio contract suite could not reach the tool side of the protocol: its mock LLM never calls a tool, so a permission request, or a cancel with something running, never happened on the wire. tests/test_acp_stdio_tools.py drives tests/_tool_agent.py (the real serve() with a scripted LLM turn that calls one tool) the way the TUI drives the agent, and asserts what the pager depends on: - in ask mode a `write` call is announced as a tool_call update and then put as session/request_permission, before the tool runs: title, kind, raw_input and the two options allow-once / reject-once (the tool had not run when the request arrived); - allow-once runs the tool; reject-once and a `cancelled` outcome do not, and the turn still ends (the model is told); auto mode never asks; - session/cancel ends the prompt as `cancelled` while a question is open (the pager answers it `cancelled` after the cancel) and while a `bash` call runs, which is then reaped (POSIX only); with nothing running, or for a session the agent does not have, it is harmless. _tool_agent.py gains PI_TEST_TOOL_CALL (a JSON tool call replacing the long bash) and PI_TEST_PERMISSION (default auto, as before). Its stream now ends `aborted` when the signal is set before the next LLM call, as the LangChain adapter does; without that a cancelled turn went on to the next scripted call and ended end_turn, which says something about the stream, not the agent. 8 tests (7 on Windows, where the bash one is skipped), plus the 4 shutdown tests that share the helper, all pass on Windows and in WSL2. Mutation checks: an agent that allows whatever the client answers fails the two refusal cases; one that never asks fails 5; one whose cancel does nothing fails the two cancel cases (by timeout).
`read_text_lines(path, max_lines=1)` read and decoded the whole file and then cut the first line off, so listing sessions cost the total size of every session file (one of the files on this machine is 1.85 MB). It now reads only as far as the lines asked for, with the same newline handling as before (universal newlines, then `str.splitlines`), so every result is the same; the tests compare against the old behaviour over awkward line endings and check that bytes after the header are never decoded. `LocalExecutionEnv.list_dir` called `file_info` on every child: a worker-thread round trip and about six `stat` / `realpath` calls each, 1.3 s for a directory of 1000 files on Windows. It is now one `scandir` in one worker thread and one `resolve` for the directory, with the same `FileInfo` for every child (a test compares them child by child, symlinks included). A child that disappears while the directory is read is left out instead of failing the whole listing. Both were found by measuring `session/list` for plan 0.7 (scripts/session_list_bench.py).
`JsonlSessionRepo.list` reads the header of every session file, so a listing, and every
`session/load`, which looked its session up through it, cost the whole directory.
`list_page({limit, after, cwd})` walks the file names newest first (the names start with the
creation time) and reads a header only when the page reaches that file; it stops once the page
is full and the next session is in sight, and says where the next page starts (`next_after`,
a file name, so no state is kept and a cursor survives a restart). A page that carries a
cursor is never followed by an empty one.
`find(id)` picks the candidates from the file names (`...-<id>.jsonl`) and reads only those
headers; only when none of them is the session does it fall back to reading every file, as
`list` would, so a file named by someone else is still found.
`list` itself is unchanged.
…eal agent scripts/session_list_bench.py writes a synthetic PI_HOME (N sessions, a third of them the project's) and times, over stdio as the TUI talks to the agent: start up to `initialize`, the first page of `session/list`, every page, the cross-project listing, `session/load`, and the repo and preview parts in-process. It is how plan 0.7 gets the number ADR1 hangs on (what a list costs once the TUI no longer reads the session files itself).
…lan 1.P1) The picker asked for `session/list` once and ignored `nextCursor`, which was fine while the Python agent answered with everything but would show only the first page once it pages. It now follows the cursor until the agent sends none. The cursor is opaque (ACP): it is handed back exactly as received and never read. The loop also ends on an empty cursor, on a cursor the agent already gave (it would go round forever), and at 100 pages (5000 sessions at the agent's 50 a page); the sessions collected so far are shown and a warning is logged. An error on any page fails the whole fill with the same `seq`, so a picker never silently stops half way. Tests drive a scripted agent over the same channel the real one uses: several pages in order (with the cursor and cwd checked on every request), a single page, a repeated cursor, an empty cursor, the page cap, and an error on a later page. Two mutations (the cursor not sent back; no repeat check) make exactly the two tests that cover them fail.
…up by id (plan 1.P1) `session/list` answered with every session and ignored `cursor`. It now returns SESSION_LIST_PAGE_SIZE (50) sessions, newest first, with `nextCursor` while more follow. The cursor carries the file name the next page continues below, so the agent keeps nothing between requests; a string it did not issue is `invalid_params`. The TUI follows `nextCursor` (previous commit), so its picker still shows everything. `load` / `resume` / `pi/session/delete` found their session by listing all of them; they now ask the repo for that id (one header read). Measured on a synthetic home with the real agent (scripts/session_list_bench.py, medians): Windows, 1000 sessions: first `session/list` 3171 ms -> 113 ms, `session/load` 2084 ms -> 108 ms; WSL2 ext4, 1000 sessions: 492 ms -> 79 ms and 560 ms -> 78 ms. See plan section 10.11.
ACP lets a client hand an agent MCP servers in session/new, session/load and session/resume, and says an agent MUST support stdio ones. The Python agent connects to none (MCP is a Phase 4 non-goal) and used to drop the list without a word, so a user who had set a server up in their editor just found its tools missing. The three handlers now pass the list to the deferred session setup, which logs a warning and sends one agent message once the response has flushed, like the other setup notices: This agent does not connect to MCP servers, so the 2 MCP servers the client passed (a, b) were ignored and their tools are not available in this session. Only the names are used, in the log and in the message (at most 8, each cut at 60 characters, then "and N more"): `env`, `headers`, `args` and `url` routinely hold tokens. Servers given as dicts or as the SDK's models are both read; one without a usable name shows as "(unnamed)". The departure from ACP stays a documented deviation, not a fix: packages/pi-agent-cli/AGENTS.md gets a section saying what the agent does with the list, why the notice is the place to start if MCP is ever implemented, and that `initialize` advertises no mcpCapabilities. tests/test_mcp_notice.py (12): the names and the message for none / one / many / long lists, no token in the message or in the log, and the agent's own new_session, load_session and resume_session driven with a recording client (one notice each; none for an empty list; the session still answers a prompt). Mutation: load_session without the list makes test_load_and_resume_say_it_too fail.
…1.P4) The pager read `.mcp.json`, `~/.claude.json` and Cursor's `mcp.json` (pi_shell `load_mcp_servers`) and put every server it found into the request, including each one's `env` and `headers`. The Python agent connects to none of them, so that only copied tokens into a process that threw them away, and the user was told nothing. Both requests now carry an empty list, and the `session.create.done` log no longer reports a server count. `load_mcp_servers` is left in pi-shell; nothing in the pager calls it any more. The test writes a project `.mcp.json` with a token in it, checks that `load_mcp_servers` still finds it (so the test cannot pass because the file was never read), and drives NewSession and LoadSession through a scripted agent: neither request has an `mcp_servers` entry. Mutation: putting `.mcp_servers(load_mcp_servers(..))` back into both calls makes the test fail (WSL).
…rrive in pages docs/TUI-AND-CODE-AGENT.md still drew MCP in the TUI's box and said nothing about two things the ACP side now does differently: the agent ignores the MCP servers a client passes (and says so in the session), and `session/list` returns 50 sessions at a time with `nextCursor`, which zypi follows.
… failed The stdio bridge waited only for the pager's own stop request. An agent that went away by itself (a crash, a kill, a Python that cannot import pi_agent_cli) went unnoticed: nothing waited on the child, so the pager sat on requests that could not be answered, and the event loop's "agent disconnected" arm (it quits when the connection's cancel fires) never ran. A start that failed was no better: the reason was the bridge thread's return value, which only the final join reads, so `initialize` reported a closed channel and nothing else. The bridge now waits on the child too. When it exits unasked, the bridge lets the reader finish, fires the connection's cancel (the event loop quits), drops its tasks (the pending requests fail instead of waiting for ever) and ends with an error that names the program, how it ended and the last lines of its run in agent.stderr.log. `connect()` returns that error when `initialize` fails, and the process guard prints it after the terminal is restored when the agent went away mid-session. The pager's own exit status is unchanged. Found by the end-to-end test against the real agent (next commit): `false` as the agent made connect() hang, and a program that does not exist was reported only as "channel closed".
…plan 0.3) tests/python_agent_e2e.rs drives pi_pager::acp::connect over real stdio against `python -m pi_agent_cli` (mock model), without a pseudo-terminal: the handshake and a clean stop; a streamed prompt; a session listed with its title and replayed by a later agent; session/list followed by cursor (51 sessions, and an invalid cursor); the permission question and both answers; cancel of a running tool, whose process is reaped; a program that does not exist; an agent that exits at once; a Python without pi_agent_cli; an agent killed in the middle of a turn. PI_E2E_PYTHON names the interpreter. Without it the tests say so and pass; with PI_E2E_REQUIRED=1 (set by tui-ci.yml) a missing interpreter is a failure, so a CI that lost its Python is not green. The manifest runs the file in the pi-pager suite (10 tests on Linux, reference 5,587). tui-ci.yml installs the Python packages for it, and also runs when the agent's own package changes.
…0.7) scripts/tui_pty/resume_matrix.py makes one session through the TUI with the Python agent, then starts zypi again with --continue, --resume <id>, --resume <title>, --session-id and `zypi export`, and says whether each did what its help text promises. Measured on WSL2 against a debug zypi: 9 cases hold and 4 do not. `--resume <title>` finds nothing (it matches the old agent's summaries only), `--session-id` is ignored by the Python agent and its in-use check cannot see Python sessions, and `export` says "not found". The four are recorded as known gaps in the table (reported as GAP, and --strict fails them) for the change that lands ADR1 to close; a gap that has closed shows as FIXED until its text is dropped. --continue works only because the pager's fallback reads the first line of each session file and the Python header happens to carry the same `id` and `cwd` keys: format knowledge the pager should not have. The judging and the table are unit-tested in scripts/tests/test_tui_pty.py; those tests need no zypi and run on any platform.
5,587 only added the new end-to-end tests to the last CI count. A local run of the pi-pager suite through the baseline runner on Linux (scrubbed environment, the end-to-end tests required, not skipped) passed 5,602 with 13 ignored: the 5,577 of CI run 38052332014, plus 10 end-to-end tests, 8 agent-exit tests, 6 fetch_session_list tests and the no-mcp_servers test. The next CI run confirms it.
…ours (plan 1.P3) The TUI offers Normal, Always-Approve, Plan and Auto (Shift+Tab, --always-approve), and tells the agent with a pi/yolo_mode_changed notification, or for Plan with session/set_mode. The Python agent understands the notification only. mode_probe.py starts a fresh zypi per way of choosing a mode against the scripted agent (permission mode ask, first turn calls write) and prints whether a question appeared and whether the file was written unasked. Today: Normal asks; --always-approve and Always-Approve do not; back to Normal asks again; Plan asks and, once allowed, writes (it restricts nothing: set_mode is answered "method not found" and the TUI only logs that); Auto never asks (the TUI describes a classifier that does not exist behind the Python agent). The probe judges nothing; it is the evidence for plan section 10.11 and the baseline a change to the mode mapping is compared with.
The probe covered the Shift+Tab cycle and --always-approve. `zypi --help` lists six values for --permission-mode, and the probe now starts zypi with each of them. On WSL2 only bypassPermissions changes anything: it behaves as --always-approve (status bar "always-approve", no question, the file is written). default, acceptEdits, auto, dontAsk and plan behave exactly as no flag at all: the status bar shows nothing and the write is asked about, so acceptEdits does not accept edits, dontAsk does not stop asking, and plan does not enter Plan. The reason is on the agent's side of the wire: the TUI puts yoloMode / autoMode in the _meta of session/new, which the Python agent does not read. A mode chosen at the start works only where the TUI answers permission questions itself (always-approve). The probe still judges nothing; the plan (section 10.11) cites its table.
… a decision (plan r9) The three gates still open after r8 (0.3 on the Rust side, 0.7, 0.8) are written down, together with the parts of phase 1 that need no decision. Nothing here changes behaviour that was not already committed; every behaviour-changing decision is a proposal for the user (section 10.11, twelve items), not a change. - docs/PLAN/PLAN-RUST-AGENT-RUNTIME-REMOVAL.md: section 10.11 (PR #9's second CI run, the e2e suite and the two product bugs it found, the cold-start latency before and after, the 13 disk-read call sites in 8 files, the 13-case resume matrix, the landing design of ADR1 with the sandbox ordering and the PR slicing, the 12-scenario permission-mode probe, what changed, what was run, what was not, the twelve proposals); appendix A (capability matrix with every row marked decided / proposed / open, the ACP version pairing and the upgrade rules, the per-method table); B.4 (how to reproduce); the checklist ticks and the r9 revision-log line. - docs/baselines/tui.md: pi-pager 5,577 -> 5,602 (the four new groups of tests), the full ten-suite run on WSL2 (8,659 passed / 0 failed / 22 ignored = CI's 8,634 + 25), the path-filter change. - CHANGELOG.md, [Unreleased]: the session list is read in pages, the pager no longer sends mcp_servers and the agent says it ignores them, the agent-exit fixes. - AGENTS.md: the resume matrix, the mode probe and python_agent_e2e.rs (PI_E2E_PYTHON). packages/pi-agent-cli/AGENTS.md: what the pager shows when the agent exits on its own. Verification: scripts/tui_baseline.py test exits 0 (10 suites, 8,659 passed / 0 failed / 22 ignored); pytest 2,352 passed on Linux (WSL) and 2,351 on Windows; scripts/tests/test_tui_pty.py 19 passed with pyte; ruff check and ruff format --check clean with the venv's 0.16.10 and with CI's pinned 0.16.0. All of it is WSL2's or Windows' own run; this branch has not been pushed, so the first CI run confirms or corrects the numbers and the new e2e install step.
#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
…on works (ADR1 PR 1, plan r10) (#11) ## Summary **Stacked on #10**, which is merged first; this branch is then rebased onto `main` and its base moved there. One commit: ADR1 PR 1 of `docs/PLAN/PLAN-RUST-AGENT-RUNTIME-REMOVAL.md` (section 10.11 has the design and the PR slicing, section 10.12 the record). It is the Python half only: **nothing in the TUI changes**. A client that never sends `_meta.sessionId` sees one difference: the `_meta["pi/cwd"]` key in the responses that open a session. `zypi --session-id <uuid>` sends the id in `session/new`'s `_meta.sessionId`, and the agent ignored it and picked its own, so the flag did nothing on the Python agent (`resume_matrix.py` case 10). This makes it work, and gives the TUI two things it will rely on once it stops reading session files, plus one library helper for the first: - **`session/new` takes `_meta.sessionId` as the session's id.** It has to be a UUID in any form `uuid.UUID` reads, and it is stored in canonical form (lower case, hyphens). Anything else is `invalid_params`, because the id ends up in a file name (`../../outside` is refused). An id some session already has is `invalid_params` too, whatever directory that session belongs to: `JsonlSessionRepo.create` accepts an id but does not refuse a used one, so a second file with the same id would fit beside the first and either could answer a later `session/load`. Requests for the same id at once make one session (an asyncio lock). - **`session/new`, `load` and `resume` answer with `_meta["pi/cwd"]`**, the directory the session works in. `load` and `resume` bind the one saved with the session, not the client's, so a client that only knows where it was started (`zypi --resume <id>` from another directory) can learn the real one. Nothing in the TUI reads it yet. - **`JsonlSessionRepo.has_session(id)`** answers "is this id taken?" without reading every header. `find` answers the same, but for an id nobody has (the usual case) it reads all of them: 1.7 s at 5,000 sessions. `has_session` opens a file only when its name does not already say it belongs to another session (`<timestamp>-<id>.jsonl`, as the repo writes them), so a free id costs the directory listing: 0.07 s at 1,000 sessions and 0.3 s at 5,000, on Windows and WSL2 alike. It takes one thing on trust: a file renamed by hand to carry another id. ## Not done, not verified - **The pager does not use any of this yet.** It does not read `pi/cwd` and does not pre-check a duplicate `--session-id`, so case 11 of the matrix stays a GAP: the pager's in-use check reads the old layout and never sees the session, sends `session/new`, the agent refuses, and the TUI shows `Session creation failed: Invalid params` and stays up instead of exiting 1. That is PR 2. - Two agent processes on one home are not covered by the lock (it is in-process). - Nearly all of what `has_session` still costs is `LocalExecutionEnv.list_dir` building a `FileInfo` per entry (`os.listdir` of the same 5,000 files takes 2-11 ms). It is shared with `session/list` and left alone (proposal 13 in the plan). - Python 3.11 and 3.13 run only in CI. Mocks and scripted agents only, no real LLM; the real `zypi` ran on Linux (WSL2) only. ## Review guide One commit, 12 files, +499 / -37. | Files | What | |---|---| | `packages/pi-agent-harness/.../session/jsonl_repo.py`, `tests/test_harness_session_page.py` | `has_session`, seven tests | | `packages/pi-agent-cli/pi_agent_cli/agent.py` | `_meta.sessionId` on `session/new`, the per-id lock, `_meta["pi/cwd"]` on `new` / `load` / `resume` | | `packages/pi-agent-cli/tests/test_session_chosen_id.py`, `test_acp_stdio_contract.py` | 18 + 2 tests: id honoured and visible after a restart, canonical form, bad values, id in use (other spelling, after a restart, another directory), five concurrent requests make one, `pi/cwd` in all three responses | | `scripts/session_list_bench.py`, `scripts/tui_pty/resume_matrix.py`, `scripts/tests/test_tui_pty.py`, `scripts/tui_pty/README.md` | benchmark numbers for `new` and a free id; matrix case 10 loses its gap and case 11's gap text says what is seen now; the test that pinned "every fresh-id case is a gap" goes | | `docs/PLAN/...`, `CHANGELOG.md`, `packages/pi-agent-cli/AGENTS.md` | plan section 10.12, one CHANGELOG line, the agent's contract | ## Verification - **CI.** The run on the commit as first pushed (`1a9a336`) is green: Ruff, pytest on 3.11 / 3.12 / 3.13 (about 2 minutes each), and the TUI job, which runs because `pi_agent_cli/**` changed: 10 suites, **8,659 passed / 0 failed / 22 ignored** with `pi-pager` at 5,602, which includes the 10 `python_agent_e2e` tests that drive the pager's ACP client against this agent ([TUI CI run 38073976529](https://github.com/zy1233/pi-python/actions/runs/38073976529); `cargo test` 32 m 53 s). The branch was then rebased onto `main` after #10's squash; the tree of the rebased commit is the tree of `1a9a336`, so that result stands. - pytest: 2,378 passed / 36 skipped on Windows (Python 3.12.7), 2,379 passed / 35 skipped on Linux (WSL2, ext4 clone). 27 new tests. - `ruff check .` and `ruff format --check .`: clean. - Eleven one-line mutations of the new code, each caught by at least one test. - Against the real `zypi` on WSL2 (`scripts/tui_pty/resume_matrix.py`): `--session-id <fresh uuid>` now starts the session with that id; the matrix goes from 9 PASS / 4 GAP to 10 PASS / 3 GAP, no FAIL.
zy1233
added a commit
that referenced
this pull request
Oct 10, 2026
…P3 (a))
The Python agent does two things about a tool call: it asks, or it approves everything. The TUI
still offered Plan, Auto, a settings "Default" and six --permission-mode values. Measured against
the real zypi on WSL2 (plan section 10.11): Plan restricts nothing (the agent has no
session/set_mode handler, the call fails unseen and the screen goes on saying "plan"); Auto never
asks, because the agent treats `auto` like `always-approve` while the screen promises a classifier;
the settings' Default is a value the agent drops, so after Always-Approve it says Default and the
agent still approves everything; of the six --permission-mode values only bypassPermissions did
anything. All of them err the same way: the user thinks the agent is more careful than it is.
Hidden, not deleted. app/agent_modes.rs holds three capability constants (PLAN, AUTO, DEFAULT, all
false: "the agent honours this"); AppView.{plan,auto,default}_mode_gate and the mirrored
PagerLocalSnapshot gates follow them. Test fixtures open the gates, so the hidden paths and their
old tests still run, and flipping one constant brings its mode back with its tests (1.P3 (b)).
- Shift+Tab cycles Normal <-> Always-Approve (dispatch_cycle_mode_without_plan, next stop from the
pure next_ring_step_without_plan; Auto sits between them only while its gate is on). Everything
else is as before: the pre-session branch stages the flags SessionCreated reads, the session
branch goes through set_yolo_mode_inner, both persist; the managed-policy pin keeps Normal and
shows the policy's warning; a stale Plan flag leaves through Normal with session/set_mode(default).
- Settings: no plan_mode row and no plan-nudge toggle, permission_mode offers Ask and Always
approve only (its description no longer mentions Default, Auto or a classifier), set_plan_mode
does nothing, set_permission_mode turns Auto/Default into Ask, the plan nudge tip is not shown,
and apply_contextual_hints (the one funnel for the hint toggles) clamps plan_mode.
- A saved permission_mode = "auto" or "default" is shown as Ask and the file is left alone; a launch
never starts in Auto (agent_modes::launch_auto).
- --permission-mode takes `default` (no flag) and `bypassPermissions` (--always-approve); plan,
auto, acceptEdits and dontAsk exit 2 with a message that says the flag would change nothing and
lists the valid values. pi-shell's PermissionMode::VALID_VALUES (agent-definition frontmatter) is
untouched.
The Python agent is untouched.
Against the real zypi (scripts/tui_pty/mode_probe.py, now 10 scenarios, the four refused values and
a settings-page check): Shift+Tab goes Normal -> Always-Approve -> Normal -> Always-Approve (asked
in Normal, not asked in Always-Approve); --permission-mode default / bypassPermissions behave as
before; a saved auto or default shows nothing in the status bar and the write is asked about; a
saved always-approve shows "always-approve" and is not asked; plan, auto, acceptEdits, dontAsk exit
2; the settings search for "plan" finds no Plan mode row and the permission picker offers Ask and
Always approve.
Tests: 24 new in pi-pager (21 lib: agent_modes 2, cli 3, dispatch modes 13, settings_modal 3; 3
settings_e2e) and 4 in scripts/tests/test_tui_pty.py. Three old tests that assumed Plan or Default
were visible were adjusted (rows_contain_categories_and_settings_through_pr_14,
expanded_description_wraps_to_modal_width, enum_choice_gated_off_covers_voice_and_permission).
Docs: plan section 10.13, appendix A.1 and B.6, CHANGELOG, packages/pi-agent-cli/AGENTS.md,
scripts/tui_pty/README.md; the baseline reference for pi-pager is 5,626 and docs/baselines/tui.md
records PR #10's CI run (8,659 passed, the numbers r9 had measured locally).
Verified: scripts/tui_baseline.py test on WSL2 (10 suites) 8,683 passed / 0 failed / 22 ignored,
pi-pager 5,626; pytest on Windows 2,382 passed / 36 skipped, on Linux (WSL2, ext4 clone) 2,383
passed / 35 skipped; ruff check and format --check clean with the venv's 0.16.10 and CI's pinned
0.16.0. Twelve one-line mutations of the new code, each caught by at least one test; one of them
(a Default in front of Ask in the picker) was not seen by the e2e picker test, which only pressed
Down, so it now presses Up too. Only mocks and scripted agents, no real LLM; zypi ran on Linux only.
Not pushed: this branch sits on codex/adr1-pr1-python, which sits on PR #10.
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.
zy1233
added a commit
that referenced
this pull request
Oct 10, 2026
…P3 (a), plan r10) (#12) ## Summary **Stacked on #11**, which is merged first; this branch is then rebased onto `main` and its base moved there. Three commits: plan 1.P3 (a) (the first option of decision 7 in section 10.11), a docs commit that names the PRs of the two r10 branches, and a docs commit that records the CI runs and how the stack was merged. Section 10.13 of `docs/PLAN/PLAN-RUST-AGENT-RUNTIME-REMOVAL.md` has the record. **This changes what users see.** The Python agent does two things about a tool call: it asks, or it approves everything. The TUI still offered Plan, Auto, a settings "Default" and six `--permission-mode` values. Measured against the real `zypi` on WSL2 (section 10.11): - Plan restricts nothing: the agent has no `session/set_mode` handler, the call fails unseen, and the screen goes on saying "plan". - Auto never asks, because the agent treats `auto` like `always-approve`, while the screen promises a classifier. - The settings' Default is a value the agent drops: after Always-Approve it says Default and the agent still approves everything. - Of the six `--permission-mode` values, only `bypassPermissions` did anything. All of them err the same way: the user thinks the agent is more careful than it is. This PR stops offering what the agent does not honour. **The Python agent is untouched.** **Hidden, not deleted.** `app/agent_modes.rs` holds three capability constants (`PLAN`, `AUTO`, `DEFAULT`, all `false`: "the agent honours this"); `AppView.{plan,auto,default}_mode_gate` and the mirrored `PagerLocalSnapshot` gates follow them. Test fixtures open the gates, so the hidden paths and their old tests still run, and flipping one constant brings its mode back together with its tests (decision 7 (b)). - **Shift+Tab** cycles Normal <-> Always-Approve (Auto sits between them only while its gate is on). The pre-session branch stages the flags `SessionCreated` reads, the session branch goes through `set_yolo_mode_inner`, and both persist. The managed-policy pin keeps Normal and shows the policy's warning. A stale Plan flag leaves through Normal with `session/set_mode(default)`. - **Settings:** no `plan_mode` row and no plan-nudge toggle; `permission_mode` offers Ask and Always approve only, and its description no longer mentions Default, Auto or a classifier; the plan nudge tip is not shown. `set_plan_mode` does nothing, and `set_permission_mode` turns Auto / Default into Ask. - **A saved** `permission_mode = "auto"` or `"default"` is shown as Ask and the file is left alone; a launch never starts in Auto. - **`--permission-mode`** takes `default` (no flag) and `bypassPermissions` (`--always-approve`). `plan`, `auto`, `acceptEdits` and `dontAsk` exit 2 with a message that says the flag would change nothing and lists the valid values. `pi-shell`'s `PermissionMode::VALID_VALUES` (agent-definition frontmatter) is untouched. ## Decision for you This is (a) of decision 7, the option you picked (`mode_a`). A real Plan mode ((b): the agent grows a `session/set_mode` handler and a read-only toolset) or a real Auto ((c)) would start from the three constants; neither is done here. ## Not verified, left - Only mocks and scripted agents, no real LLM; the real `zypi` ran on Linux (WSL2) only, so macOS and Windows runs of the TUI are not done. - Left, and listed in section 10.13: the settings search for "plan" still finds "Show contextual hints", and `permission_mode`'s search keywords still include "classifier" and "auto"; `--no-plan` is still in `--help`; the embedded upstream user guide (extracted to `<home>/docs/user-guide/`) still describes the old Plan / Auto cycle; the other permission settings (`remember_tool_approvals`, `default_selected_permission`) are not checked against the Python agent (candidate 14); the path that leaves a stale Plan flag is covered by a unit test only. ## Review guide | Files | What | |---|---| | `tui/crates/codegen/pi-pager/src/app/agent_modes.rs` (new) | the three constants, `permission_mode_flag_values()`, `launch_auto(...)`, the module doc with the three measured facts | | `app/app_view.rs`, `app/dispatch/modes.rs`, `dispatch/prompt.rs`, `dispatch/mod.rs`, `dispatch/settings/ui.rs`, `app/event_loop.rs`, `app/mod.rs` | the gates, the Shift+Tab ring (`next_ring_step_without_plan`, `dispatch_cycle_mode_without_plan`), the degrade paths, the plan nudge | | `settings/registry.rs`, `settings/defs.rs`, `views/settings_modal/state.rs`, `actions/defaults.rs` | settings rows and choices, the `CycleMode` action text | | `app/cli.rs` | `--permission-mode` value parser | | tests in `pi-pager` | 24 new (`agent_modes` 2, `cli` 3, `dispatch/tests/modes.rs` 13, `settings_modal` 3, `settings_e2e` 3) and 3 adjusted old ones (`rows_contain_categories_and_settings_through_pr_14`, `enum_choice_gated_off_covers_voice_and_permission`, `expanded_description_wraps_to_modal_width`) | | `scripts/tui_pty/mode_probe.py`, `scripts/tests/test_tui_pty.py` | the probe now has 10 scenarios, 4 refused values and a settings-page check; 4 new helper tests | | `scripts/tui_baseline.toml`, `docs/...`, `CHANGELOG.md`, `packages/pi-agent-cli/AGENTS.md` | `pi-pager` `reference_passed` 5,626; plan section 10.13, A.1, B.6; one CHANGELOG entry under Changed | ## Verification - **CI.** The run on the commit as first pushed (`bd44289`) is green ([TUI CI run 38074038191](https://github.com/zy1233/pi-python/actions/runs/38074038191)): the ten suites add up to **8,683 passed / 0 failed / 22 ignored** with `pi-pager` at 5,626 (13 ignored), each suite equal to its `reference_passed`, so the local numbers below held on the runner, including the `settings_e2e` test that was edited after the local manifest run (that binary alone: 254 passed). `cargo test` took 29 m 13 s, `Gates + cargo check` 5 m 46 s, Ruff 10 s, pytest on 3.11 / 3.12 / 3.13 about 2 minutes each. The branch was then rebased onto `main` after #11's squash and one docs commit was added; compared with the commit CI ran on, only three files differ (the plan, `docs/baselines/tui.md`, and a comment in `scripts/tui_baseline.toml`). - Whole manifest on WSL2 (`python scripts/tui_baseline.py test`, scrubbed environment): exit 0, 10 suites, **8,683 passed, 0 failed, 22 ignored** = #10's CI result (8,659) + 24 new `pi-pager` tests. `pi-pager`: 5,626. - pytest: 2,382 passed / 36 skipped on Windows, 2,383 passed / 35 skipped on Linux (WSL2); `scripts/tests/test_tui_pty.py` with `pyte`: 23 passed. - `ruff check .` and `ruff format --check .`: clean (227 files) with the project's Ruff 0.16.10 and with CI's pinned 0.16.0. - Twelve one-line mutations of the new code, each caught by at least one test. One was not seen at first: a Default listed in front of Ask in the picker, because the e2e test only pressed Down and the picker clamps at both ends; it presses Up as well now. - Against the real `zypi` (`scripts/tui_pty/mode_probe.py`): Shift+Tab goes Normal -> Always-Approve -> Normal -> Always-Approve (a write is asked about in Normal, not in Always-Approve); `--permission-mode default` and `bypassPermissions` behave as before; a saved `auto` or `default` shows nothing in the status bar and the write is asked about; a saved `always-approve` shows "always-approve" and is not asked; `plan`, `auto`, `acceptEdits` and `dontAsk` exit 2; the settings search for "plan" finds no Plan mode row, and the permission picker offers Ask and Always approve.
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
Stacked on #9 (base
codex/linux-wsl-r8). It adds sixteen commits on top ofd84d98a: the three phase 0 gates ofdocs/PLAN/PLAN-RUST-AGENT-RUNTIME-REMOVAL.mdthat 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-idcontract, what happens tozypi 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:
python_agent_e2e.rs: 10 tests drive the pager's ACP client against the real Python agent, andtui-ci.ymlinstalls the agent first and setsPI_E2E_REQUIRED=1so 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 intochannel closed, andconnect()hung when the agent exited at once.session/listwent from 2.0 s to 0.11 s andsession/loadfrom 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.session/listis paged with an opaque cursor, the pager's picker followsnextCursor, and a session is looked up by id without reading every file.mcp_servers; the agent says which ones a client passed and that it ignored them.session/cancelare covered over real stdio.channel closed.scripts/tui_pty/mode_probe.pyrecords 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-modevalues onlybypassPermissionsdoes anything.Decisions that were open
Plan section 10.11, last list. The three that unblock the most, and what became of them:
_meta.sessionIdhonoured bysession/new, the session's cwd in thesession/loadresponse). No TUI change, least risk. Done in feat(agent): honour _meta.sessionId on session/new, say where a session works (ADR1 PR 1, plan r10) #11.--permission-modevalues that do something. Today Plan gives users a false sense of safety. Done in feat(tui): offer only the permission modes the agent honours (plan 1.P3 (a), plan r10) #12.Not verified
--resume <id>(which directory the TUI uses for@search, diffs and git info), the settings "Default" path under a pty, and the-m/--modelflag.Review guide
28fed9454a38fe,1b9fb47e944ed5scripts/session_list_bench.pyc1f02c8,9f7bc35session/list; the pager followsnextCursor935e4b8,7becb49,9798e1daffda9e3fc182bpython_agent_e2e.rs,tui-ci.yml, manifestbabd75fresume_matrix.pybee3a36pi-pagerreference_passed5,602679cccb,d426253mode_probe.pyeef675bdocs/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.b0d001bmergesmaininto the branch with theoursstrategy: the tree ofmainis the tree of #9's last commit, the base of this branch, somainholds nothing the branch lacks, and the branch's tree is unchanged.Verification
eef675bis green (TUI CI run 38061734062). The new install step intui-ci.ymlran 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 withpi-pagerat 5,602: the numbers measured locally.cargo testtook 23 m 19 s (32 m 40 s on fix(tui): Linux follow-ups for the Rust agent runtime removal (plan r8, run on WSL2) #9),Gates + cargo check7 m 9 s, Ruff 9 s, pytest on 3.11 / 3.12 / 3.13 about 2 minutes each.tui-ci.ymlnow also runs whenpi_agent_cli/**changes. After fix(tui): Linux follow-ups for the Rust agent runtime removal (plan r8, run on WSL2) #9 was squash-merged,mainwas merged into this branch (b0d001b, keeping the branch's tree, which is the tree ofeef675b) and CI ran again: green too (run 38074449935;cargo test21 m 48 s, 8,659 passed again).python scripts/tui_baseline.py test, scrubbed environment,PI_E2E_PYTHONandPI_E2E_REQUIRED=1): exit 0, 10 suites, 8,659 passed, 0 failed, 22 ignored = fix(tui): Linux follow-ups for the Rust agent runtime removal (plan r8, run on WSL2) #9's CI result (8,634) + 25 newpi-pagertests (10 e2e, 8 agent-exit, 6 paging, 1 formcp_servers).pi-pager: 5,602.scripts/tests/test_tui_pty.pywithpyte: 19 passed.ruff check .andruff 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.