feat: add OpenCode integration with plugin, MCP, and skills#72
feat: add OpenCode integration with plugin, MCP, and skills#72omergk28 wants to merge 31 commits intoActiveMemory:mainfrom
Conversation
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>
…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>
feee82e to
8a15bd2
Compare
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>
…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>
|
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 Below is what I found. One blocker, the rest are non-blocking concerns BlockerLint failure breaks
|
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.
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>
f17cab6 to
ffe9793
Compare
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>
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>
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>
…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>
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>
…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>
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>
ffe9793 to
5d88505
Compare
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>
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
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>
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>
46f255a to
bca34f9
Compare
Summary
Key changes
ctx-rememberguidance and keep OpenCode docs/spec/help consistent with implemented behaviorWire-format change (cross-cutting)
internal/mcp/proto/schema.go:ToolContent.Textdropsomitempty. Everytools/callresponse now emits"text": ""for empty content instead of omitting the key. The MCP specdefines
textas required ontype:"text"content, so this is the spec-compliant form; OpenCode's Zod validator enforces it strictly, while Claude Code and Copilot CLI tolerateeither shape.
Verified live:
ctx mcp serve(this branch) registered against both Claude Code and Copilot CLI v1.0.40 —tools/call ctx_statussucceeded end-to-end on both. Locked down byTestToolContentTextFieldAlwaysPresentininternal/mcp/proto/schema_test.goso a future revert toomitemptyfails CI.Validation
Automated validation
go test ./internal/cli/setup/core/opencodego test ./internal/cli/setup/core/agentsgo test ./internal/mcp/protomake buildgo test ./...Manual smoke test
Validated the OpenCode +
ctxintegration end to end in a fresh dummy project using an isolated OpenCode home.Confirmed:
ctx setup opencode --writeinstalled the plugin, skills, AGENTS integration, and global MCP config/ctx-status,/ctx-agent,/ctx-remember,/ctx-wrap-upshell.envinjected the correctCTX_DIRgit commitflow worked