Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
dad48ea
feat: add OpenCode integration with plugin, MCP, and skills
omergk28 Apr 26, 2026
18344f9
fix(opencode): drop broken dangerous-command hook, narrow post-commit…
omergk28 Apr 26, 2026
e8e1373
context: capture OpenCode PR review session
omergk28 Apr 26, 2026
69b98b7
fix(opencode): destructure args (not input) in tool.execute.after
omergk28 Apr 26, 2026
b7d551d
fix(opencode): emit OpenCode-compatible MCP server shape
omergk28 Apr 26, 2026
4ce7d9f
fix(opencode): deploy plugin as flat .opencode/plugins/ctx.ts (not a …
omergk28 Apr 27, 2026
622851a
fix(opencode): wrap MCP launch in sh -c so CTX_DIR resolves to $PWD/.…
omergk28 Apr 27, 2026
1a2c62b
context: record block-dangerous-commands skip decision
omergk28 Apr 27, 2026
0690d13
fix(opencode): correct plugin SDK contract and stale messaging
omergk28 Apr 29, 2026
fd17b84
context: capture three OpenCode plugin SDK contract learnings
omergk28 Apr 29, 2026
10699ef
feat(opencode): add experimental.session.compacting hook to preserve …
omergk28 Apr 29, 2026
3b3989c
fix(opencode): suppress BunShell stdout to prevent ctx output leaking…
omergk28 Apr 29, 2026
bbd8509
fix(opencode): use global config path, absolute binary, drop omitempty
omergk28 Apr 30, 2026
af7da2c
docs: add ctx for OpenCode quickstart guide
omergk28 May 1, 2026
10ebabd
fix(opencode): harden shared AGENTS deployment
omergk28 May 2, 2026
c50b41a
refactor(opencode): reuse shared AGENTS deployer
omergk28 May 2, 2026
0927eb2
docs(opencode): document global MCP config and hook summary
omergk28 May 2, 2026
9ad567f
docs(opencode): align package docs and spec with shared AGENTS flow
omergk28 May 2, 2026
e57c950
docs(opencode): clarify background bootstrap behavior
omergk28 May 2, 2026
dfadf6b
docs(opencode): align setup help with refresh semantics
omergk28 May 2, 2026
23bb1d4
fix(opencode): improve ctx-remember skill guidance
omergk28 May 2, 2026
4634c27
fix(opencode): harden MCP config refresh
omergk28 May 2, 2026
a6d0d87
fix(opencode): refresh stale managed assets
omergk28 May 2, 2026
8562a58
fix(opencode): address PR review — lint shadow, warning path, docs
omergk28 May 3, 2026
5735f87
test(mcp): lock ToolContent.Text wire format with always-present key
omergk28 May 3, 2026
1811139
docs(opencode): correct compaction command and uniqueness claim
omergk28 May 3, 2026
a5b96c7
docs(opencode): correct hook visibility, slash-command, spec drift
omergk28 May 3, 2026
c58f95d
refactor(opencode): dedup skills path, sort skill iteration
omergk28 May 3, 2026
e3f649f
feat(io): add SafeWriteFileAtomic; harden MCP config writes
omergk28 May 3, 2026
7c6403f
docs(setup): list opencode in the supported-tools reference
omergk28 May 3, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .context/CONVENTIONS.md
Original file line number Diff line number Diff line change
Expand Up @@ -270,3 +270,4 @@ DO NOT UPDATE FOR:
capitalization would otherwise apply: write `CONSTITUTION.md`, not
CONSTITUTION.Md. The title-case engine refuses to capitalize lowercase tokens
following a literal . dot, but explicit backticks remain the clearest signal.
- New editor integrations include an MCP-merge test covering: create / empty file / preserve existing keys / skip when registered / reject malformed JSON
44 changes: 44 additions & 0 deletions .context/DECISIONS.md
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,9 @@
| 2026-02-26 | Agent autonomy and separation of concerns (consolidated) |
| 2026-02-26 | Security and permissions (consolidated) |
| 2026-02-27 | Webhook and notification design (consolidated) |
| 2026-04-26 | OpenCode tool.execute.before omission is permanent; block-dangerous-commands will not become a ctx Go subcommand |
| 2026-04-26 | Editor-integration plugins must filter post-commit to actual git commit invocations |
| 2026-04-26 | OpenCode plugin ships without tool.execute.before hook |
| 2026-04-25 | Use t.Setenv for subprocess env in tests, not append(os.Environ(), ...) |
| 2026-04-25 | Tighten state.Dir / rc.ContextDir to (string, error) with sentinel errors |
<!-- INDEX:END -->
Expand Down Expand Up @@ -1891,6 +1894,47 @@ Filename Format, Two-Tier Persistence Model). Users who want session history use

*Module-specific, already-shipped, and historical decisions:
[decisions-reference.md](decisions-reference.md)*
## [2026-04-26-231517] OpenCode tool.execute.before omission is permanent; block-dangerous-commands will not become a ctx Go subcommand

**Status**: Accepted

**Context**: The 2026-04-26-152858 decision shipped the OpenCode plugin without a tool.execute.before hook and noted "Re-add when block-dangerous-commands is promoted to the ctx Go binary." Revisited: that promotion is no longer planned. Keeping the open task on the books makes future sessions believe a re-add is pending.

**Decision**: We will not promote block-dangerous-commands to a ctx system Go subcommand. The OpenCode plugin's missing tool.execute.before hook is permanent, not deferred.

**Rationale**: The Cobra exit-1 / `{ blocked: true }` interaction makes any shim hostile to users without the Claude wrapper, and the safety-hook gap is acceptable given OpenCode's positioning. Recording this avoids the tax of a perpetually-pending follow-up that no one intends to land.

**Consequences**: TASKS.md item "Promote 'block-dangerous-commands' to a real ctx system Go subcommand…" marked `[-]` skipped. The 2026-04-26-152858 rationale's "Re-add when…" clause is void; the underlying ship-without-the-hook decision remains in force. Other (non-OpenCode) editor integrations that want a dangerous-command safety net will need a different mechanism.

**Related**: Amends [2026-04-26-152858] OpenCode plugin ships without tool.execute.before hook (rationale's deferred re-add is now closed).

---

## [2026-04-26-152905] Editor-integration plugins must filter post-commit to actual git commit invocations

**Status**: Accepted

**Context**: Original PR #72 OpenCode plugin ran 'ctx system post-commit' after every shell tool call, not only after real commits

**Decision**: Editor-integration plugins must filter post-commit to actual git commit invocations

**Rationale**: post-commit is meaningful only after a real commit lands; firing on every shell call is noise that trains users to ignore the resulting nudges

**Consequences**: Editor plugins always sniff the actual command string (regex on the extracted command) before triggering capture nudges that target specific commands. Same pattern applies to any future hook that targets a specific porcelain command.

---

## [2026-04-26-152858] OpenCode plugin ships without tool.execute.before hook

**Status**: Accepted

**Context**: The natural fit (block-dangerous-commands) doesn't exist as a ctx system Go subcommand; shimming to it would block every shell call on installs without the Claude wrapper because Cobra's unknown-command exit 1 is read as { blocked: true } by OpenCode

**Decision**: OpenCode plugin ships without tool.execute.before hook

**Rationale**: Better to ship a feature-narrower plugin than one that bricks the editor for users without the wrapper. Re-add when block-dangerous-commands is promoted to the ctx Go binary.

**Consequences**: OpenCode users get bootstrap, persistence, post-commit, and task-completion nudges but no dangerous-command safety net. specs/opencode-integration.md records the deliberate omission.

---

Expand Down
107 changes: 107 additions & 0 deletions .context/LEARNINGS.md
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,16 @@ DO NOT UPDATE FOR:
| 2026-02-22 | Permission and settings drift (consolidated) |
| 2026-02-22 | Gitignore and filesystem hygiene (consolidated) |
| 2026-01-28 | IDE is already the UI |
| 2026-04-29 | BunShell ctx.$ calls echo stdout to OpenCode's process unless .quiet() is set — leaks visible noise |
| 2026-04-29 | OpenCode plugin compaction interop is breadcrumb-mediated: own your context preservation explicitly |
| 2026-04-29 | @opencode-ai/plugin event hook is a single dispatcher, not an object of named handlers |
| 2026-04-29 | OpenCode plugin hooks like shell.env take (input, output) and mutate; returned objects are ignored |
| 2026-04-29 | OpenCode shell.env injects env only into agent's shell tool, not into plugin's own ctx.$ calls |
| 2026-04-26 | OpenCode auto-loads only flat .ts files under .opencode/plugins/; subdirectories are ignored |
| 2026-04-26 | OpenCode opencode.json MCP shape: command is Array<string>, no separate args field |
| 2026-04-26 | make test exit code unreliable due to -cover covdata tooling issue |
| 2026-04-26 | Trailing word boundary in regex matches commit-tree as git commit |
| 2026-04-26 | ctx system help can list project-local hooks not in the Go binary |
| 2026-04-25 | Confident code comments can pull an LLM away from first-principles knowledge |
| 2026-04-25 | filepath.Join('', rel) returns rel as CWD-relative, not error |
| 2026-04-25 | Parallel go test ./... packages can race on ~/.claude/settings.json |
Expand Down Expand Up @@ -1734,6 +1744,103 @@ git integration, extensions - all free.

*Module-specific, niche, and historical learnings:
[learnings-reference.md](learnings-reference.md)*
## [2026-04-29-050000] BunShell ctx.$ calls echo stdout to OpenCode's process unless .quiet() is set — leaks visible noise

**Context**: After PR #72 wired session.created and session.idle to fire `ctx system bootstrap`, `ctx agent --budget 4000`, and friends, end users started seeing chunks of Markdown bleeding into the OpenCode TUI: `## Steering`, `# Product Context`, `Describe the product...`. These are the contents of `.context/steering/` template stubs that `ctx agent --budget 4000` includes in its context packet. The plugin used the shell-level `2>/dev/null || true` to swallow stderr and force exit 0, but stdout was untouched.

**Lesson**: BunShell's documented behavior: *"By default, the shell will write to the current process's stdout and stderr, as well as buffering that output."* So an `await ctx.$\`...\`` call in a plugin echoes its stdout/stderr to OpenCode's process, which the TUI/agent surfaces. Shell-level `2>/dev/null` only suppresses stderr; stdout still leaks. The fix is BunShell's `.quiet()` modifier on the BunShellPromise, which configures the shell to only buffer the output rather than also writing to the parent process.

**Application**: Always chain `.nothrow().quiet()` on BunShell template literals in OpenCode plugins, even for fire-and-forget calls where you discard the result: `await ctx.$\`ctx system bootstrap\`.nothrow().quiet()`. With both modifiers, you don't need shell-level `2>/dev/null || true` — `.nothrow()` swallows non-zero exits at the BunShell layer, `.quiet()` keeps every byte of output buffered. Pattern is the cooperative default for any plugin that spawns long-output commands during the agent session lifecycle.

---

## [2026-04-29-040000] OpenCode plugin compaction interop is breadcrumb-mediated: own your context preservation explicitly

**Context**: After PR #72 wired `session.created` / `session.idle` / `tool.execute.after` / `shell.env`, a `/compact` test in OpenCode (with `oh-my-openagent@3.17.6` also installed) recovered ctx context post-compaction *only by accident*: oh-my-openagent's `experimental.session.compacting` 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 post-compaction. Without that section, our context would have evaporated silently into the compaction summary.

**Lesson**: Two compaction-aware plugins in the same session can synergize without either knowing about the other — but the synergy is fragile because it depends on undocumented serialization choices in the *other* plugin. If the other plugin's template ever changes (e.g., drops file-path preservation, swaps the "Active Working Context" section name, condenses paths to basenames), the breadcrumbs disappear and ctx context is lost without any signal. The `Hooks` interface in `@opencode-ai/plugin` v1.4.x exposes `experimental.session.compacting?: (input, output: { context: string[]; prompt?: string }) => Promise<void>` — pushing to `output.context` is *additive* (appends to the default prompt), and replacing `output.prompt` is *destructive* (only one plugin can win that race).

**Application**: Register `experimental.session.compacting` in your own plugin and push high-signal context strings (e.g., `ctx system bootstrap` output) to `output.context` so context preservation does not depend on coexisting plugins. Never set `output.prompt` from a thin shim — that would conflict with primary compaction harnesses like oh-my-openagent. Composition via `output.context` is the correct cooperative pattern.

---

## [2026-04-29-030000] @opencode-ai/plugin event hook is a single dispatcher, not an object of named handlers

**Context**: PR #72's first OpenCode plugin shipped with `event: { "session.created": fn, "session.idle": fn }` — an object keyed by event type. It compiled clean against `satisfies Plugin` but never fired. End-to-end trace showed neighboring hooks (`shell.env`, `tool.execute.after`) running while every event handler silently no-op'd.

**Lesson**: `@opencode-ai/plugin` v1.4.x defines `event?: (input: { event: Event }) => Promise<void>` — one dispatcher called for every event with `input.event.type` discriminating. Asymmetric with neighbors because `shell.env` and `tool.execute.*` *are* top-level named keys; only the dozens of `EventX` types collapse into the single `event` slot.

**Application**: Use `event: async ({event}) => { if (event.type === "session.created") { ... } else if (event.type === "session.idle") { ... } }`. Type discriminator strings live under each `EventX` type in `node_modules/@opencode-ai/sdk/dist/gen/types.gen.d.ts`.

---

## [2026-04-29-030100] OpenCode plugin hooks like shell.env take (input, output) and mutate; returned objects are ignored

**Context**: First plugin had `"shell.env": () => ({ CTX_DIR: ".context" })`. The hook fired but the agent's bash tool never saw `CTX_DIR`; manual export was required for every ctx call. The returned object was dropped on the floor by the runtime.

**Lesson**: Multiple hooks in `@opencode-ai/plugin` v1.4.x take two arguments where the second is an OUT param. Examples: `shell.env: (input, output: {env}) => void` (mutate `output.env`), `tool.execute.after: (input, output: {title, output, metadata}) => void`, `chat.params: (input, output: {temperature, ...}) => void`, `chat.headers: (input, output: {headers}) => void`. Pattern is consistent across the SDK.

**Application**: Always read the type definition in `node_modules/@opencode-ai/plugin/dist/index.d.ts` for any hook before wiring. If a hook signature has two parameters where the second is an object, it's a mutation hook — return values are discarded.

---

## [2026-04-29-030200] OpenCode shell.env injects env only into agent's shell tool, not into plugin's own ctx.$ calls

**Context**: After fixing `shell.env`'s `(input, output) => mutate output.env` signature so `CTX_DIR` reached the agent's bash tool, the plugin's own `ctx.$\`ctx system bootstrap\`` calls still failed silently — they ran without `CTX_DIR` and ctx fell back to `~/.context`. The hook fired correctly; the plugin's subprocess side-effects didn't see the env.

**Lesson**: `shell.env` injects env into the agent's shell-tool invocations. The plugin's own BunShell calls (`ctx.$\`...\``) inherit OpenCode's process env, which is *separate*. Two shells, two envs.

**Application**: Build an env-aware BunShell once in the plugin factory: `const $ = ctx.$.env({ ...process.env, CTX_DIR: \`${ctx.directory}/.context\` })`. Reuse it for every plugin-initiated subprocess call. `ctx.directory` is the project root from `PluginInput`.

---

## [2026-04-26-180000] OpenCode auto-loads only flat .ts files under .opencode/plugins/; subdirectories are ignored

**Context**: Initial OpenCode integration deployed the plugin as `.opencode/plugins/ctx/index.ts` (a directory with index.ts inside, mirroring npm package conventions). End-to-end smoke testing showed the plugin file was present and the binary was current, yet OpenCode never invoked any of the plugin's hooks (no `module-load` trace fired even with `--print-logs --log-level DEBUG`). Copying the same content to a flat `.opencode/plugins/ctx.ts` file made the plugin load and fire correctly.

**Lesson**: OpenCode's plugin auto-discovery only scans top-level files under `.opencode/plugins/` and `~/.config/opencode/plugins/`. Subdirectories are silently skipped — there is no log line indicating a subdirectory was found and ignored. The official docs at opencode.ai/docs/plugins/ say only "files in these directories are automatically loaded at startup" without specifying the rule, so this is easy to miss. The `opencode plugin <module>` CLI registers npm modules (a different code path) and accepts only npm names, not local paths.

**Application**: Deploy single-file plugins as `.opencode/plugins/<name>.ts`, not `.opencode/plugins/<name>/index.ts`. No `package.json` is required when the plugin uses type-only imports (`import type` is erased at compile time) and the host runtime injects the plugin context. To verify a plugin is actually loaded, add a top-of-module side effect (e.g. `appendFileSync` to a known path) and confirm it fires before debugging hook contracts.

---

## [2026-04-26-165500] OpenCode opencode.json MCP shape: command is Array<string>, no separate args field

**Context**: `ctx setup opencode --write` was generating `opencode.json` with the Copilot CLI MCP shape (`{type: "local", command: "ctx", args: ["mcp", "serve"]}`). OpenCode rejected the file at startup with `Configuration is invalid… Expected array, got "ctx" mcp.ctx.command` and `Missing key mcp.ctx.enabled`.

**Lesson**: OpenCode's `McpLocalConfig` (in `@opencode-ai/sdk`) defines `command: Array<string>` as a single field that holds the binary AND its arguments — there is no separate `args` field. It also requires `enabled: boolean` at runtime even though the TS type marks it optional. The Copilot CLI MCP shape is similar in spirit but structurally different; do not copy-paste between them.

**Application**: For OpenCode MCP entries always use `command: ["ctx", "mcp", "serve"]` and include `enabled: true`. If you add a new editor integration with its own MCP file format, read the upstream type definitions from `node_modules/@<vendor>/sdk/dist/gen/types.gen.d.ts` (or equivalent) before reusing an existing generator.

---

## [2026-04-26-152850] make test exit code unreliable due to -cover covdata tooling issue

**Context**: make test exited 1 even with all 123 packages passing on this Go install; root cause is missing covdata tool when -cover is enabled

**Lesson**: Don't trust make test exit code alone when verifying changes. The -cover flag in the test target can fail with 'no such tool covdata' even when every package passes.

**Application**: When make test fails, fall back to 'go test ./...' (no -cover) and tally ^ok / ^FAIL counts to distinguish real failures from tooling issues.

---

## [2026-04-26-152842] Trailing word boundary in regex matches commit-tree as git commit

**Context**: First post-commit filter regex \bgit\s+commit\b in the OpenCode plugin would have triggered on git commit-tree because \b matches between t and -

**Lesson**: A trailing word boundary doesn't exclude hyphenated continuations — \b matches every word/non-word transition. Use (?!-) negative lookahead to specifically reject hyphen-suffixed siblings.

**Application**: For any porcelain with hyphenated cousins (commit-tree, commit-graph, for-each-ref), append (?!-) to the boundary.

---

## [2026-04-26-152836] ctx system help can list project-local hooks not in the Go binary

**Context**: PR #72 plugin called 'ctx system block-dangerous-commands'; user's installed ctx 0.7.2 listed it in help, but no directory exists under internal/cli/system/cmd/ — it's a Claude Code plugin-local hook surfaced via wrapper

**Lesson**: ctx system help output is a union of compiled Go subcommands and project-local Claude wrappers; non-Claude integrations only see the Go subset

**Application**: When porting plugin behavior to a new editor, only call subcommands that have a directory under internal/cli/system/cmd/. Don't trust ctx system help output as the canonical surface.

---

Expand Down
4 changes: 4 additions & 0 deletions .context/TASKS.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,10 @@ TASK STATUS LABELS:
-->

## Phase 0 Grounding
- [ ] Add TypeScript type-check step (bunx tsc --noEmit) for embedded editor-plugin assets to CI; nothing currently checks .opencode/plugins/ctx/index.ts before embedding #priority:low #added:2026-04-26-152912

- [-] Promote 'block-dangerous-commands' to a real ctx system Go subcommand so OpenCode and other non-Claude editor integrations can ship the safety hook #priority:medium #added:2026-04-26-152911 #skipped:2026-04-26-231517 reason: decided not to do — OpenCode's exit-code semantics make a Cobra-based block-command shim too risky, and the safety-net omission in OpenCode is now treated as permanent (see decision 2026-04-26-231517)


- [ ] The target project (to be given to the Agent) has a good "phasing"
mechanism for tasks; implement that; maybe `ctx task add` can have a
Expand Down
4 changes: 4 additions & 0 deletions docs/cli/setup.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ ctx setup <tool> [flags]
| `cline` | Cline (VS Code extension) |
| `aider` | Aider CLI |
| `copilot` | GitHub Copilot |
| `opencode` | OpenCode (terminal-first AI coding agent) |
| `windsurf` | Windsurf IDE |

!!! note "Claude Code Uses the Plugin System"
Expand All @@ -55,4 +56,7 @@ ctx setup copilot --write
ctx setup kiro --write
ctx setup cursor --write
ctx setup cline --write

# Generate OpenCode plugin, skills, AGENTS.md, and global MCP config
ctx setup opencode --write
```
Loading
Loading