Skip to content

feat: add OpenCode integration with plugin, MCP, and skills#72

Open
omergk28 wants to merge 31 commits intoActiveMemory:mainfrom
omergk28:feat/opencode-integration
Open

feat: add OpenCode integration with plugin, MCP, and skills#72
omergk28 wants to merge 31 commits intoActiveMemory:mainfrom
omergk28:feat/opencode-integration

Conversation

@omergk28
Copy link
Copy Markdown

@omergk28 omergk28 commented Apr 26, 2026

Summary

  • Add and harden the OpenCode integration setup so managed plugin, skills, AGENTS content, and MCP config install correctly and refresh when stale.
  • Align help/docs/spec language with actual runtime behavior, especially around background bootstrap and recall-driven context access.
  • Reuse shared AGENTS deployment behavior instead of maintaining OpenCode-specific duplication.

Key changes

  • refresh stale managed OpenCode assets on rerun instead of only skipping existing files
  • harden MCP config generation and refresh behavior
  • quote MCP launcher arguments safely and handle non-ENOENT config read failures correctly
  • validate managed targets before refresh to avoid unsafe/non-regular replacements
  • improve ctx-remember guidance and keep OpenCode docs/spec/help consistent with implemented behavior
  • cover the new refresh and validation behavior with targeted tests

Wire-format change (cross-cutting)

internal/mcp/proto/schema.go: ToolContent.Text drops omitempty. Every tools/call response now emits "text": "" for empty content instead of omitting the key. The MCP spec
defines text as required on type:"text" content, so this is the spec-compliant form; OpenCode's Zod validator enforces it strictly, while Claude Code and Copilot CLI tolerate
either shape.

Verified live: ctx mcp serve (this branch) registered against both Claude Code and Copilot CLI v1.0.40 — tools/call ctx_status succeeded end-to-end on both. Locked down by
TestToolContentTextFieldAlwaysPresent in internal/mcp/proto/schema_test.go so a future revert to omitempty fails CI.

Validation

Automated validation

  • go test ./internal/cli/setup/core/opencode
  • go test ./internal/cli/setup/core/agents
  • go test ./internal/mcp/proto
  • make build
  • go test ./...

Manual smoke test

Validated the OpenCode + ctx integration end to end in a fresh dummy project using an isolated OpenCode home.
Confirmed:

  • ctx setup opencode --write installed the plugin, skills, AGENTS integration, and global MCP config
  • slash skills worked: /ctx-status, /ctx-agent, /ctx-remember, /ctx-wrap-up
  • shell.env injected the correct CTX_DIR
  • edit/write flow worked and captured session diffs
  • OpenCode shell-driven git commit flow worked
  • idle hook fired successfully
  • compaction fired successfully
  • recall still worked after compaction, including seeded task and decision context
  • MCP wire-format compatibility verified live against Claude Code and Copilot CLI v1.0.40

@omergk28 omergk28 requested a review from josealekhine as a code owner April 26, 2026 18:30
omergk28 added a commit to omergk28/ctx that referenced this pull request Apr 26, 2026
Persist learnings, decisions, conventions, and follow-up tasks from
the PR ActiveMemory#72 review and refinement pass.

Learnings:
- ctx system help can list project-local Claude wrappers that aren't
  real Go subcommands; non-Claude integrations only see the Go subset
- Trailing \b in a regex matches commit-tree as git commit; need (?!-)
- make test exit code unreliable due to -cover covdata tooling issue

Decisions:
- OpenCode plugin ships without tool.execute.before until
  block-dangerous-commands is a real ctx system Go subcommand
- Editor plugins must filter post-commit to actual git commit calls

Conventions:
- New editor integrations include an MCP-merge test covering the
  five canonical edge cases

Tasks (follow-up):
- Promote block-dangerous-commands to a Go subcommand
- Type-check embedded TS plugin assets in CI

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
omergk28 added a commit to omergk28/ctx that referenced this pull request Apr 27, 2026
…subdirectory)

OpenCode auto-loads only top-level .ts/.js files under
.opencode/plugins/; subdirectories are silently ignored. The
v0.7.x setup deployed the plugin to .opencode/plugins/ctx/index.ts,
so the entire OpenCode integration shipped in PR ActiveMemory#72 — the
session/idle hooks, the post-commit nudge, the check-task
-completion nudge — was never actually loaded by OpenCode. The
file was correct; OpenCode's discovery rule made it dead code.

Verified by smoke-testing both layouts side-by-side:
.opencode/plugins/ctx/index.ts produced no trace events even
with --print-logs --log-level DEBUG. .opencode/plugins/ctx.ts
loaded immediately, factory-call invoked, tool.execute.after
fired with the expected args shape.

Changes:

- internal/cli/setup/core/opencode/plugin.go now writes the
  embedded index.ts content to .opencode/plugins/ctx.ts (flat).
- New cfgHook.FileOpenCodePluginDeploy = "ctx.ts" constant.
  cfgHook.FileIndexTs is kept as the embedded-asset key (the
  source-of-truth filename in the binary) and its docstring now
  spells out the flat-vs-subdir discovery rule for future
  maintainers.
- Drop internal/assets/integrations/opencode/plugin/package.json
  and its //go:embed directive: the plugin uses a type-only
  import of @opencode-ai/plugin (erased at compile time) and the
  host runtime injects PluginInput, so there is no runtime
  dependency tree to install.
- New errSetup.MissingEmbeddedAsset() helper with a matching text
  key, so the new asset lookup uses the err package rather than a
  naked fmt.Errorf (audit fix).
- specs/opencode-integration.md updated to describe the flat
  layout and a smoke-step that verifies a hook actually fires.
- LEARNINGS.md captures the discovery so future plugins for any
  editor verify load before debugging hook contracts.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@omergk28 omergk28 force-pushed the feat/opencode-integration branch from feee82e to 8a15bd2 Compare April 27, 2026 00:26
omergk28 added a commit to omergk28/ctx that referenced this pull request Apr 29, 2026
Three "I wish I knew this earlier" gotchas surfaced while
fixing PR ActiveMemory#72. Persisting before they fade so the next
@opencode-ai/plugin bump (or anyone wiring a new editor
plugin) doesn't repeat them:

- event hook is a single dispatcher, not an object of named
  per-event handlers — asymmetric with neighboring named hooks
  in the same SDK
- multiple plugin hooks (shell.env, tool.execute.after,
  chat.params, chat.headers, ...) take (input, output) and
  mutate output; returned values are silently discarded
- shell.env env injection only reaches the agent's shell tool,
  not the plugin's own ctx.$ subprocess calls — those need a
  pre-configured BunShell built from ctx.directory

Spec: specs/opencode-integration.md

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
omergk28 added a commit to omergk28/ctx that referenced this pull request Apr 29, 2026
…ctx context across compaction

Smoke-testing PR ActiveMemory#72 with oh-my-openagent@3.17.6 installed
revealed that ctx context survives /compact only by accident:
oh-my-openagent's pre-compaction handler builds a structured
summary template that happens to preserve .context/-prefixed
file paths in its "Active Working Context → Files" section.
Combined with our shell.env CTX_DIR injection, the agent had
enough breadcrumbs to re-read DECISIONS.md from disk after a
test compaction — quoted line 65 verbatim.

That's a fragile property: depends on undocumented
serialization choices in another plugin. If oh-my-openagent
ever drops file-path preservation, swaps section names, or
condenses paths, the breadcrumbs disappear and ctx context
is lost without any signal.

Fix: register experimental.session.compacting in our plugin
and push `ctx system bootstrap` output to output.context. Per
the SDK contract, output.context is *additive* (appends to
the default compaction prompt), while output.prompt is
*destructive* (one plugin replaces another). Pushing to
context composes additively with primary compaction harnesses
like oh-my-openagent — neither plugin needs to know about the
other for the integration to work.

Verified: rebuilt binary embeds the new hook, lint clean
(0 issues), all tests pass. The deployed plugin in the
project's .opencode/ has been updated; relaunching OpenCode
will pick up the new hook on the next session start.

Also persisted as .context/LEARNINGS.md entry 2026-04-29-040000
so a future SDK or oh-my-openagent bump that breaks this
interop is easier to diagnose.

Spec: specs/opencode-integration.md

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@omergk28 omergk28 requested a review from bilersan as a code owner May 1, 2026 04:09
@josealekhine
Copy link
Copy Markdown
Member

Hi @omergk28

Thanks for the careful work on this.

Tthe commit history makes it clear you smoke-tested end-to-end and dug into the actual OpenCode plugin SDK contracts
(signature shape, plugin discovery rule, MCP schema, CTX_DIR resolution)
rather than guessing. The shape mirrors the Copilot CLI integration cleanly,
and the persisted DECISIONS/LEARNINGS entries make the rationale recoverable for future maintainers.

Below is what I found. One blocker, the rest are non-blocking concerns
and follow-ups.


Blocker

Lint failure breaks make test

internal/cli/setup/core/agents/agents_test.go:82 shadows the outer err
declared at line 76:

template, err := agent.AgentsMd()
if err != nil {
    t.Fatalf("read embedded AGENTS.md: %v", err)
}

var buf bytes.Buffer
if err := Deploy(testCmd(&buf)); err != nil {   // <- shadows
    t.Fatalf("Deploy() error = %v", err)
}

govet flags this and internal/compliance/TestGolangciLint fails as a
result, so a full make test is red even though
go test ./internal/cli/setup/core/opencode ./internal/cli/setup/core/agents
passes. One-line fix — rename to deployErr, or hoist the Deploy call
above the template, err := line.


Other Concerns

1. Cross-cutting MCP schema change is buried in an OpenCode PR

internal/mcp/proto/schema.go:

-Text string `json:"text,omitempty"`
+Text string `json:"text"`

Justified by commit afcb1297 (OpenCode's Zod schema requires text
present), but it now changes the wire format for every MCP client — not
just OpenCode. Two requests:

  • Confirm Claude Code and Copilot CLI MCP clients are still happy with
    always-present empty text fields. The MCP spec allows it, so this is
    almost certainly fine, but worth one round of explicit verification.
  • Call this out in the merge commit / release notes so it isn't
    rediscovered as a regression by a non-OpenCode user.

2. MCPConfigPathOpenCode warning string ignores $OPENCODE_HOME

internal/config/setup/setup.go:

MCPConfigPathOpenCode = "~/.config/opencode/opencode.json"

This literal is passed to writeErr.WarnFile when ensureMCPConfig fails,
but the actual writer (globalConfigPath) honors $OPENCODE_HOME. If a
user has $OPENCODE_HOME set and the write fails, the warning will name
the wrong path. Cosmetic but misleading — easy follow-up: have the warning
report the path that globalConfigPath() actually computed.

3. Global config write deserves a caveat in user-facing docs

ctx setup opencode --write is the only ctx integration that mutates a file
outside the project root (~/.config/opencode/opencode.json). It's
intentional (non-interactive shells need the global path — see commit
afcb1297), but it surprises a user who expects setup commands to be
project-local. A one-line note in docs/home/opencode.md ("This also
registers the ctx MCP server in OpenCode's global config at
~/.config/opencode/opencode.json so non-interactive shells can find it.")
would prevent confusion.

4. Plugin refresh semantics aren't surfaced to users

When deployPlugin finds a stale .opencode/plugins/ctx.ts, it rewrites
in place — but OpenCode only picks it up on next launch, not mid-session.
This is the right behavior (and is documented in commit messages) but
isn't mentioned in the quickstart. Worth a sentence: "If you re-run
ctx setup opencode --write, restart OpenCode to pick up the refreshed
plugin."


Documentation gaps

Recipes

  • No docs/recipes/ entry references OpenCode at all. Copilot CLI is
    mentioned in multi-tool-setup.md, guide-your-agent.md, and
    index.md. OpenCode should appear in the same places, at minimum:
    • multi-tool-setup.md — add OpenCode to the # ## Cursor / Aider / Copilot / Windsurf ## block (line 34) and the per-tool sections
      further down.
    • guide-your-agent.md — list OpenCode as an AGENTS.md-aware tool.
    • recipes/index.md — add OpenCode to whatever tool index lives there.

Other missing docs

  • No dedicated troubleshooting section in docs/home/opencode.md. The
    PR commit history surfaces three real failure modes that users will hit:

    • opencode mcp list shows ctx ✗ failed MCP error -32000: Connection closedCTX_DIR not resolving (covered by the sh-wrapper, but a
      user with a stale install needs to know to re-run ctx setup opencode --write).
    • Plugin appears installed but no hooks fire → flat-vs-subdirectory
      discovery rule (the v0.7.x layout). Worth telling users how to verify
      the plugin loaded (opencode --print-logs --log-level DEBUG).
    • ctx agent markdown leaking into the TUI → was the
      .nothrow().quiet() fix; if users ever see it again on an SDK bump,
      they should know what to look for.
  • The hooks.yaml (internal/assets/commands/text/hooks.yaml) gained
    20 lines but docs/operations/integrations.md is the only place this
    surfaces. If there's a generated hook reference page (the way Copilot
    CLI has one), OpenCode needs an entry there too.

  • Block-dangerous-commands omission is permanent (DECISIONS entry
    2026-04-26-231517) but not mentioned in user-facing docs. A user
    coming from Claude Code may notice the missing protection. One sentence
    in docs/home/opencode.md under "What Happens Automatically" would
    cover it: "Note: dangerous-command blocking is Claude Code-specific and
    is not part of the OpenCode integration."

  • Compaction behavior is mentioned in the quickstart but not explained
    for users who don't know what experimental.session.compacting is. The
    current text ("your memory survives compaction") is great for marketing
    but doesn't tell a user what to expect when their context window fills
    up. A short paragraph would help.


What's good

  • Spec is current. specs/opencode-integration.md matches the landed
    code, including the deliberate tool.execute.before skip.
  • Docstrings are not lazy. Each func has Parameters/Returns blocks;
    tricky bits (sh-wrapper rationale, plugin discovery, BunShell
    .nothrow().quiet(), additive output.context) get real prose.
  • The index.ts header comment is genuinely instructive for the next
    maintainer — it spells out the SDK contract gotchas you discovered the
    hard way. Future plugin work for any editor benefits from this.
  • Magic-string discipline preserved — new constants (KeyEnabled,
    DirXDGConfig, FileOpenCodePluginDeploy, CmdFlag,
    FormatPOSIXSpawnRelativeCtxDir, MissingEmbeddedAsset) keep call
    sites clean and audit-friendly.
  • Refactoring AGENTS deployment into shared coreAgents.Deploy is a
    net win — opencode.Deploy() stays small, and Copilot CLI / future
    integrations get the same symlink/non-regular-file rejection for free.

TL;DR

It would be awesome if you can address all these.

The blocker one I stated is a must, but the others are also equally important.

Nice work overall.

omergk28 added a commit to omergk28/ctx that referenced this pull request May 3, 2026
Add TestToolContentTextFieldAlwaysPresent so a future revert of the
omitempty drop on ToolContent.Text fails CI. The MCP spec requires
text present on type:"text" content; OpenCode's Zod validator enforces
it strictly. Verified live (PR ActiveMemory#72) against Claude Code and Copilot
CLI v1.0.40 — both accept the always-present empty-string form.
omergk28 added a commit to omergk28/ctx that referenced this pull request May 3, 2026
Add TestToolContentTextFieldAlwaysPresent so a future revert of the
omitempty drop on ToolContent.Text fails CI. The MCP spec requires
text present on type:"text" content; OpenCode's Zod validator enforces
it strictly. Verified live (PR ActiveMemory#72) against Claude Code and Copilot
CLI v1.0.40 — both accept the always-present empty-string form.

Signed-off-by: omergk28 <omergk28@gmail.com>
@omergk28 omergk28 force-pushed the feat/opencode-integration branch from f17cab6 to ffe9793 Compare May 3, 2026 15:14
omergk28 added 18 commits May 3, 2026 11:17
OpenCode (opencode.ai) is a terminal-first AI coding agent that reads
AGENTS.md natively and supports MCP servers. This adds `ctx setup opencode`
following the Copilot CLI blueprint: a thin TypeScript plugin embedded as a
static asset that shims OpenCode lifecycle hooks to ctx system subcommands.

Deployed by `ctx setup opencode --write`:
- .opencode/plugins/ctx/index.ts — lifecycle plugin (~35 lines)
- .opencode/plugins/ctx/package.json — minimal dependencies
- opencode.json — MCP server registration (merge-safe)
- AGENTS.md — shared agent instructions
- .opencode/skills/ctx-*/SKILL.md — 4 portable skills

Plugin hooks: session.created (bootstrap), tool.execute.before (dangerous
command blocking), tool.execute.after (post-commit + task completion),
session.idle (persistence nudges), shell.env (CTX_DIR injection).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

Signed-off-by: Omer Kocaoglu <omergk28@gmail.com>
…, add tests

The original plugin called `ctx system block-dangerous-commands`, which is
not a real subcommand on the ctx Go binary (it's a Claude-Code plugin-local
hook). On any install without that wrapper Cobra returns exit 1, the
plugin reads that as `{ blocked: true }`, and OpenCode blocks every shell
tool call. Pulling the `tool.execute.before` hook until block-dangerous-
commands is promoted into the Go binary.

Other fixes in the same pass:

- Narrow `post-commit` to actual `git commit` invocations via a regex
  with a negative lookahead so `git commit-tree` / `commit-graph` don't
  trigger it. The previous code ran post-commit after every shell tool.
- Drop the embedded `INSTRUCTIONS.md` asset that nothing read; AGENTS.md
  is what's actually deployed for OpenCode.
- Treat empty / whitespace-only `opencode.json` as "no existing config"
  in `ensureMCPConfig`; previously a pre-created empty file made setup
  hard-error on unmarshal.
- Tighten `extractCommand` to read `{command: string}` shapes instead of
  JSON-stringifying arbitrary input into the dangerous-command pipe.
- Add `mcp_test.go` covering create / empty-file / preserve-keys /
  skip-if-registered / reject-malformed-JSON; add `testmain_test.go`.
- Update user-facing summary text and integration docs to match the
  shipped behavior (drop "blocks dangerous commands" claim, document
  `bun install` step).
- Refresh `specs/opencode-integration.md` to match the landed code and
  record why we deliberately skip `tool.execute.before`.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

Signed-off-by: Omer Kocaoglu <omergk28@gmail.com>
Persist learnings, decisions, conventions, and follow-up tasks from
the PR ActiveMemory#72 review and refinement pass.

Learnings:
- ctx system help can list project-local Claude wrappers that aren't
  real Go subcommands; non-Claude integrations only see the Go subset
- Trailing \b in a regex matches commit-tree as git commit; need (?!-)
- make test exit code unreliable due to -cover covdata tooling issue

Decisions:
- OpenCode plugin ships without tool.execute.before until
  block-dangerous-commands is a real ctx system Go subcommand
- Editor plugins must filter post-commit to actual git commit calls

Conventions:
- New editor integrations include an MCP-merge test covering the
  five canonical edge cases

Tasks (follow-up):
- Promote block-dangerous-commands to a Go subcommand
- Type-check embedded TS plugin assets in CI

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

Signed-off-by: Omer Kocaoglu <omergk28@gmail.com>
The plugin callback's first argument is `{tool, sessionID, callID,
args}` per @opencode-ai/plugin v1.4.x. Destructuring `input` pulled
a non-existent property, so the git-commit detection branch and
the EDIT_TOOLS branch never had a real command to inspect — the
post-commit and check-task-completion nudges silently no-op'd.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

Signed-off-by: Omer Kocaoglu <omergk28@gmail.com>
OpenCode's McpLocalConfig schema (in @opencode-ai/sdk) requires
`command` to be an Array<string> holding both the binary and its
arguments — there's no separate `args` field — and an `enabled`
boolean on the entry. The generator was emitting the Copilot CLI
shape (`command` as a string, `args` as a separate array), so
opencode startup rejected the file with:

  Configuration is invalid at /…/opencode.json
  ↳ Expected array, got "ctx" mcp.ctx.command
  ↳ Missing key mcp.ctx.enabled

Fold mcpServer.Command + Args() into a single command array, set
enabled: true, and drop the args field for the OpenCode path.
The Copilot CLI generator is unchanged — it still uses the
{command, args} split that mcp-config.json expects.

Add KeyEnabled constant; update the MCP regression test to assert
the new shape (command as []string of length 3, no args field,
enabled=true).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

Signed-off-by: Omer Kocaoglu <omergk28@gmail.com>
…subdirectory)

OpenCode auto-loads only top-level .ts/.js files under
.opencode/plugins/; subdirectories are silently ignored. The
v0.7.x setup deployed the plugin to .opencode/plugins/ctx/index.ts,
so the entire OpenCode integration shipped in PR ActiveMemory#72 — the
session/idle hooks, the post-commit nudge, the check-task
-completion nudge — was never actually loaded by OpenCode. The
file was correct; OpenCode's discovery rule made it dead code.

Verified by smoke-testing both layouts side-by-side:
.opencode/plugins/ctx/index.ts produced no trace events even
with --print-logs --log-level DEBUG. .opencode/plugins/ctx.ts
loaded immediately, factory-call invoked, tool.execute.after
fired with the expected args shape.

Changes:

- internal/cli/setup/core/opencode/plugin.go now writes the
  embedded index.ts content to .opencode/plugins/ctx.ts (flat).
- New cfgHook.FileOpenCodePluginDeploy = "ctx.ts" constant.
  cfgHook.FileIndexTs is kept as the embedded-asset key (the
  source-of-truth filename in the binary) and its docstring now
  spells out the flat-vs-subdir discovery rule for future
  maintainers.
- Drop internal/assets/integrations/opencode/plugin/package.json
  and its //go:embed directive: the plugin uses a type-only
  import of @opencode-ai/plugin (erased at compile time) and the
  host runtime injects PluginInput, so there is no runtime
  dependency tree to install.
- New errSetup.MissingEmbeddedAsset() helper with a matching text
  key, so the new asset lookup uses the err package rather than a
  naked fmt.Errorf (audit fix).
- specs/opencode-integration.md updated to describe the flat
  layout and a smoke-step that verifies a hook actually fires.
- LEARNINGS.md captures the discovery so future plugins for any
  editor verify load before debugging hook contracts.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

Signed-off-by: Omer Kocaoglu <omergk28@gmail.com>
…context

The MCP server registered by 'ctx setup opencode --write' failed
to hand-shake from OpenCode. Three failure modes, one root cause:
ctx requires CTX_DIR to be absolute (internal/rc.ContextDir's
"absolute-only hardline"), and OpenCode has no path templating
in opencode.json — neither environment.CTX_DIR=".context" nor a
literal absolute path that follows the user's checkout works.

Without an explicit pin, OpenCode forwards the parent shell's
CTX_DIR. A stale value (anchor drift) gives 'context directory
not found'; an unset value with overlapping .context candidates
gives 'multiple candidates visible'. Both kill the JSON-RPC
handshake before any tool can register, leaving 'ctx ✗ failed
MCP error -32000: Connection closed' in 'opencode mcp list'.

Verified: OpenCode launches MCP children with project root as
CWD and forwards parent env (incl. user CTX_DIR). Both confirmed
empirically with a debug shim that logged argv/cwd/env from
inside an opencode mcp list invocation.

Fix: emit ['sh', '-c', 'exec env CTX_DIR="$PWD/.context" ctx mcp
serve']. $PWD is set by sh to the project root OpenCode chose,
giving us an absolute path anchored to whichever checkout owns
this opencode.json. exec replaces the shell so OpenCode's
process tree has ctx directly, no lingering sh layer.

Verified end-to-end: 'opencode mcp list' shows '✓ ctx connected'
and a manual initialize+tools/list handshake against the same
launcher returns the 15 ctx tools.

Changes:

- internal/cli/setup/core/opencode/mcp.go: emit the sh wrapper
  via a new launchCommand() helper; drop the broken
  environment.CTX_DIR field; comment captures the rejection
  reasoning so a future maintainer doesn't reintroduce the
  relative-path attempt.
- internal/cli/setup/core/opencode/mcp_test.go: assert the new
  shape — sh/-c prefix, script substrings (exec env, the quoted
  $PWD/.context expansion, the wrapped invocation), and an
  explicit assertion that 'environment' must NOT be present (the
  failure mode this commit fixes).
- internal/config/shell/shell.go: new CmdFlag ('-c') and
  FormatPOSIXSpawnRelativeCtxDir constants, keeping the
  inline-script template out of call sites per the magic-string
  audit.

Spec: specs/opencode-integration.md

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

Signed-off-by: Omer Kocaoglu <omergk28@gmail.com>
Capture decision 2026-04-26-231517: the OpenCode plugin's
missing tool.execute.before hook is permanent, not deferred.
Promoting block-dangerous-commands to a ctx Go subcommand was
on the books as follow-up but has been ruled out — Cobra's
exit-1 / { blocked: true } interaction would brick OpenCode for
users without the Claude wrapper.

Marks the Phase-1 task '[-]' skipped with a reason pointer to
the new decision so future sessions don't believe a re-add is
pending.

Spec: specs/opencode-integration.md

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

Signed-off-by: Omer Kocaoglu <omergk28@gmail.com>
Three of four lifecycle hooks were silently no-ops because they
used the wrong signatures for @opencode-ai/plugin v1.4.x:

- shell.env: declared as `() => env` (returns); actual contract
  is `(input, output) => void` (mutates output.env). CTX_DIR was
  never injected into the agent's bash tool, so every embedded
  `ctx system X` invocation fell back to ~/.context.
- event: declared as `event: { "session.created": fn, ... }`
  (object of named handlers); actual contract is a single
  dispatch function `event: ({event}) => void`. session.created
  and session.idle never fired.
- tool.execute.after: declared as `({tool, args}) => void`; the
  actual contract is `(input, output) => void`. The destructure
  worked by accident because tool/args live on input.

The plugin's own `ctx.$` subprocess calls also ran without
CTX_DIR, since shell.env only injects into the agent's shell
tool. Build a CTX_DIR-aware BunShell from `ctx.directory` once
and reuse it for every `ctx system` call.

Verified end-to-end: instrumented plugin in a sandbox project
captures factory invocation, all hook firings, and exit codes
+ stdout from each subprocess. session.created runs bootstrap
and `ctx agent --budget 4000` to exit 0; session.idle runs
check-persistence and check-task-completion to exit 0;
shell.env injects the absolute CTX_DIR for every shell call.

Stale messaging cleaned up alongside:
- ctx setup opencode (dry-run + post-write summary): drop
  references to .opencode/plugins/ctx/index.ts and the never-
  written package.json. The flat-layout fix landed in 8a15bd2
  but the user-facing strings were never updated.
- skip-reason for existing files: was "(ctx plugin exists,
  skipped)" even for opencode.json/AGENTS.md/skills. Now reads
  "(already present, skipped)".
- Go doc comments in opencode.go / doc.go: described the old
  ctx/index.ts + package.json subdirectory layout.
- specs/opencode-integration.md: dropped the stale
  "Add this back when block-dangerous-commands is promoted to
  the ctx Go binary" clause, which was contradicted by
  decision 2026-04-26-231517 making the omission permanent.

Spec: specs/opencode-integration.md

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

Signed-off-by: Omer Kocaoglu <omergk28@gmail.com>
Three "I wish I knew this earlier" gotchas surfaced while
fixing PR ActiveMemory#72. Persisting before they fade so the next
@opencode-ai/plugin bump (or anyone wiring a new editor
plugin) doesn't repeat them:

- event hook is a single dispatcher, not an object of named
  per-event handlers — asymmetric with neighboring named hooks
  in the same SDK
- multiple plugin hooks (shell.env, tool.execute.after,
  chat.params, chat.headers, ...) take (input, output) and
  mutate output; returned values are silently discarded
- shell.env env injection only reaches the agent's shell tool,
  not the plugin's own ctx.$ subprocess calls — those need a
  pre-configured BunShell built from ctx.directory

Spec: specs/opencode-integration.md

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

Signed-off-by: Omer Kocaoglu <omergk28@gmail.com>
…ctx context across compaction

Smoke-testing PR ActiveMemory#72 with oh-my-openagent@3.17.6 installed
revealed that ctx context survives /compact only by accident:
oh-my-openagent's pre-compaction handler builds a structured
summary template that happens to preserve .context/-prefixed
file paths in its "Active Working Context → Files" section.
Combined with our shell.env CTX_DIR injection, the agent had
enough breadcrumbs to re-read DECISIONS.md from disk after a
test compaction — quoted line 65 verbatim.

That's a fragile property: depends on undocumented
serialization choices in another plugin. If oh-my-openagent
ever drops file-path preservation, swaps section names, or
condenses paths, the breadcrumbs disappear and ctx context
is lost without any signal.

Fix: register experimental.session.compacting in our plugin
and push `ctx system bootstrap` output to output.context. Per
the SDK contract, output.context is *additive* (appends to
the default compaction prompt), while output.prompt is
*destructive* (one plugin replaces another). Pushing to
context composes additively with primary compaction harnesses
like oh-my-openagent — neither plugin needs to know about the
other for the integration to work.

Verified: rebuilt binary embeds the new hook, lint clean
(0 issues), all tests pass. The deployed plugin in the
project's .opencode/ has been updated; relaunching OpenCode
will pick up the new hook on the next session start.

Also persisted as .context/LEARNINGS.md entry 2026-04-29-040000
so a future SDK or oh-my-openagent bump that breaks this
interop is easier to diagnose.

Spec: specs/opencode-integration.md

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

Signed-off-by: Omer Kocaoglu <omergk28@gmail.com>
… into TUI

End users running OpenCode in a real ctx-managed project saw
chunks of `ctx agent --budget 4000` Markdown bleeding into the
TUI: section headers like `## Steering` and `# Product Context`,
followed by steering-template placeholder text like
`Describe the product...`. These are real strings from the
context packet that the session.created hook fires.

Root cause: BunShell's documented default behavior is to write
to the parent process's stdout/stderr in addition to buffering.
The plugin used the shell-level `2>/dev/null || true` to swallow
stderr and force exit 0, but stdout was untouched — so every
byte that `ctx agent` emitted got echoed to OpenCode's process
and surfaced through the TUI.

Fix: chain `.nothrow().quiet()` on every BunShell template
literal in the plugin. `.nothrow()` swallows non-zero exits at
the BunShell layer; `.quiet()` keeps stdout/stderr in the
buffer instead of writing to the parent process. Both modifiers
together let us drop the redundant shell-level `2>/dev/null || true`.

Five fire-and-forget callsites updated:
- session.created → bootstrap, agent --budget 4000
- session.idle → check-persistence, check-task-completion
- tool.execute.after (shell+git commit match) → post-commit
- tool.execute.after (edit/write) → check-task-completion

(experimental.session.compacting was already using .nothrow().quiet()
since commit 942304d — needed it for reading exitCode.)

Persisted as .context/LEARNINGS.md entry 2026-04-29-050000 so
this BunShell stdout-leak gotcha is documented for future plugin
work.

Spec: specs/opencode-integration.md

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

Signed-off-by: Omer Kocaoglu <omergk28@gmail.com>
- Write MCP config to ~/.config/opencode/opencode.json (global) instead
  of project-local opencode.json so non-interactive shells find it.
- Resolve ctx binary to absolute path via exec.LookPath at setup time.
- Remove omitempty from ToolContent.Text to satisfy OpenCode's Zod schema.
- Extract .config to DirXDGConfig constant to pass audit checks.

Signed-off-by: Omer Kocaoglu <omergk28@gmail.com>
Standalone getting-started page targeting OpenCode users with
before/after pitch, one-command setup, lifecycle hook reference,
slash commands, and MCP tools table. Added to Get Started nav.

Signed-off-by: omergk28 <omergk28@gmail.com>
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>

Signed-off-by: Omer Kocaoglu <omergk28@gmail.com>
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>

Signed-off-by: Omer Kocaoglu <omergk28@gmail.com>
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>

Signed-off-by: Omer Kocaoglu <omergk28@gmail.com>
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>

Signed-off-by: Omer Kocaoglu <omergk28@gmail.com>
omergk28 added 7 commits May 3, 2026 11:17
Spec: specs/opencode-integration.md
Signed-off-by: omergk28 <omergk28@gmail.com>
Spec: specs/opencode-integration.md
Signed-off-by: omergk28 <omergk28@gmail.com>
Spec: specs/opencode-integration.md
Signed-off-by: omergk28 <omergk28@gmail.com>
Spec: specs/opencode-integration.md
Signed-off-by: omergk28 <omergk28@gmail.com>
Spec: specs/opencode-integration.md
Signed-off-by: omergk28 <omergk28@gmail.com>
- Fix govet shadow: rename inner err to deployErr in agents_test.go
- Use computed globalConfigPath() in MCP warning instead of static constant
- Add troubleshooting section, compaction explanation, restart note,
  dangerous-command omission note, and global-config caveat to opencode.md
- Add OpenCode to multi-tool-setup.md, guide-your-agent.md, recipes/index.md

Signed-off-by: omergk28 <omergk28@gmail.com>
Add TestToolContentTextFieldAlwaysPresent so a future revert of the
omitempty drop on ToolContent.Text fails CI. The MCP spec requires
text present on type:"text" content; OpenCode's Zod validator enforces
it strictly. Verified live (PR ActiveMemory#72) against Claude Code and Copilot
CLI v1.0.40 — both accept the always-present empty-string form.

Signed-off-by: omergk28 <omergk28@gmail.com>
omergk28 added a commit to omergk28/ctx that referenced this pull request May 3, 2026
Persist learnings, decisions, conventions, and follow-up tasks from
the PR ActiveMemory#72 review and refinement pass.

Learnings:
- ctx system help can list project-local Claude wrappers that aren't
  real Go subcommands; non-Claude integrations only see the Go subset
- Trailing \b in a regex matches commit-tree as git commit; need (?!-)
- make test exit code unreliable due to -cover covdata tooling issue

Decisions:
- OpenCode plugin ships without tool.execute.before until
  block-dangerous-commands is a real ctx system Go subcommand
- Editor plugins must filter post-commit to actual git commit calls

Conventions:
- New editor integrations include an MCP-merge test covering the
  five canonical edge cases

Tasks (follow-up):
- Promote block-dangerous-commands to a Go subcommand
- Type-check embedded TS plugin assets in CI

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
omergk28 added a commit to omergk28/ctx that referenced this pull request May 3, 2026
…subdirectory)

OpenCode auto-loads only top-level .ts/.js files under
.opencode/plugins/; subdirectories are silently ignored. The
v0.7.x setup deployed the plugin to .opencode/plugins/ctx/index.ts,
so the entire OpenCode integration shipped in PR ActiveMemory#72 — the
session/idle hooks, the post-commit nudge, the check-task
-completion nudge — was never actually loaded by OpenCode. The
file was correct; OpenCode's discovery rule made it dead code.

Verified by smoke-testing both layouts side-by-side:
.opencode/plugins/ctx/index.ts produced no trace events even
with --print-logs --log-level DEBUG. .opencode/plugins/ctx.ts
loaded immediately, factory-call invoked, tool.execute.after
fired with the expected args shape.

Changes:

- internal/cli/setup/core/opencode/plugin.go now writes the
  embedded index.ts content to .opencode/plugins/ctx.ts (flat).
- New cfgHook.FileOpenCodePluginDeploy = "ctx.ts" constant.
  cfgHook.FileIndexTs is kept as the embedded-asset key (the
  source-of-truth filename in the binary) and its docstring now
  spells out the flat-vs-subdir discovery rule for future
  maintainers.
- Drop internal/assets/integrations/opencode/plugin/package.json
  and its //go:embed directive: the plugin uses a type-only
  import of @opencode-ai/plugin (erased at compile time) and the
  host runtime injects PluginInput, so there is no runtime
  dependency tree to install.
- New errSetup.MissingEmbeddedAsset() helper with a matching text
  key, so the new asset lookup uses the err package rather than a
  naked fmt.Errorf (audit fix).
- specs/opencode-integration.md updated to describe the flat
  layout and a smoke-step that verifies a hook actually fires.
- LEARNINGS.md captures the discovery so future plugins for any
  editor verify load before debugging hook contracts.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
omergk28 added a commit to omergk28/ctx that referenced this pull request May 3, 2026
Three "I wish I knew this earlier" gotchas surfaced while
fixing PR ActiveMemory#72. Persisting before they fade so the next
@opencode-ai/plugin bump (or anyone wiring a new editor
plugin) doesn't repeat them:

- event hook is a single dispatcher, not an object of named
  per-event handlers — asymmetric with neighboring named hooks
  in the same SDK
- multiple plugin hooks (shell.env, tool.execute.after,
  chat.params, chat.headers, ...) take (input, output) and
  mutate output; returned values are silently discarded
- shell.env env injection only reaches the agent's shell tool,
  not the plugin's own ctx.$ subprocess calls — those need a
  pre-configured BunShell built from ctx.directory

Spec: specs/opencode-integration.md

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
omergk28 added a commit to omergk28/ctx that referenced this pull request May 3, 2026
…ctx context across compaction

Smoke-testing PR ActiveMemory#72 with oh-my-openagent@3.17.6 installed
revealed that ctx context survives /compact only by accident:
oh-my-openagent's pre-compaction handler builds a structured
summary template that happens to preserve .context/-prefixed
file paths in its "Active Working Context → Files" section.
Combined with our shell.env CTX_DIR injection, the agent had
enough breadcrumbs to re-read DECISIONS.md from disk after a
test compaction — quoted line 65 verbatim.

That's a fragile property: depends on undocumented
serialization choices in another plugin. If oh-my-openagent
ever drops file-path preservation, swaps section names, or
condenses paths, the breadcrumbs disappear and ctx context
is lost without any signal.

Fix: register experimental.session.compacting in our plugin
and push `ctx system bootstrap` output to output.context. Per
the SDK contract, output.context is *additive* (appends to
the default compaction prompt), while output.prompt is
*destructive* (one plugin replaces another). Pushing to
context composes additively with primary compaction harnesses
like oh-my-openagent — neither plugin needs to know about the
other for the integration to work.

Verified: rebuilt binary embeds the new hook, lint clean
(0 issues), all tests pass. The deployed plugin in the
project's .opencode/ has been updated; relaunching OpenCode
will pick up the new hook on the next session start.

Also persisted as .context/LEARNINGS.md entry 2026-04-29-040000
so a future SDK or oh-my-openagent bump that breaks this
interop is easier to diagnose.

Spec: specs/opencode-integration.md

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
omergk28 added a commit to omergk28/ctx that referenced this pull request May 3, 2026
Add TestToolContentTextFieldAlwaysPresent so a future revert of the
omitempty drop on ToolContent.Text fails CI. The MCP spec requires
text present on type:"text" content; OpenCode's Zod validator enforces
it strictly. Verified live (PR ActiveMemory#72) against Claude Code and Copilot
CLI v1.0.40 — both accept the always-present empty-string form.

Signed-off-by: omergk28 <omergk28@gmail.com>
@omergk28 omergk28 force-pushed the feat/opencode-integration branch from ffe9793 to 5d88505 Compare May 3, 2026 15:18
omergk28 added 5 commits May 3, 2026 12:11
Two factual fixes in docs/home/opencode.md:

- "How Compaction Works" referenced `ctx agent --budget 4000`; the
  plugin actually runs `ctx system bootstrap` (index.ts:69). Updated
  the description to match the breadcrumb-mediated reality the spec
  already documents.
- "This is the only ctx integration that writes a file outside the
  project root" was wrong — the Copilot CLI integration writes to
  ~/.copilot/mcp-config.json for the same non-interactive-shell reason.
  Reworded to drop the uniqueness claim and reference the parallel.

Signed-off-by: omergk28 <omergk28@gmail.com>
Third-pass review fixes — docs and assets only, no behavior change.

- docs/home/opencode.md & docs/operations/integrations.md: rewrite the
  hook table/list so it matches what the plugin actually does. Previous
  text implied session.created/session.idle output was visible to the
  user; in fact those calls run with .nothrow().quiet() and produce no
  observable side effect. Compaction text now correctly describes the
  breadcrumb mechanism (push bootstrap output into output.context).
- skills/ctx-status/SKILL.md: drop "/ctx-status --verbose" and
  "/ctx-status --json" examples — OpenCode slash commands don't pass
  args to the underlying CLI, so these taught a non-existent
  invocation form. Replaced with a note pointing the agent at
  "ctx status --verbose" / "--json" directly.
- plugin/index.ts: add a comment on extractCommand documenting the
  silent-no-op behavior if a future SDK bump sends `command` as an
  array instead of string.
- specs/opencode-integration.md: drop the bogus PluginPathOpenCode
  constant reference (constant doesn't exist; deploy path is composed
  from cfgHook constants at the call site) and update the package
  file inventory to include validate.go + test files.

Signed-off-by: omergk28 <omergk28@gmail.com>
- opencode.go: compose the skill warning path inline from
  cfgHook.DirOpenCode + cfgHook.DirOpenCodeSkills, matching how
  skill.go composes the actual deploy path. Eliminates the duplicate
  cfgSetup.SkillsPathOpenCode definition that could drift from the
  real path.
- config/setup/setup.go: drop SkillsPathOpenCode (now unused);
  document MCPConfigPathOpenCode as a fallback-only display string
  for warnings when globalConfigPath() can't resolve.
- skill.go: iterate skills in sorted order so partial-failure
  filesystem state is deterministic and tests that plant blocking
  files at a specific skill path observe stable behavior.

Signed-off-by: omergk28 <omergk28@gmail.com>
Adds an atomic same-directory temp + fsync + rename helper and
applies it to the two integrations that write MCP config files
outside the project root (opencode, copilot_cli). Without this, a
crash mid-write or two concurrent setup invocations could truncate
the host tool's config and silently wipe every other registered
MCP server on the next run.

Also wraps raw stdlib errors in opencode/mcp.go through the
existing errFs/errSetup constructors per CONVENTIONS.md, and adds
a test for the LookPath-success branch in launchCommand that the
existing QuotesBinaryPath test deliberately skips.

- internal/io/security.go: SafeWriteFileAtomic (write-temp + sync
  + close + chmod + rename, with cleanup on every failure path).
- internal/io/security_test.go: cover create / overwrite / temp
  cleanup / perm application.
- internal/config/file/name.go: TempSuffixPattern constant for
  the os.CreateTemp pattern suffix.
- internal/cli/setup/core/opencode/mcp.go: route raw errors
  through errFs.FileRead/FileWrite/Mkdir + errSetup.MarshalConfig;
  use SafeWriteFileAtomic for the merged config write.
- internal/cli/setup/core/copilot_cli/mcp.go: use
  SafeWriteFileAtomic for the same exposure.
- internal/cli/setup/core/opencode/mcp_test.go: add
  TestEnsureMCPConfig_ResolvesBinaryToAbsolutePath, which seeds
  a fake `ctx` binary on PATH so the LookPath success path is
  actually exercised.

Signed-off-by: omergk28 <omergk28@gmail.com>
Adds OpenCode to docs/cli/setup.md — the canonical user-facing
reference page that lists every tool ctx setup supports — and
includes a corresponding example in the examples block.

Closes the last documentation gap from PR ActiveMemory#72 review: hooks.yaml
gained the hook.opencode entry but the generated reference page
that surfaces tool support to users wasn't updated alongside it.

Signed-off-by: omergk28 <omergk28@gmail.com>
omergk28 added a commit to omergk28/ctx that referenced this pull request May 3, 2026
Adds OpenCode to docs/cli/setup.md — the canonical user-facing
reference page that lists every tool ctx setup supports — and
includes a corresponding example in the examples block.

Closes the last documentation gap from PR ActiveMemory#72 review: hooks.yaml
gained the hook.opencode entry but the generated reference page
that surfaces tool support to users wasn't updated alongside it.

Signed-off-by: omergk28 <omergk28@gmail.com>
…ration

Signed-off-by: omergk28 <omergk28@gmail.com>

# Conflicts:
#	.context/CONVENTIONS.md
#	.context/DECISIONS.md
#	.context/LEARNINGS.md
#	.context/TASKS.md
omergk28 added a commit to omergk28/ctx that referenced this pull request May 4, 2026
Add TestToolContentTextFieldAlwaysPresent so a future revert of the
omitempty drop on ToolContent.Text fails CI. The MCP spec requires
text present on type:"text" content; OpenCode's Zod validator enforces
it strictly. Verified live (PR ActiveMemory#72) against Claude Code and Copilot
CLI v1.0.40 — both accept the always-present empty-string form.

Signed-off-by: omergk28 <omergk28@gmail.com>
omergk28 added a commit to omergk28/ctx that referenced this pull request May 4, 2026
Adds OpenCode to docs/cli/setup.md — the canonical user-facing
reference page that lists every tool ctx setup supports — and
includes a corresponding example in the examples block.

Closes the last documentation gap from PR ActiveMemory#72 review: hooks.yaml
gained the hook.opencode entry but the generated reference page
that surfaces tool support to users wasn't updated alongside it.

Signed-off-by: omergk28 <omergk28@gmail.com>
@omergk28 omergk28 force-pushed the feat/opencode-integration branch 2 times, most recently from 46f255a to bca34f9 Compare May 4, 2026 19:58
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.

2 participants