# telltale — PARITY Cross-platform and cross-machine status. Read this when you change machines and something behaves differently than it did on the other one. Two different things get called "parity" and only one of them is achievable: - **Capability parity** — the same build, the same seats, the same postures. Achievable, and the checklist below is how you get it. - **Session continuity** — picking up the same conversation on another machine. **Not achievable, by design.** See *What does not travel*. ## Capability parity checklist 1. **Build here.** `go build -o telltale.exe ./cmd/telltale` (drop `.exe` off Windows). The binary is per-platform; it does not travel. 2. **Open the room and read the fold line.** `telltale council` names any seat that folded out of the grid and why. A seat can be installed and still not drivable — that is the case worth catching. 3. **Copy the brief.** `--brief ` / `TELLTALE_COUNCIL_BRIEF` points at a file that lives outside the repo on purpose, so git does not carry it. Without it, every seat on the new machine is guessing at conventions the old one had. ## Per-vendor platform differences These are measured, not read off `--help`. Where a claim rests on something weaker than a live run, it says so. | Seat | Windows | Notes | |---|---|---| | **Claude Code** | same as elsewhere | No known platform difference. | | **Codex** | **read `ro:enforced`, write `unsandboxed`** | Re-measured 2026-08-29 against codex-cli 0.149.1 on Windows 11: `-s read-only` now ENFORCES — a shell write came back `Access is denied.` at exit 1 with no file on disk, a read ran clean, and the resume override (`-c sandbox_mode="read-only"`) enforced the same. The read posture passes `-s read-only` on every OS again. Two Windows-only residues, both measured: the sandbox cannot spawn this machine's PowerShell (`CreateProcessAsUserW ... (Windows error 5)`, the same line 0.146.0 failed everything with) and turns complete because the model retries through cmd.exe, so a read turn can still fail to inspect when it does not retry; and `-s workspace-write` denies `.git` there and REFUSES the `writable_roots` override that unlocks it on macOS — so the write posture keeps `danger-full-access` on Windows, or the seat edits all session and never commits. The old row (both postures `danger-full-access`, verified at 0.146.0: every sandboxed spawn failed, reads included) is history as of 0.149.1; design.md §9.2's 2026-08-29 amendment carries the full capture. **Re-run 2026-09-01 at codex-cli 0.151.0, Windows 11, argv only:** the three seat shapes (read, write, resume) all passed argument parsing and produced `thread.started`, so no flag moved. The sandbox claims in this row were NOT re-measured at 0.151.0, because every turn that day died at the account's usage limit before a tool ran. What that run DID find is a stdout `turn.failed` frame with an empty stderr (design.md §9.58), which is the reason both rooms showed `failed (exit 1)` with no sentence. **Read liveness re-run 2026-09-04 at 0.151.0:** three of three read turns inspected (`dir /a` and `type README.md` via cmd.exe, each `done (exit 0)`) in a hosted read room with only codex seated — the "two of three" caveat is retired for this path. The write-side sandbox claim was not exercised by that run and still rests on 2026-08-29. **Interrupt and teardown measured 2026-09-05 at 0.151.0** (design.md §9.57): `turn/interrupt` yields `status: "interrupted"` in ~0.1 s, five of five; exit after stdin close ran 1.76–7.77 s, four of five over the seat's 4 s grace, through the `codex.cmd` shim council itself spawns. **Diagnosed the same day, forty runs:** shim, `node.exe codex.js` and native `codex.exe` all 6-8 s (five each); no thread, 0.03 s; `--disable hooks`, 0.06-0.08 s with MCP kept; `mcp_servers={}` with hooks kept, 4.5 s; `thread/unsubscribe` and `thread/archive` before the close, no change. The cost is this machine's two `SessionEnd` hooks in `~/.codex/hooks.json` (one took 4.5-6.6 s by hand), and the server is silent on stdout while they run. `Grace()` is 20 s from this date, a bound on the operator's hooks; five shim runs with an interrupted turn then exited in 4.46, 4.33, 14.73, 7.55 and 2.45 s. macOS: unmeasured on this path, and a Mac with no `SessionEnd` hook will exit faster. | | **Cursor** | **no sandbox request at all**, and **no workspace-trust screen** | Both are properties of the ACP server this seat now runs on, not of the platform: the protocol has no sandbox parameter and no trust step. The old row said `--sandbox enabled` kills the turn on Windows — true of print mode, verified 2026-08-04 against 2026.07.23-e383d2b, and no longer a flag council passes on any OS. Trust is the sharper half: verified 2026-08-08 against 2026.08.04-aaa8809, a directory print mode refused with "⚠ Workspace Trust Required" was written to over ACP with no prompt. | | **Antigravity** | same as elsewhere | `unsandboxed` on every platform — it was asked to write a file under both of its own read-only flags and wrote it. Refuted, not unverified. **Stream session measured on Windows 11 at 1.1.26, 2026-09-05** (design.md §9.57): two turns down one stdin kept one pid and one `conversation_id` with recall; `--conversation ` under stream input resumed with the same id; `--print-timeout` bounds each turn, not the process (20 s stand-in, 35 s idle); exit 0.08 s after stdin close; no stderr. Without `--input-format` the seat reads stdin as a prompt and answers, so the retreat's trigger does not fire on this build. macOS: unmeasured on this path. | | **Grok** | **measured on both, and the platforms differ** | The seat is `unsandboxed` on both platforms. `--permission-mode plan` is REFUTED on both. grok 1.0.0 (3cd0d0cbce) wrote the file under that flag, and the control run without the flag also wrote it. Windows measured this on 2026-08-09 and macOS on 2026-08-14. The macOS run confirmed the file on disk and did not use the reply text. `--sandbox` DIVERGES. On Windows the flag is UNOBSERVABLE: given `bogus-profile-xyz`, grok printed no error and no warning, and it answered normally at exit 0. On macOS the same build validates the profile and fails closed. It prints `sandbox could not be applied`, then it prints `Refusing to start with its protections missing`, and it exits 1 with no turn. The macOS section below states what this result does and does not permit. **Re-measured on Windows at grok 1.0.4 (d846eb93d9) on 2026-08-14, and both results held.** The write landed again under `--permission-mode plan`, and `bogus-profile-xyz` again drew no error and exit 0. **The Mac was updated to 1.0.4 (d846eb93d94d) on 2026-08-17 and both probes were re-run there, with both results holding.** **ACP handshake and a turn re-driven on Windows at 1.0.13 (5e9a58528b76), 2026-09-04** (design.md §9.57): `loadSession: true`, `sessionCapabilities {list, resume, close}`, no `modes` and no `configOptions` from `session/new`, a `/`-prefixed brief NOT eaten, and `_meta.usage` with token counts plus `costUsdTicks` (unit unmeasured, cost stays absent). The `--permission-mode plan` and `--sandbox` probes above were NOT re-run at 1.0.13. So this is a SAME-BUILD comparison again, on both halves, and the `--sandbox` divergence is a property of the operating system rather than of the release. See the note below. **The ACP seat's mode, permissions and resume were driven on Windows at 1.0.13 (5e9a58528b76), 2026-09-04** (§9.57's grok items; `vendors/acp.go`'s header holds the frames). `session/set_mode` answers `{}` to ANY id, a nonsense one included, so acceptance proves nothing; `plan` HELD two of two write briefs (no shell ran, nothing in the workspace, the agent's `_x.ai/exit_plan_mode` request answered with the client's empty result and read as "revise") and let a read brief read, so the ACP seat's read badge is `ro:requested` on this platform's measurement. A write session does NOT ask: two briefs wrote with no `session/request_permission` (`"yolo":true` on the wire); after a `/always-approve off` prompt the `write` tool asked with `allow-once`/`reject-once`/`allow-edits-session`, a rejection kept the file off disk, and `mkdir` still never asked. `session/load` resumed a saved id in a new process from the same cwd and refused a foreign cwd or an unknown id with `-32603 FS_NOT_FOUND`, the process surviving. Closing stdin ended the process in 2.2–4.4 s, twelve of twelve. **None of this ran on macOS**, and the ACP seat there is still unmeasured on every claim; the badge says where it was measured. | **`codex app-server` is unverified off Windows, 2026-08-29 — and since 2026-09-02 it is the SEATED path (§9.57).** The row above describes `codex exec --json`, which the room dispatched until then and keeps as the fallback. The app-server protocol's eight arms all ran on Windows 11 against codex-cli 0.149.1, which is why the off-Windows read badge dropped to `ro:requested` when the seat moved: the macOS `ro:enforced` was `codex exec`'s measurement, and a seat move re-measures rather than inherits. The same holds for the other two seats that moved that day, `grok agent stdio` and `agy --input-format stream-json`: undriven on every platform, badged `unmeasured` everywhere. Two of its findings are Windows-shaped and are the likeliest to differ on a Mac: the tool router wrapping shell commands in `pwsh.exe`, which cannot start under the Windows sandbox and left a read-posture seat unable to inspect; and the `.git` deny under `workspace-write`, which the `exec` path's own row records behaving differently on macOS. **On the Mac, run the probe before believing either.** The protocol also exposes `windowsSandbox/readiness` and `windowsSandbox/setupStart` as client requests — `readiness` answered `{"status":"ready"}` here and costs no model turn — and whether those exist at all off Windows is unread. **Cursor's ACP seat is unverified off Windows, 2026-08-08.** Every one of the thirteen arms behind that seat ran on Windows 11 against cursor-agent 2026.08.04-aaa8809. Nothing in the invocation is platform-specific — it is the single subcommand `acp`, and detection already resolves the native entry point per OS — so a macOS run is *expected* to work and has not been shown to. What is worth checking there specifically, because each one is a claim the badge makes: that `session/load` reloads a thread into a new process; that `session/set_mode` `plan` is accepted and refuses a write; that `session/request_permission` fires for a shell command and not for an edit; and that workspace trust is absent on that path too, since the Mac is where print mode's trust prompt was least likely to be hit. Record what you find here rather than in `docs/design.md §9.36`, which is the Windows capture and should stay one. **The Grok seat is now measured on macOS, 2026-08-14.** The machine is an Intel x86_64 MBP on macOS 26.5.2. It ran grok 1.0.0 (3cd0d0cbcebe) **on that date**, which was the build the Windows capture used. It runs 1.0.4 now; see the SETTLED note below, which keeps these results and adds the same probes at 1.0.4. This file listed four questions as unverified off Windows. All four now have an answer, and three of the four match Windows: | question | macOS answer | |---|---| | Does grok accept `--sandbox ` silently here? | **No. This answer diverges from Windows.** grok prints a warning, refuses to start, and exits 1 with no turn | | Does `--permission-mode plan` still write the file? | **Yes. REFUTED, as on Windows.** The run created `wrote.txt`, and a later check confirmed the file on disk | | Does the binary land at `~/.grok/bin/grok`, the POSIX guess in `grokKnownPaths`? | **Yes.** That path is a symlink to `~/.grok/downloads/grok-macos-x86_64`. The installer also writes `~/.local/bin/grok`, which points at the same file and resolves first on PATH | | Is `grok` a native executable and not a shell shim? | **Yes.** `telltale doctor` reports `drivable ok … a native executable`, so the argv transport for the brief holds here | `go test ./internal/council/vendors -tags=live -run TestLiveGrok` PASSED on this machine in 17.82s, over two turns that reused one session id. That command exercises the whole invocation, and not one flag at a time. **What the `--sandbox` result permits, and what it does not.** It does not change the flags that council passes today. Council passes no sandbox flag because `--sandbox` is not dependable across the platforms of the fleet. A flag that fails closed on one OS and does nothing on another is still not a posture that the badge can state. The seat stays `unsandboxed` on both platforms, because `--permission-mode plan` is the flag that restricts writes, and both platforms refute it. The result does change the stated REASON. "grok cannot tell a real profile from a typo" is a fact about Windows, not a fact about grok. Any text that uses that sentence as the whole justification is now half true. A per-platform code path for the macOS validation is a design decision. Nobody has made that decision, and this file does not make it. **SETTLED 2026-08-17: the `--sandbox` divergence is the PLATFORM, not the version.** The machines went out of step on 2026-08-14, when Windows was found on grok **1.0.4 (d846eb93d9)** and the Mac was still on 1.0.0. That made the divergence a different-build comparison, and it could not rule out a version difference as the cause. This file named the experiment that would settle it, and the Mac has now run it. The Mac was updated 1.0.0 → **1.0.4 (d846eb93d94d)** with grok's own `grok update`, so **both machines now run the same build**, and both probes were re-run there: | probe, at 1.0.4 on macOS | result | |---|---| | `grok --sandbox bogus-profile-xyz -p …` | **Still fails closed.** Same warning, same `Refusing to start with its protections missing`, exit 1, no turn | | `grok --permission-mode plan -p …` | **Still refuted.** `wrote.txt` confirmed on disk afterwards, not read out of the reply | So the `--sandbox` behaviour tracks the operating system and not the release. Windows accepts a nonsense profile at both 1.0.0 and 1.0.4; macOS validates and refuses at both. The row above states this as a platform difference rather than a version one, and it is now a same-build comparison on both halves. `go test ./internal/council/vendors -tags=live -run TestLiveGrok` PASSED again at 1.0.4 (19.65s), so the update did not break the invocation the seat depends on. **The `grokKnownPaths` guess survived the update, which is the part worth keeping.** After updating, `~/.grok/bin/grok` points at `../downloads/grok-1.0.4-macos-x86_64` — the download filename now carries the version, but the `bin/grok` path that detection looks for is stable across an update. An updater that moved that path would fold the seat out silently. Still unrun on the Mac at any build: the wire capture, and `--resume` against 1.0.4's optional-value spelling. Record further findings here. Do not record them in `docs/design.md` §9.39, which is the Windows capture and must stay one. **Windows launch-parent trap when driving cursor-agent by hand.** Launched from a Git Bash parent, this machine's `PreToolUse` credential-guard wrapper fails closed and every cursor-agent tool call comes back "Hook blocked with message: … syntax error near unexpected token `&`". It looks exactly like a broken vendor and is not; it cost an arm of the §9.36 capture. Drive cursor-agent from a PowerShell or cmd parent on Windows. Upstream wrapper bug, agent-ops ADR-012. **The symlink refusal now runs on Windows — Developer Mode is the prerequisite.** `TestSeedSymlinksAreNamedNotFollowed` (`internal/council/arena_seed_test.go`) pins `.worktreeinclude` seeding's refusal to follow a symlink. Creating a symlink needs `SeCreateSymbolicLinkPrivilege`, which an ordinary Windows shell does not hold, so this box skipped it with *"A required privilege is not held by the client"* until **Developer Mode was enabled on 2026-08-09; it then PASSED, measured on this workstation.** The sting worth keeping is why it mattered: design.md §9.37's stated reason for refusing symlinks is *"Windows is primary and symlink semantics differ per platform"* — so until that day, the platform the claim is about was the only platform never checking it. Two mechanics for whoever hits this on another box. **No new shell or logon is needed**: Go's `os.Symlink` retries with `SYMBOLIC_LINK_FLAG_ALLOW_UNPRIVILEGED_CREATE`, which reads Developer Mode at runtime rather than reading a token privilege granted at logon — an already-running terminal picks it up immediately. And **an elevated shell also passes** (Administrators hold the privilege) but does not close this gap, because the point is that the test runs in ordinary use. **Measured on CI, 2026-09-20: the `windows-latest` job runs the test.** It was not visible before, because that job runs `go test ./...` without `-v`. That command prints one `ok` line for each package, and a package that skips one test still prints `ok`. The `ci` workflow now runs this one test by name after `Test (fixture eval)`, with `-v` and with `-count=1`. The step reports the verdict. A skip keeps the step green, because `go test` exits 0 on a skip. A runner image that no longer grants the privilege is then a fact in this log and not a red build. A test failure turns the step red, and the `Test (fixture eval)` step above fails on the same test first. From GitHub Actions run 35539004963, job 106153105114: ``` === RUN TestSeedSymlinksAreNamedNotFollowed --- PASS: TestSeedSymlinksAreNamedNotFollowed (0.57s) ``` So the runner image grants what `os.Symlink` needs. The primary platform now checks the refusal on every push to `main` and on every pull request. The `ubuntu-latest` race job runs the test too. That job prints no test name either, but `os.Symlink` needs no privilege on Linux, so the skip branch cannot start there. `-count=1` is on the command for the reason the paragraph below gives. **One test still skips on Windows, and this one is legitimate.** `TestSavedRoomIsNotWorldReadable` (`internal/council/resume_test.go:446`) skips with *"posix file modes are not the access control on windows"* — measured 2026-08-09 in a full `go test ./... -count=1 -v` pass, where it is the **only** skip in the entire suite. The mechanism is correctly skipped; POSIX modes really are not how Windows controls access. The residue is that the *property* — a saved `room.json` is not world-readable — is therefore only ever asserted on platforms that are not the primary target. Recorded as a measured gap, not as a defect: writing the ACL-based equivalent is a judgement about whether that property needs a Windows assertion at all, and nobody has made it. **Measuring skips at all needs `-count=1`.** `go test` caches passing packages and replays their output, so a `-v` run over cached results can report a different skip count than the same command uncached — observed here the same day, one run reporting a skip and the next reporting none with nothing changed in between. A skip census over a cached suite measures the cache. **Cursor install location, Windows.** `cursor-agent` lives at `%LOCALAPPDATA%\cursor-agent\cursor-agent.cmd` and is frequently absent from the PATH of the shell running the detection. A seat that folds out as "not installed" here is usually this. The `cursor` binary on PATH is only the editor launcher and council never drives it. **The `cursor-agent` CLI manifest tree is measured on Windows only, 2026-08-29.** `internal/adapter/cursor` now reads a second store, `~/.cursor/chats///meta.json` (design.md §3.9's 2026-08-29 addendum), and resolves it from `os.UserHomeDir()` on every platform. Everything behind that path was measured on Windows 11, at `cursor-agent 2026.08.11-e8db854`: 43 manifests, `schemaVersion` 1 on all of them, `cwd` a native `C:\...` path on all of them. Nothing was read on the Mac, where PARITY already records `cursor-agent` living at `~/.local/bin/cursor-agent` rather than under `%LOCALAPPDATA%` — a different install location, which is exactly the kind of difference that could move the state directory too. **The measurement to take on the Mac:** confirm `~/.cursor/chats` exists and holds the same `//meta.json` layout, and confirm `cwd` is a POSIX path there. A tree that is absent costs nothing — the reader reports the store absent and the Composer rows still render — so this is a coverage gap, not a suspected defect. ## Seat model flags, read 2026-09-30 `telltale council --model` puts a request on each seat's argv. The flags below were read off each CLI's own `--help` on the Windows 11 PC on 2026-09-30. A help line says what the CLI PARSES. No live turn was driven with any of these flags, so no row says the vendor honours the request. That question is the column's `ran` reading, not this table. | Seat | Build | Flag council passes | Where on argv | |---|---|---|---| | Claude Code | 2.1.273 | `--model ` | first | | Codex | codex-cli 0.151.0 | `-c model=""` | before the stdin `-` on `exec` and `exec resume`; after `app-server`, which lists no `-m` | | Antigravity | 1.2.14 (newest `agy changelog` entry; no `--version`) | `--model ` | first, before `-p` | | Grok | 1.0.13 (5e9a58528b76) | `--model ` | between `agent` and `stdio` on the live seat (`grok agent stdio --help` lists no model option); first on the batch fallback | | Cursor | not installed | none | refused: no help line to read | The `ran` reading joins the seat's session id to the id in the vendor's store (`internal/council/seatmodel.go`). Only the fixture trees of the adapters exercise that join. **Unmeasured on a live seat, every vendor:** whether Claude's stream `session_id`, the Codex app-server `thread.id`, agy's `conversation_id`, grok's ACP `sessionId` and Cursor's ACP `sessionId` each equal the id the store files the session under. A join that does not hold reads `ran unknown`, never a neighbour's model. The Claude and Codex joins have indirect support: `claude --resume` and `codex exec resume` both find the stored session by that same id. To measure one: open a room with `--model`, send one brief, and compare the `ran` cell with the store record by hand. **macOS:** nothing here was read there. ## Killing the seats when the room dies abnormally **Measured 2026-08-17**, on the Mac (Intel x86_64, macOS 26.5.2), against bubbletea v2.0.8. This is the sharpest platform difference council has, because the platform that behaves worse is the one that fails SILENTLY: five agents keep running, holding sessions and spending quota, with no room attached and nothing on screen to say so. The two platforms bound a seat's lifetime by different mechanisms, and only one of them is a lifetime: | platform | mechanism | does the seat die when telltale dies? | |---|---|---| | Windows | Job Object, `JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE` | **yes, always** — the handle closes with the process and Windows reaps the tree, on every way out including the ones no handler can catch | | macOS, Linux | process group, `Setpgid` | **no** — a group is a name for a set of processes, not a lifetime. It dies when something signals it and at no other moment | | macOS, Linux — the council **host** (`--host`, design.md §7.30) | the host's **session** (`Setsid`) over the per-seat groups, plus the host's own SIGTERM/SIGINT handler, plus `telltale council kill`'s session sweep | **not by the host's death; yes by the command.** SIGTERM or SIGINT on the host reaps every seat through its handler. `telltale council kill` SIGTERMs the host and every process carrying its session id, then sweeps with SIGKILL after a bounded grace, and on a dead host's stale `host.json` it sweeps the dead session and prints how many it ended. **`kill -9` the host alone and the seats keep running** until that next `kill`. **The difference from the Job Object, precisely:** a session is membership inherited at fork and lost only by a child calling `setsid(2)` itself — such a child escapes and nothing here sees it go, where a Job Object child cannot leave. **Measured on Linux only, 2026-09-02** (the CI race job runs the tests; the built binary was driven through host/detach/ls/rejoin/kill by hand). **macOS: suites measured, hand cycle owed (2026-09-02).** The `darwin` CI job ran this package's unix test suites in-process on an Apple Silicon runner (run 33657422294: host, detach, rejoin, refusal, kill, stale file all passed, so `LOCAL_PEERPID` is exercised on every dial), after its first run found the socket path past macOS's 104-byte bound and `PipeName` learned to retreat. The built binary has not been driven through host/detach/ls/rejoin/kill by hand on a Mac, and `p_comm`'s sixteen-byte name under a real `kill` sweep is the reading still to watch. | So on unix the kill has to be MADE on the way out, and until this was measured nothing made it on any signal. `runner/proc_unix.go` claimed the "same guarantee the Windows job object gives"; it gives half of it, and the corrected comment there now says which half. ### Method, so the numbers can be re-taken A throwaway Go program: a Bubble Tea v2.0.8 model, plus one `sleep 600` child started with `SysProcAttr{Setpgid: true}` — the shape `runner/proc_unix.go` gives every seat. Teardown was reachable only from a key handler, which is how the shipped room was written. Each run signalled the program, waited two seconds, and asked whether the child's pid was still alive. | signal | what Bubble Tea did to the program | did the model's `Update` run? | the child | |---|---|---|---| | `SIGINT` | `p.Run()` returned `program was killed: program was interrupted` | **no** | **orphaned** | | `SIGTERM` | `p.Run()` returned `nil` | **no** | **orphaned** | | `SIGHUP` | nothing — the default disposition killed the process outright | **no** | **orphaned** | | `SIGKILL` | nothing, and nothing can | **no** | **orphaned** | Bubble Tea does end the program on two of those four, and ending the program is the whole of what it does. Its handler answers above the model's head — `tea.go`'s event loop returns on `QuitMsg` and `InterruptMsg` *before* it calls `model.Update` — so a model can never be handed the message, and council's teardown never ran. `internal/council/signals_unix.go` now runs teardown on the three catchable signals before the room goes out. The same probe with that handler installed reaped the child on all three. ### What is still true after the fix - **`kill -9` still orphans every seat on macOS and Linux.** SIGKILL is uncatchable, so no handler can cover it, and this is a real difference from Windows rather than a bug worth filing. If a room is ever `kill -9`'d, check for surviving vendor processes by hand. - **A Windows console close is a different mechanism** — `CTRL_CLOSE_EVENT` through `SetConsoleCtrlHandler`, not a POSIX signal — and it is unmeasured. It does not need a handler for the seats' sake: the job object covers them through it too. - **The unix behaviour is measured on macOS only.** Linux shares the `Setpgid` code path and the same Bubble Tea build, so it is expected to match and has not been shown to. Record a Linux run here. *The council host's row above is the mirror image: measured on Linux only, expected on macOS.* ## HUD adapters Adapter path resolution is portable by construction — `os.UserHomeDir()` and `os.UserConfigDir()`, which return the right root on all three platforms. The gap is **verification, not portability**: every adapter's live-corpus pass was run on Windows. No macOS or Linux corpus pass is recorded for any of the six — grok joined the list on 2026-08-09 (design.md §3.9a), surveyed against 30 sessions on the Windows box and nowhere else. Pi joined on 2026-08-16 (design.md §3.9b live pass): four probe sessions on this Windows box, pi 0.84.1. No macOS or Linux corpus pass. Its root honours `GROK_HOME`, which the vendor's own startup log names, so the override is portable too and is likewise unexercised off Windows. That means a macOS run is expected to work and has not been shown to. Treat a missing or empty adapter on macOS as unverified rather than broken, and record what you find here. ## `telltale doctor` The Windows box ran it first (2026-08-09) — five installs, five `--version` answers. **The Mac has now run it too** (2026-08-10, Intel x86_64, macOS 26.5.2, a binary built from `main` at `9b67e04`), so path resolution and the version probes are measured on both platforms rather than portable-by-construction on one: | seat | binary | version | |---|---|---| | `claude` | `~/.local/bin/claude` | `2.1.222 (Claude Code)` | | `codex` | `/usr/local/bin/codex` | `codex-cli 0.146.0` | | `agy` | `~/.local/bin/agy` | `1.1.10` (`1.1.11` on the 2026-08-14 re-run) | | `cursor` | `~/.local/bin/cursor-agent` | `2026.08.04-aaa8809` | | `grok` | `~/.local/bin/grok` | `grok 1.0.0 (3cd0d0cbcebe)` at that run; **1.0.4 (d846eb93d94d)** since 2026-08-17 | All four installed seats report `drivable ok` as **native executables**, which is the row that matters beyond the version string: it is the measurement behind "the prompt goes in argv and no shell sees it", so the brief's argv transport holds on macOS and does not depend on a shell quoting it. Cursor's POSIX entry point taking the bare `--version` was true by construction here; it is now true by measurement. **A cold run is several seconds slower than a warm one, and the report says so per seat.** First run after boot: `claude` 3.89s, `cursor` 2.74s, the other two under 0.6s. Immediately re-run, every probe came back under 1s. The probe is launching a real vendor binary, so the first one pays that vendor's start-up — don't read a multi-second `--version` row as a hung seat, and don't quote a warm number as the cost of the check. **`grok` was absent on 2026-08-10, and it is installed now.** The original row read `binary FAILED`. That row was the report in correct operation, not a doctor defect. An operator installed grok later on the same day. A re-run on **2026-08-14**, from a binary built from `main` at `230ba54`, reports **all five seats as `binary ok` and `drivable ok`, and each one as a native executable**. The run passed 15 checks and failed 0 over 5 seats. This machine now seats the same five vendors as the Windows machine. The capability gap that the Mac entry in `dotfiles/PARITY.md` tracked is closed, and that entry is retired. The re-run also showed two things worth a record. First, `agy` reported `1.1.11` and not the `1.1.10` in the table above. `agy` updates itself, so a version in this table is a timestamp and not a pin. Second, the `grok` probe was the fastest of the five at 0.03s, against 7.02s for a cold `agy`. Do not read a slow `--version` as a sick seat, and do not read a fast one as a healthy install. That is the reason `auth` and `network` stay `not checked`. Treat a wrong-looking doctor row on the Mac as unverified rather than broken, and record what you find here. **Linux ran it on 2026-09-02** — a source build of the crew integration, on the Linux box the integration was done on, with `HOME` pointed at an empty directory. One vendor was installed there: `claude` at `/opt/node22/bin/claude` on PATH, `2.1.258 (Claude Code)`, `drivable ok` as a native executable. codex, agy, cursor-agent and grok were absent and reported `binary FAILED`, naming the known install locations under the sandboxed home they also looked in. The tally was `3 checks passed, 4 failed, 18 not checked, over 5 seats`, exit 0; `auth` and `network` read `not checked` on every seat; nothing was written under that home. The same build's `telltale council ls` printed `host none is running` and `no room is saved yet` at exit 0 and wrote nothing, and `telltale council replay-check` over the checked-in fixture listed its three seats, two session ids, three tool lines with the gate's verdict and the counts, exit 0. That is what the README means by the `linux_amd64` source build having been driven by hand; the `linux_amd64` archive a release attaches is still not run. ## The macOS arrival of a released archive **Measured 2026-08-17.** Every macOS run recorded above used a binary built on the Mac itself. This entry is the first one that starts at a published release asset, which is what a stranger actually gets. | field | value | |---|---| | machine | Intel x86_64 MBP | | OS | macOS 26.5.2, build 25F84 (`sw_vers`) | | tag walked | `v0.2.0`, asset `telltale_0.2.0_darwin_amd64.tar.gz`, no re-tag | | transport | `curl -fsSL` over HTTPS, HTTP 200, 3,912,801 bytes | | checksum | `shasum -a 256 -c checksums.txt --ignore-missing` printed `telltale_0.2.0_darwin_amd64.tar.gz: OK` | | signature | `codesign -dvv` printed `code object is not signed at all`; `spctl -a -vv` printed `rejected` and `source=no usable signature` | | binary identity | `telltale version` printed `telltale 0.2.0` | **The method reproduces the browser download; it is not a browser download.** `curl` writes `com.apple.provenance` and does **not** write `com.apple.quarantine`, which was confirmed with `xattr -l` on the fetched archive. Quarantine is the attribute Gatekeeper acts on, so the operator wrote it by hand with `xattr -w com.apple.quarantine "0083;00000000;Chrome;"` before unpacking. The system `tar` then propagated it: the extracted binary carried `com.apple.quarantine: 0283;6a832c78;;`. Read every result below as measured over a reproduced mark rather than over a real browser download. **The gate, verbatim.** `./telltale doctor` under the mark was killed every time. The terminal reported: ``` /bin/bash: line 1: 45341 Killed: 9 ./telltale doctor ``` The exit status was 137, and the binary wrote nothing to stdout or stderr. macOS also raised a dialog on the operator's screen, which read: ``` "telltale" Not Opened Apple could not verify "telltale" is free of malware that may harm your Mac or compromise your privacy. [Move to Trash] [Done] ``` The operator clicked neither button. The binary was **not** deleted by the kill; it stayed on disk with the attribute intact. **The remedy exercised.** `xattr -d com.apple.quarantine telltale` removed the attribute, which `xattr -l` confirmed. `./telltale doctor` then ran to completion at exit 0 and reported `15 checks passed, 0 failed, 10 not checked, over 5 seats`. Right-click-open was never a candidate here: `telltale` is a command-line binary and not an app bundle, so the Finder path does not apply to it. One shape worth carrying into the docs: `xattr -d com.apple.quarantine` prints `xattr: telltale: No such xattr: com.apple.quarantine` and exits 1 when the attribute is absent, so it is not a harmless no-op line in a `curl` sequence. That is why `README.md` keeps `xattr -d` out of its main block and offers it as the browser-path remedy. The block it does show was then run line by line in a clean directory on the same day, and every line exited 0. **Owed, and unrun at any date:** - **A real browser walk.** Nobody has downloaded a release archive with a browser on this machine and run the result. Until somebody does, the quarantine mark in this entry is reproduced and the flags a real browser writes are unmeasured. - **The System Settings > Privacy & Security "Open Anyway" path.** Only the `xattr -d` remedy was exercised. Whether that pane offers an entry for a killed command-line binary, and whether the entry works, is unmeasured. - **The `darwin_arm64` archive.** It was not walked at all. Since 2026-09-02 `ci.yml`'s `darwin` job runs a binary built from each commit on an Apple Silicon runner, which retires "built, not run" for the platform; the archive goreleaser attaches is still the thing nobody has unpacked and run there. The Homebrew tap on Apple Silicon is also unwalked; the Intel walk below installed `darwin_amd64`. Windows and SmartScreen belong to the other machine. `SECURITY.md` still records that prompt as unmeasured, and nothing here changes it. ## Homebrew tap, walked 2026-09-11 The tap was already present on this Intel Mac. `brew install telltale` failed until an explicit trust grant. Homebrew 6.0.22 refuses to load a third-party formula from an untrusted tap. The error named two remedies. This walk used the formula-level grant, because Homebrew prefers a narrow grant over whole-tap trust: ``` brew trust --formula sanlee-ys/telltale/telltale ``` stdout: `Trusted formula: https://github.com/sanlee-ys/telltale/telltale`. `brew tap-info sanlee-ys/telltale` still printed `Untrusted` after that, which is the tap-level state. The formula grant is what `brew install telltale` needs. | field | value | |---|---| | machine | Intel x86_64 MBP | | OS | macOS 26.6.2, build 25G83 (`sw_vers`) | | Homebrew | 6.0.22 | | tap HEAD | `2b6d511` (same as `origin/main` that day) | | tag walked | `v0.3.0`, formula URL `telltale_0.3.0_darwin_amd64.tar.gz` | | install | `brew install telltale` wrote `/usr/local/Cellar/telltale/0.3.0` (6 files, 11.5MB) | | PATH | `/usr/local/bin/telltale` → `../Cellar/telltale/0.3.0/bin/telltale` | | version | `telltale version` printed `telltale 0.3.0` | | `brew test` | ran `${bin}/telltale version`, exit 0 | | quarantine | `xattr -l /usr/local/bin/telltale` printed nothing | | signature | `codesign -dvv` printed `code object is not signed at all` | | doctor | exit 0; 6 passed, 3 failed (`codex`, `agy`, `cursor-agent` absent on PATH), 16 not checked | | `council ls` | `host none is running`; `no room is saved yet` | Homebrew printed an Intel x86_64 support warning (no bottles as of September 2026). The formula does not use a bottle. It fetches the GitHub release archive. The warning did not block the install. Apple Silicon `brew install` remains unwalked. ## Terminal profile The same frame reads better on macOS than on Windows, and the reason is not a platform difference in telltale — there is one code path, one glyph set, and Windows Terminal is the declared reference environment. macOS terminals ship with more generous leading and a softer rasterizer. What closes most of that gap is the terminal's own configuration, and it is usually untouched. This profile is the tuned reference. It is additive: keep your existing profiles and open this one to compare. ```json { "name": "telltale council", "commandline": "pwsh.exe -NoExit -Command \"telltale council\"", "font": { "face": "JetBrainsMono Nerd Font Mono", "size": 12, "weight": "medium", "cellHeight": "1.25" }, "padding": "16, 12, 16, 12", "antialiasingMode": "grayscale" } ``` Why each of those, since none of it is obvious: - **`cellHeight: "1.25"`** is the one that matters. It is the leading macOS gives you by default and Windows Terminal does not, and a grid of character cells with no air between rows is most of what "cramped" means. Requires Windows Terminal 1.19 or newer. - **`weight: "medium"`** compensates for DirectWrite drawing thinner stems than CoreText at the same nominal weight. This is the closest a setting gets to the rasterizer difference; it does not eliminate it. - **`antialiasingMode: "grayscale"`** rather than ClearType, whose subpixel fringing is what reads as "digital" against a macOS capture. - **`padding`** buys margin the frame itself should not have to spend cells on. **Do not reach for a smaller font to fit more in.** `TestFrameCorpusReportsFill` in `internal/council` measures this: an idle four-seat room occupies about six rows *regardless of window height*, so every additional row of terminal is one more row of rules drawn around nothing — 75% of the frame at 24 rows, 90% at 60. Shrinking the font makes an idle room emptier, not denser. Going up a size is the counterintuitive but measured direction. **What this cannot fix.** DirectWrite is not CoreText and a TUI cannot reach past its terminal's rasterizer. The bar here is the best native result on each platform, not pixel parity with the Mac; anything claiming otherwise is selling a screenshot. ### The clipboard is not the same mechanism on both machines **Measured 2026-08-10**, and it is the one place where the reference box being Windows hid a real defect rather than merely flattering the render. `y` copied correctly on Windows Terminal and copied **nothing** on the macOS box, in the same build, while reporting `copied …` on both. The mechanism was OSC 52 — an escape sequence written into the terminal with no acknowledgement of any kind (design.md §9.15). Windows Terminal honours it. **Terminal.app does not implement it at all**, and **iTerm2 ships the permission off** (General → Selection → *"Applications in terminal may access clipboard"*). Because the sequence cannot be acknowledged, council had no way to tell the two outcomes apart, and the notice claimed the copy on both. Fixed by preferring the platform's own helper, which is checkable where the escape sequence is not: | platform | mechanism | can council tell whether it worked? | |---|---|---| | macOS | `pbcopy` | **yes** — exit status | | Linux | `wl-copy`, else `xclip -selection clipboard` | **yes** — exit status | | Windows | OSC 52 | no, and it does not need to: measured working | | over SSH, anywhere | OSC 52 | no — the standing limitation, unchanged | If `y` ever reports a copy and your clipboard is empty, the seat fell back to OSC 52 and the terminal ate it. That is a terminal fact, not a council bug — record the emulator and its version here. ## What does not travel between machines - **The room.** `~/.telltale/council/room.json` holds each vendor's *session ids*, and those point into that machine's local session store. Copying the file does not copy the transcripts. Council already handles this honestly: a seat whose thread the vendor no longer has says the history is gone and starts fresh, briefed. - **Flow artifacts.** `~/.telltale/council/artifacts/` is machine-local for the same reason. - **The brief.** Deliberately outside the repo, so git does not carry it. **The cross-machine carrier is `STATE.md` plus GitHub**, not room state. That is what those are for. ## Flags worth knowing `--write` is accepted and ignored — the room writes by default now. `--read` opens a deliberation-only room in which no seat may touch the workspace. If you are following older notes that say to pass `--write`, they are stale.