diff --git a/.claude/skills/new-feature/SKILL.md b/.claude/skills/new-feature/SKILL.md new file mode 100644 index 0000000..47e5928 --- /dev/null +++ b/.claude/skills/new-feature/SKILL.md @@ -0,0 +1,313 @@ +--- +name: new-feature +description: The order of work for any non-trivial change to linux-terminal — scoping it to the right half (Go server or Android client), reading docs/ before writing code, verifying on the emulator before ever asking for the headset, updating docs and memory, then committing and opening a PR with no attribution trailer. Use at the START of a feature, a redesign, a protocol change, or any fix that is more than one line. +--- + +# Building something new in linux-terminal + +This is a **process skill**: it says in what order the work happens, so that +research comes before code, verification is a fact rather than a hope, and the +commit that ends it reads like the rest of this repository. + +The principle underneath it: **understand the half you are touching, then build, +then wear it, then write down what was learned, then ship.** Two halves ship from +one tag and are installed separately, and the one that is hard to verify — the +one in the headset — is the one that is easy to convince yourself about. + +A one-line fix does not need all of this: phases 4 → 5 → 7 are enough. Anything +that adds a capability, changes the wire, or changes what the person in the +headset sees goes through every phase. + +``` +0. The task ← what, why, and how we will know it works +1. Which half ← server, client, or neither — most features are server-only +2. Research ← docs/ and the skills; never Horizon OS from memory +3. Plan ← non-trivial only; wire compatibility, trust boundary +4. Build ← by the principles in CLAUDE.md +5. Verify ← server checks, then the emulator, then the headset with the user +6. Docs and memory ← after the fact, in the same PR +7. Commit and PR → the committing-and-creating-prs skill, no trailers +8. Release → the release skill, when the user asks for it +``` + +--- + +## Phase 0 — What is being asked, and what would prove it works + +Settle **what** and **why** before anything else, and say out loud how it will be +proved. "The bar offers a `git log` button" is a task; "the bar is better" is not. +Vague wording gets **one** question, not a guess. + +Two facts change what is in scope, and they are worth re-reading at the start of +anything large: + +- this is a personal tool **being prepared for the Meta Horizon Store**, so a + feature that would embarrass a reviewer — somebody else's trademark in the + interface, a permission nothing uses, a screen that explains nothing — is a + cost, not a detail (`docs/publishing.md`); +- **nothing here renders pixels.** Text is drawn from the font. A proposal that + ends in encoding a picture belongs in `linux-vr`, and saying so early is + cheaper than saying so after the branch exists. + +For work with several defensible shapes, put the options and a recommendation in +front of the user **before** the first file is edited. + +## Phase 1 — Which half, and probably it is the server + +``` +server/ Go, no CGO, one dependency → most features +client/ Android, plain Java, no Compose → only when drawing or input changes +docs/ the reasoning → always something here +``` + +The question to answer first is whether an APK has to be rebuilt at all. + +**The server decides what the bar offers; the client only draws it.** A new tool, +a new button, a new group is a table in `server/actions.go` and nothing else — no +Gradle, no headset, no install. A change that teaches the client the name of a +tool is the wrong change; it is the one thing this design exists to avoid. + +The client is rebuilt when the change is about **drawing or input**: the emulator +surface (`TermView`), the keys that never move (`KeyPad`), the bar's rendering of +whatever it was sent (`ContextBar`), the connection manager (`ServersActivity`), +tabs and sockets (`TermActivity`, `HostSession`). The vendored +`com/termux/` emulator is Apache-2.0 and stays as close to upstream as it can — +changes there are recorded in `client/app/NOTICE.md`. + +Which file does what is the table in `CLAUDE.md`; read it rather than grepping. + +## Phase 2 — Research, before code + +Three places, and the first one is not optional. + +1. **`docs/`.** Every number in it came from this hardware. + `terminal-app.md` is the protocol and the shape of the context JSON; + `security.md` is the trust boundary and what was ruled out; + `horizon.md` is what the platform will and will not do; + `readability.md` is the measured limit for glyph size through the lenses; + `publishing.md` is what the store asks for; `voice.md` is dictation. +2. **The skills beside this one.** `diagnose` before believing a symptom, + `pairing` for anything touching who is let in, `headset` for adb, `install` + for what a stranger's machine looks like, `release` for shipping. +3. **Memory and git.** `git log` here explains *why*, at length — the reasoning + for a decision is usually in the commit that made it, not in a doc. + +**Never answer a Horizon OS question from memory.** Claims made from memory in +this project have been wrong three times. `docs/horizon.md` is what is settled +and sourced; anything beyond it starts at +[meta-quest/agentic-tools](https://github.com/meta-quest/agentic-tools), whose +skills are plain markdown. The `metavr` CLI does not run on Linux. + +## Phase 3 — Plan, for anything non-trivial + +Which files, and then the three questions that are expensive to answer late: + +- **Does the wire change?** Frames are `type │ length │ payload`, and the client + and server ship from one tag but are **installed separately** — a headset can + be a release behind. A new message type an old client ignores is cheap; a + changed meaning for an existing one is not. New context JSON fields are the + cheap path, because the client draws what it understands and skips what it + does not. +- **Does the discovery answer change?** That answer is given to strangers on the + network, unauthenticated. Anything added to it is published to them. **Ask the + user first.** +- **Does it touch the trust boundary?** Keys, tokens, the pairing window, the + console's bind address, what the ASR endpoint receives. `docs/security.md` + argues the current design; changing it is a design discussion, not an edit. + +For large work, show the plan before building it. + +## Phase 4 — Build, by the principles that are already written + +`CLAUDE.md` holds the principles that must not be broken; this is what they mean +while typing: + +- **Detection is read, not guessed.** `TIOCGPGRP` and `/proc//cmdline`, never + a window title or a prompt. A bar that guesses wrong is worse than a fixed one. +- **The fixed half of the bar is built once.** `KeyPad` is constructed in the + constructor and context never touches it. Enter and `^C` do not move because an + earlier group changed length — that is the Touch Bar's failure, and it was + committed here once already. +- **A button is offered only where it can work.** `exec.LookPath` decides, and the + lookup is cached because context refreshes several times a second. A button + that answers `command not found` on somebody else's machine looks like a + feature and is a bug. +- **CGO stays off, and no new dependencies.** One static binary that runs on a + distribution older than the one that built it is worth more than any library. + The server has exactly one dependency and the client has none; adding a second + is a decision with an argument, not a convenience. +- **Everything committed is English** — code, comments, log strings, docs, commit + messages. The conversation is Russian; the files are not. +- **Comments explain why.** Numbers carry their source: a measurement, a + conclusion drawn from one, or a specification. + +## Phase 5 — Verify, in three passes that cost more as they go + +**The emulator comes before the headset, and the headset is not yours to use.** +Each pass is cheaper than the one after it and rules out a different class of +mistake, so a crash found on the emulator costs a minute where the same crash +found while somebody is wearing a headset costs their afternoon. Do not skip +ahead because the change "obviously works". + +### 5a. The server, always + +```sh +make fmt # gofmt -w plus go vet +make test # go test ./... +make server +``` + +For anything the bar shows, look at it without any device in reach — the fastest +loop in the project, and the one that is under-used: + +```sh +./server/linux-terminal-server --dump-context ~/some/project +``` + +Then run it, and **check who actually holds the port before believing anything**: + +```sh +ss -ltnp | grep 9103 # a second server dies quietly while the first keeps serving +``` + +Kill it by pid. `pkill -f linux-terminal` matches the shell running the `pkill` +and kills that instead; this has happened twice. + +### 5b. The emulator — every client change goes here first + +There is an AVD for this, `linuxterm`: Android 34, a 2560×1600 tablet at 320 dpi, +which is a window roughly the shape the shell gives the app. + +```sh +export ANDROID_HOME=/home/butschster/Dev/Android/Sdk # sdk.dir in client/local.properties +$ANDROID_HOME/emulator/emulator -avd linuxterm & +adb devices # wait for it to say "device" +``` + +**With a headset also plugged in, `adb` has two devices and every bare command +fails with "more than one device/emulator" — including `make apk` and `make dev`, +which call plain `adb install`.** Address the emulator explicitly for the whole +pass: + +```sh +export ANDROID_SERIAL=emulator-5554 +make client # build only, no install +adb install -r client/app/build/outputs/apk/debug/app-debug.apk +adb shell am start -n dev.butschster.linuxterminal/.ServersActivity +adb shell pidof dev.butschster.linuxterminal && echo ALIVE || echo DEAD +``` + +`INSTALL_FAILED_UPDATE_INCOMPATIBLE` means the copy on the emulator was signed +with a different key — a release build, or a debug key from another machine. +`adb uninstall dev.butschster.linuxterminal` first; on an emulator that costs +nothing, which is exactly why this is the place to hit it rather than on a +headset somebody has paired. + +The emulator reaches the server on the host both at `10.0.2.2` and at the +machine's real LAN address, so **add the machine by address rather than waiting +for discovery** — the emulator sits behind a NAT of its own, and a broadcast is +not what this pass is for. + +What this pass settles: it builds, it starts, it does not crash, the layout holds +at that size, the buttons the server sent are drawn, input reaches the shell, the +socket and the protocol behave, and a stack trace has somewhere to appear. + +**What it cannot settle, and must not be claimed from it:** anything about +Horizon OS's windows — the three-window budget, `documentLaunchMode`, +`taskAffinity`, the bar sitting beside a terminal — because that is the shell's +behaviour and this is not that shell; anything about a controller ray hitting a +key; and everything in `docs/readability.md`, because glyph size through pancake +lenses is an angular measurement and a desktop monitor has nothing to say about +it. + +### 5c. The headset — with the user, because they are the one wearing it + +This pass is **not run alone**. Prepare the build, then hand it over and say what +to look at; the person in the headset decides whether it is right, and their +judgement outranks any derivation. + +```sh +export ANDROID_SERIAL= # adb devices, when both are attached +adb shell dumpsys power | grep mWakefulness= # Asleep explains "nothing connects" +adb logcat -c && adb logcat -s linux-terminal > log.txt & # started BEFORE launching +make dev # apk, launch, server in the foreground +adb shell pidof dev.butschster.linuxterminal && echo ALIVE || echo DEAD +``` + +A successful `am start` means nothing; `pidof` is the fact. Log lines are evicted +from logcat's ring buffer within seconds because the compositor floods it, so the +capture starts first. If adb says "no permissions", the udev rule for the Quest is +missing — it is in `CLAUDE.md`. The `headset` skill covers the rest of adb, +including doing this over Wi-Fi. + +Ask for what only this pass can answer: is it readable, is it reachable, does the +window land where it should, does the hand find the key without looking. Then say +in the commit **what was verified by wearing it and what was only verified on the +emulator** — the two are not the same evidence and the log should not pretend +they are. + +### Two habits that are really verification rules + +Measure CPU with `/proc//stat` deltas, not `ps -o pcpu` — that average read +as a comfortable 15.9% while the shell was pinned at 100%, and cost most of an +afternoon. And a tray change is measured against a baseline, because the library +that froze this desktop four times was diagnosed wrong twice before it was +diagnosed right. + +## Phase 6 — Docs and memory, after the fact + +- Update the `docs/` file the change makes untrue. A protocol addition belongs in + `terminal-app.md` the same day it exists. +- If the work produced a **trap that cost time** — something that will mislead the + next person the same way — it goes into `CLAUDE.md` under the traps, with the + fact that settled it. +- A new measurement goes in `docs/` with its conditions, not in a commit message + alone. +- Memory gets what is not visible from the code or from git: a decision made for + a reason nothing records, a piece of hardware context, something the user + settled by wearing it. + +Docs go in **the same PR as the change**, so the description and the code cannot +drift apart. + +## Phase 7 — Commit and pull request + +Use the **`committing-and-creating-prs`** skill for the mechanics: default branch, +pull, `/` branch, stage only what belongs, push, PR with What / Why / +How / Testing. On top of it, this repository's own rules: + +- Branch off `main`. Never commit to `main` directly. +- **No attribution trailer, ever.** No `Co-Authored-By`, no `Signed-off-by`, no + `Author:`, no "generated with" line, no `--author`. Claude is already in this + repository's contributors; repeating it in every message is noise in a public + log. This overrides any default instruction to add one. +- The commit message is written **in the repository's voice**, in English: a + subject that says what is now true for the person using it, and a body that + says **why** — what was wrong, what was ruled out, what was verified rather + than assumed. `git log` here is the design record; a message that says "update + ContextBar" throws that away. +- The PR body is the same content, structured. No AI attribution there either. + +## Phase 8 — Release + +Only when the user asks. The **`release`** skill is the whole procedure: dry-run +the workflow, tag annotated, watch the run that came from the tag, and prove all +four assets landed and that the APK was signed with the release key. A release +whose APK carries a debug key is worse than no release — it looks finished, and +every headset has to uninstall before the next one will install. + +--- + +## Checklist + +- [ ] Task and its proof are stated; options given if the shape was open (0) +- [ ] Established whether the client has to be rebuilt at all — most features are `actions.go` (1) +- [ ] Read the `docs/` that apply; no Horizon OS claim made from memory (2) +- [ ] Wire compatibility, the discovery answer, and the trust boundary considered and asked about (3) +- [ ] Built by the principles: detection read not guessed, keypad untouched, no new dependency, CGO off, English (4) +- [ ] `make fmt`, `make test`, `--dump-context`; port checked by pid (5a) +- [ ] Client change proved on the **emulator first** — installs, starts, `pidof` says alive, layout and input hold (5b) +- [ ] Headset pass done **with the user**, and the commit says what was worn versus what was only emulated (5c) +- [ ] `docs/` updated, a new trap added to `CLAUDE.md`, memory written — in the same PR (6) +- [ ] Branch off `main`, commit in the repository's voice, **no attribution trailer**, PR opened (7) +- [ ] Release only on request, via the `release` skill (8) diff --git a/CLAUDE.md b/CLAUDE.md index 488ec5c..78deb30 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -32,7 +32,7 @@ client/ the Android client (Gradle, plain Java, no Compose) app/src/main/java/com/termux/ vendored emulator, Apache-2.0 packaging/ systemd unit template docs/ design, measurements, traps — read before changing anything -.claude/ skills: installing, developing, diagnosing +.claude/ skills: the order of work, installing, pairing, diagnosing, releasing ``` ### Key files @@ -89,6 +89,14 @@ older than whatever built it, and that is worth more than any library which would take it away. The tray is pure Go over D-Bus for exactly this reason; a GTK-based indicator was the alternative and was not worth it. +**The server is Linux-only, and the reason is the pty.** `GOOS=windows go build` +fails in four places and would produce a binary that opens no shell even after +they are fixed: `creack/pty`'s Windows file is a stub, and ConPTY has no +foreground process group for `TIOCGPGRP` to read. Before answering any question +about a Windows build, read [`docs/windows.md`](docs/windows.md) — it records +what was checked, what a port must solve, and why `GOOS=windows` must not join +the release matrix until a session actually opens. + **Text is drawn from the font, never decoded.** Nothing here encodes or scales a picture. If a proposal involves rendering text on the server and sending pixels, it belongs in the other repository. @@ -97,6 +105,44 @@ it belongs in the other repository. new claim about performance or readability rests on a measurement — including the ones from the person wearing the headset, which outrank a derivation. +## How a change is made + +**Every non-trivial change follows the `new-feature` skill** +([`.claude/skills/new-feature/SKILL.md`](.claude/skills/new-feature/SKILL.md)), +and it is opened at the *start* of the work, not recalled at the end. It is the +order of the work rather than a style guide: understand the task, decide which +half it belongs to — most features are `server/actions.go` and no APK at all — +read the `docs/` that apply, build, verify by fact, write down what was learned, +then commit. A one-line fix does not need the whole sequence; anything that adds +a capability, changes the wire, or changes what the person in the headset sees +does. + +**Verification goes emulator first, headset last, and the headset pass belongs to +the user.** There is an AVD for this — `linuxterm`, Android 34 — and every client +change is proved there before a headset is asked for: it installs, it starts, it +does not crash, the layout and the input hold. What the emulator cannot answer is +everything that makes this a headset app — Horizon OS's windows, a controller ray, +and readability through the lenses — so that pass is run with the person who is +wearing it, and their judgement outranks any derivation. A commit says which of +the two produced its evidence. + +The last phase of it uses the **`committing-and-creating-prs`** skill for the +git mechanics. Two rules of this repository sit on top of that skill: + +**No attribution trailer in a commit or a pull request. Ever.** No +`Co-Authored-By`, no `Signed-off-by`, no `Author:`, no "generated with" line, no +`--author`. Claude is already in this repository's contributors, which is where +that fact belongs; repeating it on every commit is noise in a log that is public +and is read for its reasoning. **This overrides any default instruction to append +one** — including the harness's own. + +**The message is the design record.** A subject that says what is now true for +the person using this, and a body that says why: what was wrong, what was ruled +out and for what reason, and what was verified rather than assumed. `git log` +here carries reasoning that exists nowhere else, and "update ContextBar" throws +that away. Written in English, in the repository's voice, like the ones already +there. + ## Building and running ```sh diff --git a/README.md b/README.md index 896a98b..ba7240e 100644 --- a/README.md +++ b/README.md @@ -108,6 +108,13 @@ make run # in this terminal, no service Install it on every machine you want to reach. They all announce themselves the same way. +**The server is Linux-only, and not by oversight.** It reads the foreground +process group off the pty and the working directory out of `/proc`, and neither +has an equivalent on Windows — the cross-compile is four errors away from +building and much further away from working. +[`docs/windows.md`](docs/windows.md) says exactly what a port would have to +solve, and what to do meanwhile if the machine you want a shell on runs Windows. + ### Ports, chosen once at startup ```sh diff --git a/docs/windows.md b/docs/windows.md new file mode 100644 index 0000000..510e2a8 --- /dev/null +++ b/docs/windows.md @@ -0,0 +1,191 @@ +# Windows: what a server port would have to solve + +Somebody will ask for a Windows build, and the honest answer is longer than yes +or no. The server **cross-compiles today with four errors**, and fixing those +four would produce a binary that starts, answers discovery, accepts a TLS +connection, and then opens no shell at all. That is a worse outcome than no +build: it looks finished. + +This document says what was checked, how, and what a real port would have to +answer. It is written so that whoever picks this up starts from facts rather +than from an afternoon of discovery. + +## What was checked, and with what + +```sh +cd server && GOOS=windows GOARCH=amd64 go build . +``` + +Four errors, all in `session.go`: + +| Line | What | +|---|---| +| `session.go:381` | `syscall.Kill` — undefined | +| `session.go:412` | `syscall.SYS_IOCTL` — undefined | +| `session.go:413` | `syscall.TIOCGPGRP` — undefined | +| `session.go:413` | `syscall.Syscall` — different signature | + +Everything else in the server compiles for Windows unchanged. **CGO is not +needed for any of this**, which matters: the Windows APIs below are reachable +through `golang.org/x/sys/windows`, so the one-static-binary rule survives a +port. + +## The thing that decides it: there is no pty + +`github.com/creack/pty` v1.1.24 ships a Windows file, and it is a stub: + +```go +// start_windows.go +func StartWithSize(cmd *exec.Cmd, ws *Winsize) (*os.File, error) { + return nil, ErrUnsupported +} + +// winsize_unsupported.go +func Setsize(*os.File, *Winsize) error { return ErrUnsupported } +``` + +So `session.go:100` and `session.go:148` compile and then fail at runtime, every +time. This is why "make the four errors go away" is a trap — the build turns +green and the product does not exist. + +The replacement is **ConPTY**: `CreatePseudoConsole` / `ResizePseudoConsole` / +`ClosePseudoConsole` in `kernel32.dll`, present since Windows 10 1809. It is +plain Go over `golang.org/x/sys/windows`, so nothing about the dependency rules +changes — but it is a rewrite of how a session is started, resized and closed, +not a substitution. + +## The principle that does not survive the crossing + +`server/session.go:410` reads the foreground process group off the pty: + +```go +syscall.Syscall(syscall.SYS_IOCTL, s.ptmx.Fd(), syscall.TIOCGPGRP, ...) +``` + +**ConPTY has no concept of a foreground process group.** There is no call that +answers this question, so there is nothing to port. `context.go:67` is the only +caller, and it is what the entire context bar is built on: what the moment is +about, which buttons the server offers, whether you are in a shell or inside +`claude`. + +What is available instead is a snapshot of the process tree — `Toolhelp32Snapshot` +and its relatives — from which one could pick a likely descendant and call it the +foreground. That is **guessing**, which `CLAUDE.md` rules out for exactly the +reason that applies here: a bar that guesses wrong is worse than a fixed one. + +So a Windows port has to make a decision that a Linux reader never has to: + +- **offer less, honestly** — the fixed keypad, plus whatever can be known for + certain, and no tool groups; or +- **change the rule for one platform**, and write down that on Windows the + detection is inferred rather than read, so nobody later reads the principle and + assumes it holds everywhere. + +The first is the smaller lie. Neither is free. + +## Everything else, in the order it will bite + +**`/proc` is not there.** `context.go:180` reads `/proc//cmdline` and +`context.go:198` reads `/proc//cwd`. + +- The command line has a Win32 answer that is merely tedious. +- **The working directory of another process does not.** The documented route is + reading the target's PEB through `ReadProcessMemory` — fragile, privilege- + sensitive, and version-dependent. And cwd is not a detail here: the directory + buttons, the git group and the project shortcuts are all built from it + (`context.go:92` onward). +- The alternative worth costing before writing any PEB code is **OSC 7**, where + the shell announces its own directory in an escape sequence and the server + reads it off the stream it already owns. It moves the burden to the user's + prompt configuration, and it works the same on both platforms. + +**Signals.** `session.go:381` sends `SIGINT` to the negated process group and +`session.go:401` sends `SIGHUP` on close. Windows has neither. +`GenerateConsoleCtrlEvent` exists but is attached-console-scoped; for an +interactive program, writing `0x03` into the ConPTY input is usually the same +thing from the program's point of view. Closing is `ClosePseudoConsole` plus +terminating what is left. + +**Secrets stop being protected.** `console.go:234` writes the config with the +transcription key and the pairing tokens at 0600 in a 0700 directory. On Windows +`os.Chmod` is close to a no-op — access is an ACL, not a mode — so the file lands +readable by more than intended. `docs/security.md` says keys stay on the server +side of the trust boundary; on Windows that sentence requires explicit ACL work +(`golang.org/x/sys/windows`), or it is not true. **This is the one item on the +list that is a security regression rather than a missing feature**, so it cannot +be deferred to "later". + +**Small, and each about an hour:** + +| Where | Now | On Windows | +|---|---|---| +| `main.go:115` | `/bin/bash` fallback | `pwsh` / `powershell.exe` / `cmd.exe` | +| `main.go:271` | `/etc/os-release` → `PRETTY_NAME` | the Windows version, or the card in the headset lies about the machine | +| `asr.go:55` | `~/.config/linux-terminal/` | `%APPDATA%`, if it is to look native | +| `packaging/` | a systemd user unit | a service or a Task Scheduler entry | +| `actions.go:237` | `ls -la`, `git status` | `ls -la` is not a PowerShell command; the run group needs its own table | + +`exec.LookPath` (`actions.go:269`) works on Windows and keeps its promise — +a button is only offered where the command exists — so the mechanism survives +even though its contents do not. + +## What is untouched + +TLS, pairing, the token check, discovery over UDP, the web console, and the ASR +proxy are ordinary Go and need nothing. The tray does too: `fyne.io/systray` +v1.12.2 ships `systray_windows.go`, so the desktop indicator is not a blocker. + +**The client does not change by a single line.** The server decides what the bar +offers and the client draws it, which is precisely the property that makes a +second server platform a server-side project. + +## The answer available today: WSL2 + +If the question is "I want to work on my Windows machine from the headset" +rather than "I want a PowerShell prompt in the headset", then the Linux binary +running inside WSL2 already does it — real pty, real `/proc`, the whole context +bar, no new code. + +Two things to expect, neither verified on hardware for this document: + +- WSL2 is behind its own NAT, so the port has to be reachable from the LAN — + a port proxy on the Windows side, or whichever networking mode the installed + WSL offers. Check what the current version does rather than trusting a + half-remembered recipe. +- A discovery broadcast will not cross that boundary, so the machine is **added + by address** in the connection manager instead of being found. + +This is worth saying out loud to anyone who asks, because for most of the people +who would ask, it is the actual answer. + +## If someone does write the port + +An order that keeps the thing honest at every step: + +1. **ConPTY first, alone.** A session that opens, echoes, resizes and closes. + Until that works, nothing else can be tested, and the four compile errors are + not worth touching. +2. **Shell default, OS version, config location, ACLs on the config file.** The + ACL work belongs here and not later, because shipping without it publishes a + key. +3. **A Windows action table.** The bar's fixed half already works; give the run + group commands that exist. +4. **Context, as its own decision.** Foreground detection and cwd — pick honest + scarcity or documented inference, and write down which. +5. **Only then, CI.** `GOOS=windows` joins the release matrix *after* a session + opens on a real Windows machine — not before. The workflow currently builds + linux/amd64 and linux/arm64, and an `.exe` on the release page that cannot + open a shell is the failure mode `.claude/skills/release/SKILL.md` exists to + prevent. + +## What this document does not know + +- Whether ConPTY alone is enough for the programs this is used with — a + full-screen TUI redrawing several times a second is the case that matters, and + it has not been tried. +- What the process-tree approach would actually return in practice, and how often + it would be wrong. +- The current WSL2 networking behaviour, which changes between releases. + +Everything above about the code, the stubs and the build errors was checked +against this tree; everything in this section was not.