Files
platform/docs/opencode-phase1-report.md
T
pastilhasandClaude Opus 5 140288031a report phases 0 and 1 back to the spec, and hand over phase 2's answer
Implementation report for whoever wrote opencode-parity.md and opencode-phase0-review.md: what landed,
the three places the specs were wrong and how the reproduce-first rule caught each, the three places I
deliberately did not follow them, and what is unverified.

Phase 2's blocking question is answered in full — the serve takes a per-request `?directory=`, so the
coupling is gone in both architectures. The migration is not started; that decision is framed in
opencode-serve-path.md and left open.

Flags `opencode:list` as the one new capability whose happy path is unproven, and B7 as newly more
masked: the B2 fix makes the client send `model` more reliably, which hides the spurious cut-off rather
than removing it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-10 03:09:48 +00:00

148 lines
8.1 KiB
Markdown

# Phases 0 and 1: implementation report, and Phase 2's question answered
**To:** whoever wrote `opencode-parity.md` and `opencode-phase0-review.md`.
**From:** the implementation pass, 2026-08-10 (overnight).
**Scope:** everything in Phase 0 and Phase 1, plus the one Phase 2 question that turned out to be
cheap. Nothing of the Phase 2 migration was started.
Both documents were good to work from. The handover instruction — _reproduce each defect before fixing
it_ — earned its place three separate times, detailed below.
---
## What landed
| Commit | Work |
| --------- | --------------------------------------------------------------- |
| `22bcd7d` | B1 + B3 — session listing, and a resumed session's directory |
| `492509a` | B2 — route a resumed OpenCode session to OpenCode |
| `013e629` | B4 (first attempt), B5, B6, thinking selector |
| `7774a25` | B4 properly — gate the composer on the capability |
| `cfbf58c` | Delete the `AGENTS.md` injection + the one-project comment |
| `d7b2231` | Delete the dead serve-turn client; add `opencode-serve-path.md` |
| `8b409e8` | Phase 1 finish — stale comments, version pin, first tests |
| `e8bd946` | `opencode:list` — running OpenCode turns in the Live panel |
Suite 562 → 573 tests, all passing. `tsgo` clean throughout.
---
## Phase 2: the blocking question is answered
`opencode-phase0-review.md` narrowed the fork to one thing — whether the serve can take a per-request
directory, since without it a serve-based turn path reintroduces the coupling that shelved this work.
**It can.** Against the running serve (1.17.9 on the home-lab box):
```
POST /session?directory=/tmp/oc-phase2-probe → directory: "/tmp/oc-phase2-probe" honoured
POST /session with directory in the BODY → directory: "<serve cwd>" ignored
```
`directory` is a **query parameter on every `/session*` route** — create, message, abort, fork,
summarize, prompt_async, shell — alongside `workspace`. Read off the serve's own `/doc`.
**Worth flagging as a trap:** the body form is silently ignored and yields a session in the serve's own
cwd, which reads exactly like "the serve cannot do per-request directories". My first probe did this and
gave a confident wrong answer. Anyone re-checking this should use the query string.
So the coupling is gone in both architectures: `--dir` anchors a subprocess turn (your test), and
`?directory=` anchors a serve turn (this one). `docs/opencode-serve-path.md` has the three options —
keep the subprocess, move turns to the serve, or a third the parity doc did not list: move only
enumeration and reattach to the serve and leave turns on `opencode run`. It recommends the third and
**does not start any of them**, because which one to take is a product decision.
The first slice of that third option is already in (`e8bd946`, below).
---
## Where your documents were wrong, and how
Three corrections. All were caught by the reproduce-first rule, and none would have been caught by
reading.
**1. `location.directory` does not exist** (parity doc B1/B3, and its suggested fix). opencode 1.17.9+
returns `directory` at the top level, with no `location` wrapper and no `metadata`. The suggested fix —
"derive the directory from `location.directory`" — would have compiled and silently produced `''`
forever. You have since corrected this in `29d9912`.
**2. B2 is right in outline, and the interesting part is not in the outline.** The doc says
`selected.model` is never passed. In fact it _is_ produced correctly — `/chat/:id` resolves
`detail.model` to `opencode/big-pickle` and puts it on `selected` — and then dropped at the `<NewChat>`
boundary, while `initialModel` reads `locationState?.model`, a field **nothing in the tree ever writes**.
So the value existed and was read from the wrong place. Your instruction to confirm all six hops rather
than the endpoints is exactly what surfaced that.
**3. `runner.ts`'s mapping was untested but not pure** (Phase 1 item 9). It lived inside `handleLine` as
a closure over `emit`, the accumulated cost, and a reported-session flag, so it could not be called
without spawning a binary. Extracted as `mapRunLine` — line in, `{sessionId, events, costDelta}` out —
with the two line-spanning concerns left to the caller, because they are not properties of a line.
Behaviour unchanged; 11 tests now pin it.
---
## Where I deliberately did not follow the spec
**B5 — the parity doc says to guard "like the Claude one". Copying that guard would have broken
OpenCode.** Claude keeps one warm session and skips re-subscribing on later turns; OpenCode spawns a
fresh `opencode run` per turn, so a new subscription each time is _correct_. The actual defect was
overwriting the previous handle without detaching it. Fixed that instead. (Your review reached the same
conclusion independently, which was reassuring to read afterwards.)
**B4 — closed the way your review asked, not the way the parity doc did.** The doc offered the flag flip
as "the honest one-liner"; you correctly pointed out that flipping it changed nothing observable because
no code read the capability. The composer now gates on it — drop zone, paste path, attach menu — so the
flag is load-bearing. Unknown model still allows images: a missing capability should not remove a
working control.
**Thinking selector — removed, not hidden.** The doc said hide; hiding a control that does nothing still
leaves it in the tree to be re-found. The inert plumbing beneath it is untouched, because deleting that
touches the socket contract and belongs in its own change.
---
## `opencode:list`, and the one thing to check first
`e8bd946` adds `opencode:list` / `opencode:sessions` and merges both harnesses in `/chat/live`, asked in
parallel, each failing toward empty.
The OpenCode row is deliberately thinner than the Claude one rather than faked into parity:
`isGenerating` is always true (a subprocess exists only while it generates, so there is no "merely
open"), `pendingTasks` is always 0 (no background-task concept — a number would imply one), and
title/cwd are null until the `ses_…` id is reported.
**It has never returned a non-empty list.** No OpenCode turn was running to enumerate. Both failure
modes are invisible — a sidecar that does not reply, and a reply whose shape does not match, both
degrade to `[]`, which looks exactly like "nothing is running". **One real OpenCode turn while watching
`/chat/live` settles it, and that is the highest-value five minutes available tomorrow.**
---
## Other things left unverified
Testing was explicitly de-prioritised for this pass, so these are recorded rather than resolved:
- **The image gate in a browser.** The API reports `images: false` for all 61 OpenCode models and `true`
for the three Claude ones, and the gate reads that field — but no UI was opened.
- **`mapRunLine` fixtures are hand-written**, not captured from a live turn. If one fails after an
upgrade, re-read the binary's real output before editing the expectation.
- **No OpenCode turn was run after deleting `AGENTS.md`.** The premise was verified (the run path sends
no system prompt, so the instruction pointed at nothing) but the outcome was not observed.
`COMMS/BLOCKERS.md` has the same list plus the mistakes I made getting here.
---
## Suggested next, if you are writing the following spec
1. **Exercise `opencode:list`** — it is the only new capability whose happy path is unproven.
2. **Decide the fork.** The blocker is gone; `opencode-serve-path.md` frames it. If the answer is "not
yet", say so in the parity doc so it stops reading as pending work.
3. **The remaining Phase 1 residue**: `sweepStaleServes` is `/proc`-based and a no-op on macOS (B8), and
in-flight `opencode run` children still survive sidecar shutdown untracked.
4. **B7** — the latent spurious `cut-off`, still unaddressed and still masked only by the client always
happening to send `model` alongside `sessionId`. My B2 fix makes the client send `model` _more_
reliably, which deepens the mask rather than removing it.
Item 4 is worth doing before something changes the client's habits and un-masks it.