From 6aae7f494fb8281614de4306bcabca9dc87919b9 Mon Sep 17 00:00:00 2001 From: Quintin Botes Date: Tue, 4 Aug 2026 11:37:20 +0200 Subject: [PATCH] Write down what the next person needs, and what not to rediscover MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A handoff for whoever picks this up. Two jobs: say what is true right now, and stop the next person paying again for things that cost hours here. The traps are the point. Editing Rust — or switching branches — while `pnpm app` runs restarts the user's Threads. Merging with `--delete-branch` auto-closes any PR based on that branch and a closed PR cannot be reopened, which is how #34 was lost. This repository squash-merges only, so the PR body is the commit message. The established facts are there so nobody re-derives them: the gate must print nothing when it does not object, because `defer` ends the turn and reports success; `--effort` exists and an unknown value warns rather than errors; a plan only exists if the Thread *started* in plan mode; `TERM=dumb` disables ZLE; a pty read blocks uninterruptibly. Also the two patterns behind nearly every bug found: capabilities built in Rust and unreachable from the interface, and the app doing something without saying so. Six of the first in one day. The second is what the user meant by "unclear what to do where and when", and it is fixed instance by instance and never as a pattern. Next steps are ordered, with the first being an instruction not to write code: open a real zsh and check `bindkey ^T` by hand before touching the completion driver again. The last session lost several rounds patching that blind. --- docs/HANDOFF.md | 204 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 204 insertions(+) create mode 100644 docs/HANDOFF.md diff --git a/docs/HANDOFF.md b/docs/HANDOFF.md new file mode 100644 index 0000000..d0924f6 --- /dev/null +++ b/docs/HANDOFF.md @@ -0,0 +1,204 @@ +# Handoff + +Written for whoever picks this up next. It assumes you have read `README.md` and +`docs/COMPETITIVE-SPEC.md` and skips anything they already say. + +Two things this document is for: telling you what is true right now, and stopping +you rediscovering things that cost hours. Where something was verified against a +real binary or a real run, it says so. Treat anything not marked as verified as a +claim you should check. + +--- + +## Where the repository is + +`main` is green: **705 Rust tests, 342 vitest**, clippy and fmt clean. Everything +from the last session is merged (#33, #35, #36, #37, #38). + +| Branch | State | +| --- | --- | +| `main` | current, green | +| `completion-driver` | §3.2 in progress, pushed, **no PR, called from nowhere** | +| `linux-ci` (#31) | open, parked. Linux is deferred; macOS is the focus | + +`v0.1.0` is released. `curl` and Homebrew work. **`npx tervin` does not** — the +package is not on the registry. The README says so honestly; keep it that way +until it is true. + +--- + +## Traps in this repository + +Each of these cost real time. None are obvious from reading the code. + +**Do not edit Rust while `pnpm app` is running.** The Tauri watcher rebuilds and +restarts the app, killing the user's Threads mid-run. **Switching branches does +the same thing** — the watcher sees the changed files. Use a `git worktree` for +branch work while the app is up. + +**`gh pr merge --delete-branch` auto-closes any PR based on that branch**, and a +closed PR cannot be reopened or have its base changed. That is how #34 was lost +and became #37. Retarget dependent PRs *before* merging their base. + +**This repository allows squash merges only.** A branch of sixteen commits lands +as one, so put the narrative in the PR body — that becomes the commit message. + +**The ruleset requires up-to-date branches.** Every merge makes the next PR +`BEHIND`. `gh pr update-branch `, one at a time, then re-arm `--auto`. + +**Squash merges do not appear in `git branch --merged`.** Find merged branches +with `gh pr list --state merged --json headRefName`. + +**Never `git push --force --all`.** It destroyed three merged PRs earlier in this +project. `--force-with-lease` on one named branch. + +**zsh does not word-split unquoted variables**, and `grep --include=*.rs` needs +the pattern quoted or the glob expands first. + +**An agent running inside Tervin can commit to whatever repo it is pointed at.** +Check `git log` for commits you did not write. Point test agents at +`~/tervin-testbed` (`./scripts/testbed.sh`), never at Tervin's own source. + +--- + +## Facts already established + +Do not re-derive these. + +**The gate must print nothing when it does not object.** Claude Code accepts +`allow`, `deny`, `ask`. Given anything else — including Tervin's own `defer` — it +**ends the turn immediately and reports success**. Because the gate sees every +tool call, that killed every Thread at its first action. Silence plus exit 0 is +"no opinion"; a denial is a reason on stderr with exit 2. Verified by substituting +a hook that printed only the `defer` line and reproducing it exactly. + +**Model aliases resolve; do not pin identifiers.** `--model sonnet` resolved to +`claude-sonnet-5` on the work account, verified. A pinned `claude-opus-4-1` rots +in the worst way — the old name still resolves, so it fails by quietly running +last year's model rather than erroring. + +**`--effort` exists on the shipped 2.1.220 binary**, taking `low`, `medium`, +`high`, `xhigh`, `max`. An unrecognised value is a **warning**, not an error: the +CLI falls back to the default and runs anyway, so a wrong list produces a session +that reports one effort and spends another. Verified against the binary. + +**A plan only exists if the Thread started in plan mode.** `plan.proposed` comes +from `ExitPlanMode`, which an agent calls only when planning was its starting +mode. Switching mode afterwards produces nothing. + +**`TERM=dumb` disables zsh's ZLE**, and with no ZLE there is no completion system: +Tab inserts a literal tab and the reply is empty, which looks exactly like a shell +with no completions installed. + +**`Read` on a pty blocks uninterruptibly.** A deadline checked between reads is +not a deadline. Read on a thread behind a channel and use `recv_timeout`. + +**A shell's line editor calls `tcsetattr` with `TCSAFLUSH` when it starts**, which +*discards* queued input. Input written a moment too early is destroyed, not +delayed. `PtySession` gates writes until `ICANON` clears; do not remove that. + +**OSC 7 carries a hostname, not nothing.** zsh sets `$HOST`, so a bare "is it +empty" test classifies every local `cd` as remote. `Some(host)` must mean +genuinely elsewhere. + +**npm:** the token authenticates as `quintinbotes` and is then refused with 403 — +the account requires 2FA for writes. npm is **deprecating** tokens that bypass +2FA, so the token route is closing. Publish v0.1.0 by hand from a real terminal +(`npm login` needs a TTY), then configure trusted publishing for +`QuintinBotes/tervin/release.yml` and delete `NPM_TOKEN`. + +--- + +## Two patterns worth knowing + +Nearly every bug found in the last session was one of these. + +**Built in Rust, unreachable from the UI.** Six in one day: `LaunchConfig.model` +emitted `--model` and was never set; `permission_mode` was plumbed and never sent, +so the Plan surface could never fill; `task_progress` was parsed nowhere while a +`subagents` capability was advertised; `SessionMetadata` had no `cwd` though the +normalizer tracked it; `set_project_root` was correct and called from nowhere; the +dialog plugin was a dependency with permission granted and never invoked. All 63 +Tauri commands are now called — but check new fields, not just commands. + +**The app does something and does not say so.** A plan proposed with no +indication. A button that appears dead because the agent already moved on. A +message queued silently while the agent is busy. A divider that looked +unresizable because the target was 5px. Each was fixed individually; the pattern +was not. This is what the user meant by "unclear what to do where and when". + +--- + +## Next steps, in order + +### 1. Finish §3.2, CLI completion via the shell + +Branch `completion-driver`. Decoding and parsing are done and tested. Driving zsh +is not. + +**Do this first, before writing any code:** open a real zsh and run the setup by +hand, then inspect `bindkey ^T`. The `^T` widget is meant to run +`expand-or-complete` and then print its own marker, and no marker ever arrives. +The prime suspect is the function body not surviving definition on a single +written line. Establish it interactively; the last session lost several rounds to +patching blind. + +The shape is right and worth keeping: a *distinguishable second event*, not a +count of the first. After setup, the explicit `print -n` and the redrawn prompt +both emit the ready marker, so counting cannot tell "setup done" from "listing +done". + +`TERVIN_COMP_DEBUG=1` dumps the raw and decoded streams. Every failure in this +module is silent and looks identical to "this shell has no completions", so use it. + +### 2. Say when the agent is busy (task #15) + +The highest-value item from the user's UX complaints. Sending a turn while the +agent is mid-turn queues it with no acknowledgement — no busy state, no "queued", +nothing changes. The user reported this as "the agent is useless in responding". +Same root as the Plan surface's Approve button appearing to do nothing. + +### 3. Surface unmodelled runtime messages (task #18) + +`runtime.unclassified` is filtered out of the timeline entirely, so anything the +normalizer does not model is invisible rather than plain. That is exactly how +subagent `task_progress` hid — the data was never missing, it was in the discard +bucket. A count in the Bridge panel turns the next such discovery from a bug +report into a glance. + +### 4. Smaller, well-understood + +- **#10** Block output captures the next prompt's bytes past its `133;D`. Escapes + are stripped for display now, so it is invisible rather than absent. +- **#8** bash never emits `PromptEnd` (`133;B`) in the injected login shell: the + marker goes on `PS1` and anything setting `PS1` afterwards drops it. Blocks + still form; anything keyed on `PromptEnd` gets nothing from bash. +- **#17** `session-manager`'s `a_closed_port_reads_as_refused_rather_than_as_a_timeout` + failed once under full-workspace load and passes in isolation. An + intermittently red suite trains people to re-run rather than read. + +### 5. Parked, and the user's call to unpark + +- **#16** the UX pass. The user's verdict was that the interface "does not behave + well at all". They chose to finish the roadmap first. Do not start this without + asking. +- **#31** Linux CI. It found three real defects before being parked, two of them + unsound tests that had never tested what they claimed. + +### 6. Last: npm (task #5) + +Blocked on the user's account, not on code. See the facts above. + +--- + +## How to test + +```sh +./scripts/testbed.sh # ~/tervin-testbed: a real bug, a dirty tree, an AGENTS.md +pnpm app +``` + +`docs/MANUAL-TEST.md` is ordered so an early failure tells you not to trust what +follows. Start a **fresh Thread after any rebuild** — one launched by the previous +binary holds a socket that no longer answers and will keep failing regardless of +what was fixed.