Skip to content

Convert Codex plugin to an OpenCode plugin#1

Merged
TheRealDinghyDog merged 54 commits into
mainfrom
opencode-conversion
Jul 12, 2026
Merged

Convert Codex plugin to an OpenCode plugin#1
TheRealDinghyDog merged 54 commits into
mainfrom
opencode-conversion

Conversation

@TheRealDinghyDog

Copy link
Copy Markdown
Owner

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:

  • the rescue subagent shows in the Claude Code TUI, and
  • spawned sessions appear automatically in the OpenCode app/TUI (shared ~/.local/share/opencode store, scoped by project dir) and are resumable via opencode --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 OpenCode
codex app-server + custom broker opencode serve (HTTP)
JSON-RPC notifications GET /event (SSE)
stdio client + broker zero-dependency fetch + SSE client

New runtime lives in plugins/opencode/scripts/lib/: server-lifecycle.mjs (spawn/reuse/teardown opencode serve with 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 the codex app-server generate-ts build step are removed. Still zero runtime dependencies.

What's in it (by phase)

  • Phase 0 — scaffold: plugins/codexplugins/opencode, manifests/env/commands/agent/skills renamed, TS-generation build dropped.
  • Phase 1 — runtime bridge over opencode serve; read-only via the plan agent, write turns auto-approve permissions; spark/provider-model mapping; structured json_schema review output captured from OpenCode's StructuredOutput tool part.
  • Phase 2 — hardening: inter-process server lock (atomic single-winner steal), per-job serverUrl so cancel aborts the right server, provider-neutral prompting skill.
  • Phase 3transfer: converts the current Claude Code JSONL transcript into an OpenCode import document and runs opencode import, recreating a resumable session with full user/assistant history. Skips sidechain/subagent turns, non-text content, and normalizes timestamps.

Testing

  • npm test36/36 green, 0 skipped.
  • Every command was live-verified against a real opencode (task write/read, review with structured output, status, result, cancel, setup, transfer round-trip).
  • Each phase went through a two-way review loop (Claude reviewed the delegated work; an independent Codex pass reviewed the result), which caught real bugs — e.g. create-session directory/model 400s, permission-reply schema, concurrency TOCTOU in the lock, parentID/sidechain handling in transfer — all fixed and re-verified.

Not included (optional follow-ups)

  • Deleting read-only review sessions for ephemeral parity.
  • Regenerating TypeScript types from OpenCode's OpenAPI (GET /doc) to restore a type-check build.

See IMPLEMENTATION_NOTES.md and docs/opencode-adaptation-plan.md for details.

🤖 Generated with Claude Code

https://claude.ai/code/session_01VT7Nv9u8ckVJ5vRxzvegiw

TheRealDinghyDog and others added 5 commits July 8, 2026 00:26
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
@TheRealDinghyDog

Copy link
Copy Markdown
Owner Author

🔎 Adversarial review backlog

While 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 fetch failed and dropped the result — findings were recovered directly from the OpenCode server session over HTTP (GET /session/{id}/message). The recovery gap and the underlying cause are captured in #2.

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

TheRealDinghyDog and others added 19 commits July 8, 2026 12:43
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
TheRealDinghyDog and others added 28 commits July 10, 2026 05:27
…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)
@TheRealDinghyDog
TheRealDinghyDog merged commit 1599974 into main Jul 12, 2026
4 checks passed
@TheRealDinghyDog
TheRealDinghyDog deleted the opencode-conversion branch July 12, 2026 22:41
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant