project-context Skill. chat.message prepares and captures. experimental.chat.messages.transform injects context only when the current turn matches the cached message exactly.
Maturity and installation
opencode --version, locates the config directory through opencode debug paths, and requires the built plugin plus skills/project-context/SKILL.md. It installs them at <config>/plugins/powercontext-opencode.js and <config>/skills/project-context/, with separate ownership manifests.
Setup preflights both target paths and refuses a foreign same-name plugin or Skill. Each installer has its own recovery behavior, but there is no transaction spanning both artifacts. An I/O failure after the plugin install can still leave a partial setup.
Doctor checks the executable and version, the owned Skill, and plugin activation. For activation it starts a short-lived opencode serve on loopback, polls GET /session, waits for a nonce written by the plugin, and then stops the process. It creates no session, passes no model, and does not check Server readiness, prompt hooks, injection, permission UI, or Skill execution.
Minimal usage
Start OpenCode after the Server:pc_* tools for exact Memory, Handoff, Experience, Skill, or Candidate-read operations. The plugin does not start or embed the PowerContext Server.
Execution flow
experimental.chat.system.transform adds general usage guidance only. It is not the PreparedContext injection path.
Scope and session cleanup
Scope resolution uses this order:- Use
POWERCONTEXT_OPENCODE_SCOPE_ID; hash it to a bounded ID if needed. - Otherwise use the session directory as the derivation input. Explicit tools use
context.worktree || context.directory. - Resolve the Git top-level and normalize
remote.origin.urlto agit:Scope when possible. - If no usable remote exists, hash the resolved project root to a stable
local:Scope.
session.created and session.updated refresh the directory cache. If no entry exists, the plugin calls client.session.get. session.deleted removes both that entry and the session’s prepared turn. The runtime keeps at most one prepared turn per session, rather than a map containing every (sessionID, messageID) pair.
Exact matching prevents an older result from entering the wrong turn. There is still an out-of-order race: an older prepare can finish last and replace the newer cache entry. The newer turn then misses injection instead of receiving stale context. Concurrent-turn behavior is Not validated.
Injection
PreparedContext must satisfypowercontext.prepared-context.v1, including matching content_bytes. The configured prepared-content bound defaults to 8,000 bytes and accepts 512–32,768 bytes. The HTTP client separately caps a response body at 1 MiB.
The transform labels recalled content as host-supplied, untrusted historical evidence, marks the part synthetic, and preserves the session and message IDs. A second transform is idempotent.
chat.message does not rewrite the incoming part, and the plugin performs no explicit transcript write. Real-host transcript non-persistence is Not validated; the current unit evidence covers only the synthetic transform path.
Capture and flush
Capture excludes synthetic parts. It writes a deterministic prompt Source withorigin, event, cwd, session, and message metadata. Automatic capture is on by default. It skips capture when disabled, when a prompt looks like a private key or credential, or when its UTF-8 size exceeds 200,000 bytes.
Prepare receives the prompt before the capture filter runs. A secret-looking prompt may therefore be sent to the configured Server as a recall query even though it is not persisted as a Source. Recall transmission and Source capture are separate privacy decisions.
Automatic flush is optional and off by default. When enabled, a positive capture position starts a bounded flush_memory cursor loop. Success requires current_cursor >= position; error or call-budget exhaustion fails open and does not prove that a Memory exists. OpenCode has no pending boundary flusher, and its optional flush path has no OpenCode-specific test, so the flush E2E is Not validated.
The automatic path captures neither the final assistant response nor a Task Outcome.
Owned plugin and bundled Skill
Setup may refresh an owned install but preserves foreign paths. The Skill conflict path is directly tested; the equivalent foreign-plugin conflict follows the same implementation but lacks its own direct test. The installedproject-context directory is a bundled OpenCode Skill. It teaches the host when to use native plugin tools. It is not a managed Skill export. OpenCode is not a current managed export target, and real-host Skill discovery remains Not validated.
Native tools and permission
The plugin mounts 19 curated HTTP tools. They cover Memory reads and mutations, manual prepare/capture, Handoff activate/prepare/finalize/commit/continue, Experience and Skill generate/get, and read-only Candidate Review list/get. It does not mount Candidate approve/reject/revise, Work Contract creation, Handoff acknowledgement, Task Outcome recording, external-Skill registry, or Handoff-report operations. Human Candidate mutation remains outside the model surface through the Dashboard or a direct human-controlled API client. Before one of eight classified mutations, the tool calls hostcontext.ask() with permission powercontext and the operation ID. Those mutations are: remember, capture Source, revise or retire Memory, activate or commit Handoff, generate Experience, and generate Skill. This boundary is Host-gated. pc_remember has a tested ask-and-success path. A rejected ask stops before HTTP by control flow, but real UI allow/deny and a direct denial test are Not validated.
Explicit mutation payloads also reject secret-looking nested strings before transport. This check occurs after context.ask(), so the host can ask before returning secret_rejected.
Failure behavior
Automatic prepare, capture, injection, and optional flush fail open. Server unavailability or missing session context must not block normal OpenCode work. Explicit tool errors remain model-visible. A configuration error disables the plugin hooks with a warning instead of crashing the host.Negative experiments
Run these when changing setup or the plugin. Treat them as a deployment checklist, not as a claim that every item has an automated test:- Report
1.18.20or2.0.0fromopencode --version; setup must refuse before installation. - Put an unowned target at either artifact path; setup must preserve it and refuse the install.
- Stop the Server, submit a prompt, and confirm OpenCode continues without recalled context.
- Prepare one message, then transform a different session or message ID; nothing should be injected.
- Complete two prepares out of order; stale context must not enter the newer turn, though the newer injection may be missed.
- Transform the matching turn twice; only one synthetic part should exist.
- Delete the session, reuse its old IDs, and confirm both caches are empty.
- Submit a secret-looking or over-200,000-byte prompt; recall may run, but capture must not.
- Deny a classified mutation and confirm no HTTP operation runs.
- Enable flush, keep the cursor behind the captured position, and confirm the bounded loop does not claim Memory exists.
- Inspect model tools and confirm Candidate mutation, Work Contract, acknowledgement, and Task Outcome operations are absent.
Unsupported vs Not validated
The conditional real-host E2E launches a real
opencode run, uses an invalid model and empty PreparedContext, and observes prepare/capture HTTP requests. It proves neither model dispatch nor transform injection. Doctor’s nonce probe is a separate activation check.
Configuration defaults
The plugin reads connection settings from the environment. These defaults control automatic recall and capture:
Do not put Authorization in a URL, prompt, or committed example. Use HTTPS outside loopback. Disable capture before launch when the current work must not become Source evidence:
Finish the shared check on this host
What should happen after the OpenAPI contract changes?
- Store the answer in a stable Scope with
pc_remember. Passcontext.askfirst, then confirm the Citation. - Open a new session and ask the same question.
- The recall must include a Citation and appear only as an exact-turn synthetic part on the current user turn. It must not enter the OpenCode transcript.
- Stop the Server and ask again. Automatic recall may fail. Ordinary coding should continue. After you delete the old session, stale context must not be injected.
- Run
pc_rememberagain. The tool should return a failure such asunavailableand must not pretend the write succeeded.
Completion checklist
Treat the integration as complete only when these results are observable:- OpenCode is
>=1.18.21,<2, and Node satisfies the package’s>=20declaration. - Setup installs both owned paths and documents the lack of cross-artifact rollback.
- Foreign plugin and Skill targets are preserved rather than overwritten.
powercontext doctor opencodeis treated as activation evidence, not a Server or model-turn test.- Scope uses an explicit override, otherwise Git identity derived from the session/worktree directory, then a local-root hash.
chat.messageexcludes synthetic text, prepares first, and captures with separate fail-open handling.- Transform injection requires exact session and message IDs and remains idempotent.
- Concurrent out-of-order prepare is classified Not validated and can cause a missed, not wrong-turn, injection.
session.deletedclears session context and the current prepared turn.- Secret and byte-limit filtering applies to capture, while recall transmission is assessed separately.
- Optional flush is off by default, bounded by cursor position and calls, and never stands in for Memory verification.
- The 19-tool surface and eight
context.ask()-gated mutations match the plugin. - Candidate Review stays read-only on the model surface.
- Automatic assistant capture and Task Outcome stay Unsupported.
- Bundled Skill and managed Skill export remain distinct.
- Conditional real-host E2E limits remain visible.

