Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
1bac499
feat: add OpenCode integration with plugin, MCP, and skills
omergk28 Apr 26, 2026
c347675
fix(opencode): drop broken dangerous-command hook, narrow post-commit…
omergk28 Apr 26, 2026
257fd65
context: capture OpenCode PR review session
omergk28 Apr 26, 2026
6ee50fd
fix(opencode): destructure args (not input) in tool.execute.after
omergk28 Apr 26, 2026
41881a0
fix(opencode): emit OpenCode-compatible MCP server shape
omergk28 Apr 26, 2026
51a6f07
fix(opencode): deploy plugin as flat .opencode/plugins/ctx.ts (not a …
omergk28 Apr 27, 2026
dc72898
fix(opencode): wrap MCP launch in sh -c so CTX_DIR resolves to $PWD/.…
omergk28 Apr 27, 2026
4a61b30
context: record block-dangerous-commands skip decision
omergk28 Apr 27, 2026
c21648d
fix(opencode): correct plugin SDK contract and stale messaging
omergk28 Apr 29, 2026
db9a7f2
context: capture three OpenCode plugin SDK contract learnings
omergk28 Apr 29, 2026
8dec26d
feat(opencode): add experimental.session.compacting hook to preserve …
omergk28 Apr 29, 2026
74730d0
fix(opencode): suppress BunShell stdout to prevent ctx output leaking…
omergk28 Apr 29, 2026
b462297
fix(opencode): use global config path, absolute binary, drop omitempty
omergk28 Apr 30, 2026
fe2bb85
docs: add ctx for OpenCode quickstart guide
omergk28 May 1, 2026
fe3334b
fix(opencode): harden shared AGENTS deployment
omergk28 May 2, 2026
08dd432
refactor(opencode): reuse shared AGENTS deployer
omergk28 May 2, 2026
c3cd554
docs(opencode): document global MCP config and hook summary
omergk28 May 2, 2026
95c2dd9
docs(opencode): align package docs and spec with shared AGENTS flow
omergk28 May 2, 2026
c7601f8
docs(opencode): clarify background bootstrap behavior
omergk28 May 2, 2026
1e213c7
docs(opencode): align setup help with refresh semantics
omergk28 May 2, 2026
97f0ae9
fix(opencode): improve ctx-remember skill guidance
omergk28 May 2, 2026
1b2e984
fix(opencode): harden MCP config refresh
omergk28 May 2, 2026
5c1f511
fix(opencode): refresh stale managed assets
omergk28 May 2, 2026
66ba91a
fix(opencode): address PR review — lint shadow, warning path, docs
omergk28 May 3, 2026
e3b6f9b
test(mcp): lock ToolContent.Text wire format with always-present key
omergk28 May 3, 2026
7ad1781
docs(opencode): correct compaction command and uniqueness claim
omergk28 May 3, 2026
ada96fb
docs(opencode): correct hook visibility, slash-command, spec drift
omergk28 May 3, 2026
823fc1d
refactor(opencode): dedup skills path, sort skill iteration
omergk28 May 3, 2026
b518636
feat(io): add SafeWriteFileAtomic; harden MCP config writes
omergk28 May 3, 2026
9cafdba
docs(setup): list opencode in the supported-tools reference
omergk28 May 3, 2026
bca34f9
Merge remote-tracking branch 'upstream/main' into feat/opencode-integ…
omergk28 May 4, 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
2 changes: 2 additions & 0 deletions .context/CONVENTIONS.md
Original file line number Diff line number Diff line change
Expand Up @@ -177,6 +177,8 @@ DO NOT UPDATE FOR:
added, renamed, or removed, and the filesystem is self-documenting
- **Copyright headers**: All source files get the project copyright header

- New editor integrations include an MCP-merge test covering: create / empty file / preserve existing keys / skip when registered / reject malformed JSON

## Blog Publishing

- **Checklist for ideas/ → docs/blog/ promotion**:
Expand Down
46 changes: 45 additions & 1 deletion .context/DECISIONS.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,10 @@

<!-- INDEX:START -->
| Date | Decision |
|----|--------|
|------|--------|
| 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-16 | Deprecate and remove ctx backup |
| 2026-04-14 | doc.go quality floor: behavior-grounded, ~25-100 body lines, related-packages section required |
| 2026-04-14 | Bootstrap stays under ctx system bootstrap (reverted experimental top-level promotion) |
Expand Down Expand Up @@ -127,6 +130,47 @@ For significant decisions:
✗ No real alternatives existed

-->
## [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.

## [2026-04-16-011520] Deprecate and remove ctx backup

Expand Down
110 changes: 109 additions & 1 deletion .context/LEARNINGS.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,17 @@ DO NOT UPDATE FOR:

<!-- INDEX:START -->
| Date | Learning |
|----|--------|
|------|--------|
| 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-14 | Constitution forbids context window as a deferral excuse |
| 2026-04-14 | docs/cli/system.md and embed/cmd/system.go diverged on bootstrap promotion intent |
| 2026-04-14 | Raft-lite trade-off is the load-bearing choice in internal/hub |
Expand Down Expand Up @@ -121,6 +131,104 @@ DO NOT UPDATE FOR:
| 2026-04-25 | Parallel go test ./... packages can race on ~/.claude/settings.json |
<!-- INDEX:END -->

<!-- Add gotchas, tips, and lessons learned here -->
## [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.
---

## [2026-04-14-010134] Constitution forbids context window as a deferral excuse
Expand Down
8 changes: 8 additions & 0 deletions .context/TASKS.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,14 @@ TASK STATUS LABELS:
`#in-progress`: currently being worked on (add inline, don't move task)
-->

### Phase 1: [Name] `#priority:high`
- [ ] 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)

- [ ] Task 1
- [ ] Task 2

### Misc

- [x] If context is not initialized, hooks should not run. Right now they run
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