Project-specific instructions. OS-level protocol is inherited from ~/.claude/CLAUDE.md.
Local environment specifics and live execution state live in CLAUDE.local.md (gitignored).
A connector that lets Claude Code (Opus, on the Max subscription) delegate scoped agentic
work to a local OpenCode worker driving an arbitrary model (Qwen3.8-27B is target #1). It
runs over the tool boundary (the local opencode serve HTTP API), so it never touches the
Claude subscription auth. The worker-facing protocol is cross-compiled per target by
skillc, and a graded transfer score proves the retarget. The full rationale and decision
trail live in the design spec (pointer in CLAUDE.local.md).
Local endpoint details (provider name, model endpoint, how to bring the served model up and
down) are environment-specific and live in CLAUDE.local.md.
- Start the server FROM THE REPO ROOT so it loads the worker agent:
cd <repo> && opencode serve --port 47611 --hostname 127.0.0.1 &. The protocol is that agent's system prompt; if you editprotocol/opencode-worker-protocol.md, re-runpython3 scripts/build_agent.pyand restart the server (agents load at startup only, no hot reload). - Configure a provider in
~/.config/opencode/opencode.jsonpointing at your served model endpoint, then bring that model up. The concrete provider/endpoint for this box is inCLAUDE.local.md. - Smoke tests:
python3 tests/smoke.py(library path),python3 tests/mcp_smoke.py(MCP stdio path),python3 tests/agent_smoke.py(protocol-via-agent). All expect the server up (from the repo root), the model served, and the worker agent loaded. - Drive a session by hand:
python3 src/opencode_worker.py {start,steer,pending,approve,status,final,stop,run} ...
Against opencode serve v2 (/api):
- Responses wrap the payload under a top-level
datakey (sometimes with sibling keys). Always unwrapdata. The driver's_reqdoes this. - Create session:
POST /sessionwith{model:{providerID,id,variant}, location:{directory}}; the returned sessionidisses_.... - Prompt / steer:
POST /session/{id}/promptwith{prompt:{text}, delivery:"steer"|"queue"}. A PLAIN prompt (nodelivery) STARTS the turn;steerinjects into an ALREADY-running turn and will NOT start one (a fresh session given a steer sits idle at zero tokens).POST /session/{id}/interrupthalts. (Grounding found a cold receiver stuck 6+ min by starting with a steer.) - The session
location.directory(workdir) MUST live under the diropencode servewas started from (its project root). An external dir (e.g./tmp/...) makes every filesystem tool fail with a genericUnable to write/executed:falseAND raises NO serviceable gate (external-directory hard-denies instead of asking), so the worker misreads a broken sandbox. Our tests only passed because.work/...is inside the repo; grounding from/tmpsurfaced this. GET /session/{id}/messagereturns items NEWEST-FIRST (index 0 is newest). Find the newest assistant message by maxtime.created, never[-1]-- grounding caught a receiver reading a stale mid-turn message and misjudging a finished turn as running.- Messages:
GET /session/{id}/messagereturns items withtypein {assistant,user,system} (norole). An assistant message'scontentis a list of typed parts (reasoning/text/tool); the reply text is thetextparts of the newest assistant message. - Permissions:
GET /session/{id}/permissionlists pending asks as{id, action, resources, ...}(v2) or{id, permission, patterns, ...}(v1). Reply:POST /session/{id}/permission/{id}/replywith{reply:"once"|"always"|"reject"}. Gate a tool by settingpermission: {<tool>: "ask"}inopencode.json(per-tool allow/ask/deny;bashtakes wildcard maps). A workdir-localopencode.jsonscopes it to that session. Better: set the gating in the AGENT frontmatter (permission: {edit: ask, bash: ask, ...}) so it is target-keyed and travels with the pack; opencode parses it into a ruleset of{action, resource:"*", effect}. Valid tool keys: read, edit, glob, grep, list, bash, task, external_directory, todowrite, question, webfetch, websearch, lsp, doom_loop, skill (nowrite; file writes gate underedit). The worker pack gates the mutating + external tools, leaves read-only allowed.scripts/build_agent.pyrenders this from the target'ssettings.permission. - Deliver the worker protocol as an OpenCode agent system prompt, not prepended per task.
Agents load from
.opencode/agent/<name>.md(frontmatter + body = the system prompt) at server STARTUP ONLY: no hot reload, and{file:...}is NOT expanded in theprompt/body (it is stored literally). So compile the agent from the protocol (scripts/build_agent.pywrites the md) and RESTARTopencode servefrom the repo root to load it. Sessions getprojectID:"global", so the per-sessionlocation.directoryis NOT scanned for agents: a workdir-local agent file does nothing. Bind a session to the agent withPOST /session {agent:"<name>"}; an unknown agent name is accepted but the turn silently stalls (the driver'sstartguards against this). Confirm what loaded viaGET /api/agent(v2 fields:id,system). - The session object (
GET /session/{id}) has NO usable status/completion field here (statusis absent,timehas only created/updated). Turn state must be read from the newest assistant message'sfinish:tool-calls= mid-turn step (keep polling),stop/length= done,error= failed turn (with anerrorobject). An errored turn does NOT flip any session-level flag, so polling the session alone hangs to budget. The driver's_turn_status/overall_statusencode this; use them, not_status_of(session(...)). - Concurrency is a SERVING-CONFIG property, not a driver limit (re-derive it each session from
/props, do not freeze it). The driver holds no shared mutable state (OpenCodeWorkercarries onlyself.base; every method is keyed by thesidargument;run()uses only locals), so N concurrentstart/runcalls are safe by construction. The serializer is the engine's slot config:GET <model>/propsreportstotal_slotsanddefault_generation_settings.n_ctx(the per-slot context ceiling).--parallel NWITHOUT--kv-unifiedSTATICALLY cuts context to c/N (n_ctx = 65536 at N=4) and is single-tenant in effect;--parallel N --kv-unifiedshares ONE KV pool (n_ctx stays 262144, each request draws up to the full ceiling, continuous batching multiplexes the turns). tests/parallel_test.py assertstotal_slots>=Nandn_ctx==262144. When proven live it is shared-pool multiplexing (each request can reach the full ceiling; the sum of resident sequence lengths is pool-bounded), not vLLM-style independent full windows. The serving axis lives in the target (settings.serving) and re-keys the pack when it flips. If a serving deployment's manifest still pins--parallel 1, a live patch to--parallel N --kv-unifiedreverts on re-apply unless canonized in the manifest. A Q6_K + speculative-decode Qwen3.8-27B turn runs on the order of tens of tok/s with thinking on; budget 120s+ per agentic turn (concrete live numbers for this box are inCLAUDE.local.md). nvidia-smiinside the serve container shows "No running processes" and 0% util when idle (a PID-namespace artifact + P8 idle state), not a fault. To confirm the GPU is live, sample util during a real generation.
- Honest grade. The worker's "DONE" is not evidence. Verify the real result with your own check. Outcome is binary: built only if every check passes, else honest-failure; never relabel a failing check.
- Subscription-safe. The connector only ever talks to the local opencode server. Do not route Claude Code through a gateway/router (that needs API billing, not the subscription).
- Target is a parameter.
(model, harness, environment), none fixed. OpenCode and Qwen are values #1, not fixtures.resolve_artifacts(target)keys artifacts by the full target. - No em-dashes in anything a human reads (they read as AI-generated). Use commas/colons.
- Commit only when asked. Branch off
mainfor new work.
src/opencode_worker.py driver + Bash connector CLI
src/opencode_worker_mcp.py MCP server (stdio) wrapping the driver as tools
.mcp.json repo MCP config so Claude Code discovers the server
protocol/opencode-worker-protocol.md model-neutral worker contract (skillc cross-compiles it)
scripts/build_agent.py compile the protocol -> target pack + active agent install
scripts/graded_episode.py run graded episodes -> write the pack's grade.json
packs/<model>__<quant>__<harness>/ target-keyed worker pack: agent + manifest + grade.json
.opencode/agent/opencode-worker.md GENERATED active agent install (the server loads this)
skills/opencode-worker/SKILL.md Claude-side orchestrator skill (target-agnostic)
docs/design/artifact-architecture.md the two-sided, target-keyed artifact design
tests/smoke.py re-runnable end-to-end check (library path)
tests/mcp_smoke.py re-runnable end-to-end check (MCP stdio path)
tests/agent_smoke.py re-runnable check: protocol delivered via agent config
tests/target_test.py offline check: pack keying is model/quant/settings sensitive
tests/evidence/ the original proof scripts (scratch paths; historical)
README.md overview + usage
Wrap the connector as an MCP serverDONE (2026-08-17):src/opencode_worker_mcp.py(FastMCP stdio) +.mcp.json; tools start/steer/pending/approve/status/final/stop/run. Re-runnable check:tests/mcp_smoke.py.System prompt via agent configDONE (2026-08-17): the protocol is now theopencode-workerOpenCode agent's system prompt, compiled byscripts/build_agent.py; the driver binds the agent on session create and submits only the task (no prepend). Check:tests/agent_smoke.py.Two-sided, target-keyed artifact architectureDONE (2026-08-17): capturing OpenCode is (a) one Claude-side orchestrator skill (target-agnostic,skills/opencode-worker/) plus (b) an opencode-side worker pack that is model/quant/settings sensitive. The target carries(model, quant, harness, settings, env);resolve_artifactskeys the pack by the full target;build_agent.pyemitspacks/<model>__<quant>__<harness>/(agent + manifest + skill-pack), compiles the settings (permission gating + sampling) into the agent frontmatter, installs the active agent, and records the active target (--list/.opencode/active-target.json). The earned grade travels in the pack (grade.json), written byscripts/graded_episode.pyand surfaced byresolve_artifacts(...).grade. Design:docs/design/artifact-architecture.md. Checks:tests/target_test.py,tests/agent_smoke.py.- Grow the graded co-optimization loop: more tasks/targets, routing divergences to the model delta overlay or the driver protocol; record earned transfer grades per target.
- Bundle for distribution via skillc (the self-building skill compiler + grounding).
(Environment-specific follow-ups, e.g. serving/hardware tuning, are tracked in CLAUDE.local.md.)
The two skills (skills/opencode-worker, skills/model-setup) are skillc self-building files.
Build (compile). SKILL.md IS the source; author/edit it directly. skillc's canonical 84-line
rebuild recipe is stamped VERBATIM once inside it, fenced by <!-- BEGIN/END stamped rebuild recipe -->; never edit inside that fence, and never add a build script or template language to
re-stamp it (a hand-rolled emit_skill.py was removed for exactly that inversion: software instead
of a skill). The recipe's source of truth is skillc at a PINNED TAG, not a copy vendored into this
repo: git -C ~/projects/skillc show v0.3.0:seed/rebuild.skill.md. To re-stamp (only when bumping
the pin), replace the fenced span with that exact output and update the pin named in each SKILL's
<!-- BEGIN stamped rebuild recipe (verbatim from skillc@vX.Y.Z ...) --> marker. SKILL.md is
the CANONICAL, target-agnostic source: it carries no target delta.
Cross-compile (optional, for a known weak target). A variant <name>.<target>.SKILL.md = the
source + a provenance header (right under the frontmatter) + that target's MEASURED delta, stamped
from skillc/seed/targets/<target>.md. qwen-opencode is target #1
(opencode-worker.qwen-opencode.SKILL.md). The delta is measured by grounding, never invented; the
grade stays "not yet measured" until a comparative run earns it. Re-derive the variant (a one-time
agent assembly, no committed script) whenever the source changes.
Test (ouroboros / grounding). A fix is not proven until re-grounded. Ground the COMPILED file
(the source, and each variant): spawn a FRESH agent (Agent tool, general-purpose, NOT a fork)
given ONLY that one SKILL.md path and told to read nothing else in the repo. Live env: opencode serve up from the repo root, the worker agent loaded, a model served (bring the model up per
CLAUDE.local.md). Have it delegate a small independently-verifiable task and honest-grade it; it
reports built / honest-failure / cannot-build and where the file left it guessing. Route each
finding: an opencode API / drive-loop fact is SKILL CONTENT -> the source; a model-mis-follow habit
(relabeling a failed check, over-probing) is a TARGET DELTA -> skillc/seed/targets/<target>.md.
Re-stamp the variant, re-ground. The transfer grade is loss = score(claude reference) - score(target) on the same tasks.
Public-copy invariant. No em-dashes (unicode — or prose --), and run the
avoid-ai-writing pass over the README + skills before any release.
skillcowns the skill build, cross-compile, and grounding (the comparative transfer grade,loss = score(reference) - score(target)); target profiles live inskillc/seed/targets/(theqwen-opencodetarget is already seeded).- This repo owns the connector implementation and its packaging as a skill.
- The design spec is owned in a separate repo; its pointer is in
CLAUDE.local.md.