Convert Codex plugin to an OpenCode plugin#1
Merged
Conversation
Convert the Codex Claude Code plugin into an OpenCode plugin that
delegates work to a headless `opencode serve` over HTTP + SSE.
Phase 0 (scaffold):
- Move plugins/codex -> plugins/opencode; rename manifests, env vars
(OPENCODE_COMPANION_*), commands, agent, and skills.
- Remove the Codex app-server type-generation build (prebuild/build,
tsconfig.app-server.json, app-server-protocol.d.ts) and its CI step;
drop the now-unused typescript devDependency.
Phase 1 (runtime bridge):
- Replace the Codex app-server + Unix-socket broker with OpenCode's
native server: server-lifecycle.mjs (spawn/reuse/teardown
`opencode serve`, health-poll startup), opencode-server.mjs
(zero-dep fetch + SSE client), opencode.mjs (SSE turn capture).
- Read-only via the `plan` agent; write turns auto-approve permissions
via a wildcard rule; models map spark -> openai/gpt-5.3-codex-spark
and provider/model -> {providerID, modelID} on the message.
- Do not send `directory`/`model` on create-session (real server 400s);
permission replies use {response: always|reject}.
- Fake `opencode serve` test harness; 30/30 tests green.
Verified live against a real `opencode serve`: write turn (edit
auto-approved, file written), read-only turn (no edits), sessions
visible in `opencode session list`, progress streamed to the Claude TUI.
transfer remains stubbed (Phase 3). See IMPLEMENTATION_NOTES.md and
docs/opencode-adaptation-plan.md.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VT7Nv9u8ckVJ5vRxzvegiw
OpenCode returns json_schema output as a synthetic "StructuredOutput" tool call whose completed `state.input` holds the schema-conforming object — there is no text part. `/opencode:review` therefore captured the model's prose and failed JSON parsing. - Extract `state.input` from the StructuredOutput tool part and use it as the final message when an output schema was requested. - Align the result renderer's stale fallback field (`result.opencode.stdout`, was `result.codex.stdout`). Verified live: `/opencode:review` now returns parsed structured findings; `npm test` 30/30. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VT7Nv9u8ckVJ5vRxzvegiw
Implemented by Codex, then a two-way review round (Claude reviewed Codex's work; Codex reviewed the result), with Codex's findings fixed. - server-lifecycle: add an inter-process lock to `ensureServer` so concurrent plugin commands don't spawn duplicate `opencode serve` processes. Atomic mkdir lock, token-matched release, double-checked healthy-session read, dead-PID/age stale detection. Review-round fixes: stale-takeover TOCTOU -> atomic single-winner rename-steal; leaked lock on owner-file write failure -> cleanup on failure. - cancel: record each job's `serverUrl` and abort against that specific server (fallback to `ensureServer`), so cancel reaches the right instance for backgrounded jobs. - tests: StructuredOutput json_schema regression test; concurrent- startup test now uses a race-safe boot-marker counter (the fixture's `serverStarts` read-modify-write could itself miss a double-spawn). - gpt-5-4-prompting skill generalized to be provider-neutral; `codex-prompt-*.md` -> `opencode-prompt-*.md`. npm test 32/32. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VT7Nv9u8ckVJ5vRxzvegiw
Delegated to Codex, then live-verified end-to-end by Claude. - `transfer` converts the current Claude Code JSONL transcript into an OpenCode import document and runs `opencode import <file>`, creating a resumable session with full user/assistant turn history; prints `opencode --session <id>` to continue. - New pure converter `buildOpenCodeImportDocumentFromClaudeJsonl`: extracts text from user/assistant content (string or parts), skips non-text parts (thinking/tool_use/tool_result/images) and empty messages, normalizes timestamps to strictly increasing (preserving order), and threads assistant `parentID` to the preceding user message. - Edge-case fix from live testing: drop assistant turns before the first user message — `opencode import` rejects a null `parentID` and would otherwise fail the whole import. - Fake CLI fixture handles `import`/`--version`; converter + command tests, including a leading-assistant regression test. Live-verified: real transcripts import with correct roles/order/text and are resumable. npm test 35/35. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VT7Nv9u8ckVJ5vRxzvegiw
From Codex's independent Phase 3 review (second pass): - Skip `isSidechain` transcript entries in the converter — Claude Code interleaves subagent turns into the same JSONL, and importing them would pollute the transferred main conversation and mis-thread assistant `parentID`s. Live-verified: subagent content no longer leaks. - Fake `opencode import` now enforces the real importer's constraints (session `slug` present; assistant `parentID` a string) so a converter schema regression fails the end-to-end test. npm test 36/36. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VT7Nv9u8ckVJ5vRxzvegiw
This was referenced Jul 8, 2026
Owner
Author
🔎 Adversarial review backlogWhile smoke-testing the OpenCode subagent flow on this branch, we ran an OpenCode adversarial review of the conversion code itself, plus a transport root-cause diagnosis by Claude Code. The review turn actually completed, but the companion returned Backlog (grouped by category):
Line numbers in each issue are from the reviewed revision — verify against current before fixing. Suggested first two to land: #4 (silent result loss on cancel race) and the #2 result-fallback recovery. Generated with Claude Code |
The turn was gated on Promise.all([completion, sendMessage]). The /message POST is held open for the whole turn, so on long turns (e.g. a routine 6-min review) it hits undici's ~300s fetch timeout and rejects with `fetch failed` ~before `session.idle` arrives — failing a turn the server actually completed. - Drive completion from the event stream; treat the prompt request's transport failure as non-fatal, but still fail fast on a real HTTP rejection (bad request/model). - Add OpencodeServerClient.listMessages and recover the finished assistant message over HTTP when the turn ends without a captured result. - Cap runaway turns with a generous outer timeout (review finding #17) and clean up the timer. - Fixture: serve GET /session/:id/message and simulate a mid-turn transport drop; add two regression tests that the old Promise.all path fails. Refs #2 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The "no recoverable current-turn message" test seeded the server session's directory with the raw makeTempDir() path. On macOS that is a /var symlink, but findLatestTaskThread matches against the realpath'd workspace root, so --resume-last threw "No previous session" before the turn ran (empty stdout), masking the actual Fix-1/Fix-2 behavior. Seed the realpath instead. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
fix(#2): recover slow turns instead of failing on transport timeout
fix(#4): serialize state.json + compare-and-set status + server leases
fix(#3): correctness — spawn ordering, resumable status, transfer threading, stop-review flag
Replace best-effort abort cleanup with DELETE session cleanup when captureTurn fails before a prompt is sent.
fix(#5): leaks — temp-dir cleanup, cancel server teardown, orphaned session deletion
fix(#6): error-handling — session-end ordering, stop-review cleanup, cancel logging, event-stream cleanup
The test asserted payload.turnId, but the task --json payload exposes only
{status, threadId, rawOutput, touchedFiles, reasoningSummary} — no turnId — so
it was always undefined. status 0 + the recovered rawOutput already prove the
.pop() fallback selected the right message despite a stale event id.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…sync fix(#12): transport — sync capture + fresh-connection recovery + pop-fallback
…ening Fix #13: cancel / teardown / lease hardening
…ardening Fix #15: transport & recovery hardening
Fix #16: don't follow untracked symlinks in review context
The stop-review gate previously returned no decision when OpenCode was unavailable, which Claude Code treats as allow — so a missing reviewer (PATH/CI misconfig) silently bypassed a gate whose own setup text promises it can "require" a fresh review. The gate is opt-in, so when enabled and the reviewer is unavailable it now emits an explicit block decision with an actionable reason (restore OpenCode, or disable via /opencode:setup --disable-review-gate). Available-reviewer and disabled-gate paths are unchanged. Setup/README wording updated to match. Regression test covers the unavailable-while-enabled block. npm test: 73 passed, 0 failed, 0 skipped (localhost tests included). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
package.json had no files allowlist and no .npmignore, so `npm pack` archived the whole tree including .claude/settings.local.json (user-local shell allowlists, machine paths, PIDs). Add a `files: ["plugins/opencode/"]` allowlist scoped to the shipped plugin artifact (manifest, hooks, commands, agents, prompts, schemas, scripts all confirmed present in the pack; settings + tests excluded). New hermetic tests/packaging.test.mjs plants a sentinel settings.local.json and asserts `npm pack --dry-run` excludes it and the test tree while including the plugin manifest/entrypoint. Also drop conversion residue: remove the unused `@openai/codex` install from CI (the suite uses the fake OpenCode fixture) and delete the unreferenced tests/fake-codex-fixture.mjs. npm test: 73 passed, 0 failed, 0 skipped (localhost tests included). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Fix #18: fail closed when stop-review gate reviewer is unavailable
Fix #17: tighten npm-pack surface; drop Codex conversion residue
- SessionEnd teardown is now bounded: teardownServerSession takes a lock-acquire
timeout and returns {skipped, reason:"lock-timeout", diagnostic} on
contention; the SessionEnd hook passes a 3s server-lock + 500ms state-lock
budget, logs the diagnostic, and skips clearing state on timeout so it can't be
killed mid-teardown by the hook host. ensureServer also fails safely when a
bounded acquisition times out.
- State lock adds async APIs (withStateLockAsync / updateStateAsync) that yield
the event loop via awaitable polling instead of Atomics.wait. The async
companion paths (enqueue/spawn bookkeeping, terminal-index sync, cancel read,
in-flight cancellation) and the SessionEnd job cleanup now use them. The
synchronous lock keeps stale-takeover recovery (no premature default timeout;
lockAcquireTimeoutMs is opt-in).
- EPERM is treated as "unconfirmed" (not alive) for lease liveness, so a reused
foreign pid can no longer hold server teardown hostage for the 6h TTL; the TTL
still covers legitimate long sessions.
- server.json is written atomically (temp-file + rename), reusing the state
writer.
npm test: 79 passed, 0 failed, 0 skipped (localhost tests included).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…sue-19-lifecycle # Conflicts: # tests/runtime.test.mjs
Fix #19: server-lifecycle & state-lock hardening
Write turns previously created sessions with a bare
`{permission: "*", action: "allow", pattern: "*"}` rule and answered every
permission.asked with "always". OpenCode merges session rules AFTER the
agent ruleset and resolves with the LAST matching rule, so the wildcard
silently stripped the stock build agent's safety guards
(external_directory/.env reads/doom_loop stay on "ask"), and the
auto-"always" responder would have approved any that survived.
Send no session-level permission rules at all — the stock build agent's
ruleset (wildcard allow + ask-guards, plus its own tool-output allowances)
is exactly the intended headless write profile — and reject every headless
permission ask with a diagnostic naming the gated category. Under the
stock agents an ask only fires when a guard trips, so routine write turns
stay approval-free.
Also documents that OpenCode permissions are approval controls rather than
an OS sandbox, updates the fake fixture so workspace edits are ungated
while a gated ask must be denied, and marks the original auto-approve
design in the adaptation plan as superseded.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ssions Deny gated permissions in headless runs; drop the session wildcard (#26)
…servers (#27) Plugin-owned `opencode serve` processes ran unsecured: any local process that could reach the loopback port had full API access (file content, sessions, lifecycle). Conversely, a user who exported OPENCODE_SERVER_PASSWORD got a server the plugin's header-less client could no longer talk to. Every owned server now gets a per-server random password, passed only via the child environment (never argv) with the username pinned, and recorded in server.json written owner-only (0600). The client sends HTTP Basic auth — matching OpenCode's own convention, username defaulting to "opencode" — on all four paths: normal requests, fresh-connection requests, health checks, and the SSE event stream. On 1.17.15 every route including /global/health and /event requires the header, verified live. External servers configured via OPENCODE_COMPANION_SERVER_URL read the same OPENCODE_SERVER_PASSWORD/OPENCODE_SERVER_USERNAME variables the user already exports for a secured server; an unauthenticated 401 now produces a precise diagnostic instead of a generic health failure. Cancel processes re-derive credentials from the persisted server session or the ambient variables, and teardown authenticates its dispose call (dispose only cleans instance state — the PID termination remains the actual shutdown). The fake fixture now enforces Basic auth on every route whenever a password is present in its environment, so the whole existing suite exercises authenticated paths; new tests cover unauthenticated HTTP/SSE rejection, owner-only server.json, credential redaction in status output, and password-protected external servers (missing, wrong, and correct credentials). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Authenticate plugin-owned OpenCode servers; support secured external servers (#27)
PR #35 was accidentally merged into its stacked base branch fix/issue-26-write-permissions instead of opencode-conversion; this brings the already-reviewed content onto the integration branch.
…#28) The SSE client parsed an invented event shape: it looked for parts on message.updated and never handled message.part.updated, so against a real server (1.17.15 OpenAPI verified) no streamed text, reasoning, or structured-output part ever arrived — a valid answer could be reported as failed whenever the held-open /message response dropped and HTTP recovery was unavailable. Question events had no handler at all, so a plan-agent question stalled the turn for the full 30-minute ceiling. Client changes: - message.part.updated: accumulate part snapshots by part id (insertion order preserved) and assemble the main-session final message from the turn's assistant message text parts. - message.part.delta: append field deltas to known parts; the next snapshot stays authoritative. - message.updated is metadata-only (properties.info): assistant message id (fixes turnId picking up evt_ envelope ids) and json_schema output via info.structured. - session.created/session.updated register child sessions from properties.info (id/parentID/title/agent), so subagent output is labeled instead of polluting the final message. - question.asked / question.v2.asked are rejected immediately via POST /question/{requestID}/reject with a progress diagnostic — the model must proceed autonomously instead of stalling headlessly. The fake fixture now emits the real envelope ({id: evt_*, type, properties}) with contract-complete Session/AssistantMessage/Part objects, and a new contract test drives it over raw HTTP and validates every emitted event against tests/opencode-event-contract.json, pinned from a real 1.17.15 /doc — the fixture can no longer drift back to invented shapes. New regression tests cover question rejection, delta-only final-message assembly (dropped POST, recovery withheld), and subagent isolation. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Implement the real OpenCode event contract; reject headless questions (#28)
External-server mode had two workspace-safety gaps. First, the client never sent the `directory` query, so every project-scoped request (and the /event subscription) resolved against the server process's launch directory — a review could inspect, and a write task could edit, the wrong repository. Second, job records stored only serverUrl; cancel recomputed "external" by comparing against the CURRENT process's OPENCODE_COMPANION_SERVER_URL, so a cancel process without the job-start environment would send /global/dispose to a user-managed server. The client now appends the canonical (realpath) workspace directory to every non-/global/ route — all of them accept it per the 1.17.15 OpenAPI document, and live checks confirm session creation and listing scope to the query value. Job records persist `serverExternal` at job start (and via progress updates for foreground runs); cancel passes the persisted flag through interruptServerTurn, which no longer infers ownership from the environment. Teardown runs only when the job explicitly recorded plugin ownership; a recorded URL with unknown ownership (legacy records) fails safe and leaves the server running. The fixture now honors the directory query on session create/list and records the scope of event subscriptions. New tests: two workspaces sharing one external server get correctly-scoped sessions and event streams; cancelling a job whose environment differs from the job-start environment aborts the session but leaves the external server alive (the fixture exits on dispose, so a regression fails the health check); client unit coverage that project routes carry the directory and /global routes never do. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…wnership Bind requests to the invoking workspace; persist server ownership (#29)
…#30) A closed event stream raced the whole turn: after a 5s grace, captureTurn made one recovery attempt and could fail immediately even while the held-open /message request was still running and about to succeed (the review reproduced a turn failing at ~5.8s while the response completed at 6.2s). Treat stream closure as loss of one observation channel, not turn completion. After the stream drops, wait the grace for the response and trailing events to land, then actively poll the server for the finished message on an interval until the turn completes another way (response fallback, HTTP recovery, a reconnected session.idle) or the OUTER turn timeout fires. The turn is never failed merely because the stream ended. The grace is now a delay before polling starts rather than a deadline, and a bounded poll interval (default 2s) governs subsequent attempts. The loop is drained in finally so it cannot poll past return, and it still honors the outer timeout so a genuinely stalled turn ends promptly instead of spinning. captureTurn is exported for tests, which drive it against a localhost server across three timings: stream drop + slow-but-successful response (previously failed at the grace), stream drop + lost POST recoverable only via GET, and stream drop + response that never lands (must end near the outer timeout). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…-recovery Poll for recovery after a stream drop instead of failing at the grace (#30)
Stale persisted server records were torn down by signalling the stored
PID and its process group with no check that the PID still belonged to
the plugin's `opencode serve` — after OS PID reuse this could SIGTERM an
unrelated process.
Teardown now re-reads the PID's live command line (cross-platform, via
the same helper the task-worker guard uses) and signals only when it
still looks like the plugin-owned `opencode serve` for the session's
port. The port is derived from the persisted URL when the explicit field
is absent, so records written before this change remain verifiable
instead of falling open. Anything unverifiable is left untouched; only
the stale metadata is cleared, reported as
{ killSkipped: true, reason, diagnostic } — `skipped` keeps its existing
"teardown did not run" meaning (leases/lock timeouts), so the SessionEnd
hook still clears the record correctly. Cancel logs the skip.
The matcher requires an opencode-ish executable token plus
`serve --port <port>`, and re-splits shell-wrapped Windows command lines
(cmd.exe /c "opencode serve ...") so win32 servers do not become
unkillable orphans. The spawn-time command line is recorded for
forensics; verification always matches the live value.
Drafted by OpenCode via the plugin's own rescue path (issue #31 task),
then reviewed and reworked: fail-closed for legacy records, URL-derived
port, de-duplicated cleanup, return-shape fix for the SessionEnd hook,
Windows shell-blob matching, and expanded tests.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ck (#32) /opencode:review and /opencode:adversarial-review both rendered the adversarial "break confidence" template — only the result label differed — and the normal review silently accepted focus text its docs said it rejects. getAuthStatus reported loggedIn whenever /config or /provider was merely readable, so setup marked the plugin ready with zero connected providers and the first real task failed later. review now renders a neutral high-signal template (prompts/review.md) selected via explicit per-command config (promptTemplate), rejects positional focus text with an error pointing at adversarial-review, and adversarial-review is unchanged. loggedIn requires a non-empty `connected` set from /provider (id strings or {id} objects); a readable endpoint with nothing connected reports the provider-setup next step, and a failing /provider endpoint is loggedIn: false instead of a crash or a false positive. Drafted by OpenCode via the plugin's own rescue path (issue #32 task), then reviewed and polished: template selection moved from display-label string matching to explicit config, and an invented `connections` field alias dropped from the provider parsing. Tests assert the actual generated prompt per command, the focus-text rejection, and connected/disconnected/failing provider setups against the fixture's real /provider shape. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The README's marketplace command used an invalid source format, so a fresh user failed at the first documented step; CI ran only Node 22 on ubuntu-latest despite declaring Node >=18.18 support and containing Windows-specific process logic; the transfer converter stamped openai/gpt-5.4-mini into every imported session regardless of the user's provider; and the misleading persistThread option suggested review sessions get deleted when it only ever controlled the session title. README now documents `/plugin marketplace add TheRealDinghyDog/opencode-plugin-cc`; CI runs Node 18.18/20/22 on ubuntu-latest plus Node 22 on windows-latest with fail-fast off; imported sessions carry neutral claude-code/imported-transcript metadata (verified against a real OpenCode 1.17.15 import + export); and persistThread is renamed to taskSessionTitle with a comment stating that review sessions intentionally remain reopenable in OpenCode's store. Drafted by OpenCode via the plugin's own rescue path (issue #33 task); review found no defects to correct. Live-verified the neutral metadata against the real importer. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…identity Verify server PID identity before teardown kills (#31)
…setup Split normal vs adversarial review prompts; honest setup provider check (#32)
The windows-latest leg added in this PR ran the suite on Windows for the first time and found six failures, all environmental: - canonicalWorkspaceDirectory and the tests' expected paths disagreed on Windows 8.3 short names (RUNNER~1 vs runneradmin): use fs.realpathSync.native, which expands short names and matches a child process's cwd, consistent with resolveStateDir. - findLatestTaskThread compared stored canonical directories against the raw cwd, which on Windows is git's forward-slash toplevel — resume lookups could never match; compare canonical-to-canonical. - Two tests hand-rolled a shebang opencode stub that Windows cannot execute via PATH; they now install the same .cmd shim the main fixture uses. - node --import was passed a bare absolute path, which Windows parses as an unsupported "d:" URL scheme; pass a file:// URL. - The transfer test faked only HOME; os.homedir() reads USERPROFILE on Windows. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Fix install docs, widen CI matrix, neutral transfer metadata (#33)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Converts this Codex Claude Code plugin into an OpenCode plugin: the same delegation/review workflow (
/opencode:rescue,/opencode:review,/opencode:transfer, …) now drives OpenCode instead of Codex. Both of the original plugin's defining behaviors are retained:~/.local/share/opencodestore, scoped by project dir) and are resumable viaopencode --session <id>.Architecture change
The Codex integration used
codex app-server(JSON-RPC over stdio) fronted by a custom Unix-socket broker to multiplex concurrent calls. OpenCode ships a native multi-client HTTP server, so that whole layer is replaced:codex app-server+ custom brokeropencode serve(HTTP)GET /event(SSE)fetch+ SSE clientNew runtime lives in
plugins/opencode/scripts/lib/:server-lifecycle.mjs(spawn/reuse/teardownopencode servewith an inter-process lock + health-poll startup),opencode-server.mjs(HTTP/SSE client),opencode.mjs(SSE turn capture). The Codex app-server/broker files and thecodex app-server generate-tsbuild step are removed. Still zero runtime dependencies.What's in it (by phase)
plugins/codex→plugins/opencode, manifests/env/commands/agent/skills renamed, TS-generation build dropped.opencode serve; read-only via theplanagent, write turns auto-approve permissions;spark/provider-modelmapping; structuredjson_schemareview output captured from OpenCode'sStructuredOutputtool part.serverUrlsocancelaborts the right server, provider-neutral prompting skill.transfer: converts the current Claude Code JSONL transcript into an OpenCode import document and runsopencode import, recreating a resumable session with full user/assistant history. Skips sidechain/subagent turns, non-text content, and normalizes timestamps.Testing
npm test— 36/36 green, 0 skipped.opencode(task write/read, review with structured output, status, result, cancel, setup, transfer round-trip).directory/model400s, permission-reply schema, concurrency TOCTOU in the lock,parentID/sidechain handling in transfer — all fixed and re-verified.Not included (optional follow-ups)
GET /doc) to restore a type-check build.See
IMPLEMENTATION_NOTES.mdanddocs/opencode-adaptation-plan.mdfor details.🤖 Generated with Claude Code
https://claude.ai/code/session_01VT7Nv9u8ckVJ5vRxzvegiw