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

8.1 KiB

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.