diff --git a/.opencode/INSTALL.md b/.opencode/INSTALL.md index 080f043f8f..611cb101a4 100644 --- a/.opencode/INSTALL.md +++ b/.opencode/INSTALL.md @@ -1,16 +1,22 @@ # Installing Superpowers for OpenCode +> **OpenCode v2 only.** This plugin uses the OpenCode v2 plugin API. OpenCode +> v1 plugins do not run under v2 — use an older Superpowers release for +> OpenCode v1. + ## Prerequisites -- [OpenCode.ai](https://opencode.ai) installed +- [OpenCode v2](https://v2.opencode.ai) installed ## Installation -Add superpowers to the `plugin` array in your `opencode.json` (global or project-level): +Add superpowers to the `plugins` array in your `opencode.json` (global or +project-level). `plugins` is the canonical v2 key; the legacy singular `plugin` +key still loads for now but may be removed: ```json { - "plugin": ["superpowers@git+https://github.com/obra/superpowers.git"] + "plugins": ["superpowers@git+https://github.com/obra/superpowers.git"] } ``` @@ -39,6 +45,12 @@ rm -rf ~/.config/opencode/superpowers Then follow the installation steps above. +## Migrating from OpenCode v1 + +If your `opencode.json` used the singular `plugin` key, rename it to `plugins`. +No other changes are needed — the same git package spec works, and the plugin +now loads through the OpenCode v2 API automatically. + ## Usage Use OpenCode's native `skill` tool: @@ -59,7 +71,7 @@ To pin a specific version: ```json { - "plugin": ["superpowers@git+https://github.com/obra/superpowers.git#v5.0.3"] + "plugins": ["superpowers@git+https://github.com/obra/superpowers.git#v5.0.3"] } ``` @@ -67,9 +79,10 @@ To pin a specific version: ### Plugin not loading -1. Check logs: `opencode run --print-logs "hello" 2>&1 | grep -i superpowers` -2. Verify the plugin line in your `opencode.json` -3. Make sure you're running a recent version of OpenCode +1. Check logs: `grep -i "loading plugin" ~/.local/share/opencode/log/opencode.log` (OpenCode v2 logs to a file; the CLI `run` command no longer has a `--print-logs` flag) +2. Verify the plugin line in your `opencode.json` uses the `plugins` key +3. If the shared background service is stuck, restart it: `opencode service restart` +4. Make sure you're running OpenCode v2 ### Windows install issues @@ -87,7 +100,7 @@ Then use the installed package path in `opencode.json`: ```json { - "plugin": ["~/.config/opencode/node_modules/superpowers"] + "plugins": ["~/.config/opencode/node_modules/superpowers"] } ``` @@ -98,13 +111,13 @@ Then use the installed package path in `opencode.json`: ### Tool mapping -Skills speak in actions ("create a todo", "dispatch a subagent", "read a file"). On OpenCode these resolve to: +Skills speak in actions ("create a todo", "dispatch a subagent", "read a file"). On OpenCode v2 these resolve to: - "Create a todo" / "mark complete in todo list" → `todowrite` - `Subagent (general-purpose):` template → `task` tool with `subagent_type: "general"` (or `"explore"` for codebase exploration) - "Invoke a skill" → OpenCode's native `skill` tool - "Read a file" → `read` -- "Create a file" / "edit a file" / "delete a file" → `apply_patch` +- "Create a file" → `write`; "edit a file" → `edit` - "Run a shell command" → `bash` - "Search file contents" / "find files by name" → `grep`, `glob` - "Fetch a URL" → `webfetch` diff --git a/.opencode/plugins/superpowers.js b/.opencode/plugins/superpowers.js index 423e5ed513..d6c80349ba 100644 --- a/.opencode/plugins/superpowers.js +++ b/.opencode/plugins/superpowers.js @@ -1,17 +1,29 @@ /** - * Superpowers plugin for OpenCode.ai + * Superpowers plugin for OpenCode v2. * - * Injects superpowers bootstrap context via message transform. - * Auto-registers skills directory via config hook (no symlinks needed). + * OpenCode v2 replaced the v1 plugin API entirely (v1 plugins do not run under + * v2). This plugin targets the v2 API documented at + * https://v2.opencode.ai/build/plugins: + * + * - Default export is a plain `{ id, setup }` object. (`Plugin.define()` from + * "@opencode-ai/plugin/v2" is an identity wrapper, so we avoid the runtime + * import to keep this a zero-dependency plugin that loads in any layout.) + * - `ctx.skill.transform()` registers the bundled skills directory as a skill + * source, so OpenCode discovers superpowers skills with no symlinks or + * manual `skills.paths` edits. + * - `ctx.session.hook("context", ...)` injects the superpowers bootstrap into + * the first user message immediately before each model dispatch. */ import path from 'path'; import fs from 'fs'; -import os from 'os'; import { fileURLToPath } from 'url'; const __dirname = path.dirname(fileURLToPath(import.meta.url)); +// Bundled skills live two levels up from this plugin file (../../skills). +const superpowersSkillsDir = path.resolve(__dirname, '../../skills'); + // Simple frontmatter extraction (avoid dependency on skills-core for bootstrap) const extractAndStripFrontmatter = (content) => { const match = content.match(/^---\n([\s\S]*?)\n---\n([\s\S]*)$/); @@ -33,60 +45,41 @@ const extractAndStripFrontmatter = (content) => { return { frontmatter, content: body }; }; -// Normalize a path: trim whitespace, expand ~, resolve to absolute -const normalizePath = (p, homeDir) => { - if (!p || typeof p !== 'string') return null; - let normalized = p.trim(); - if (!normalized) return null; - if (normalized.startsWith('~/')) { - normalized = path.join(homeDir, normalized.slice(2)); - } else if (normalized === '~') { - normalized = homeDir; - } - return path.resolve(normalized); -}; - // Module-level cache for bootstrap content. // The SKILL.md file does not change during a session, so reading + parsing it // once eliminates redundant fs.existsSync + fs.readFileSync + regex work on -// every agent step. See #1202 for the full analysis. +// every model dispatch. See #1202 for the full analysis. let _bootstrapCache = undefined; // undefined = not yet loaded, null = file missing -export const SuperpowersPlugin = async ({ client, directory }) => { - const homeDir = os.homedir(); - const superpowersSkillsDir = path.resolve(__dirname, '../../skills'); - const envConfigDir = normalizePath(process.env.OPENCODE_CONFIG_DIR, homeDir); - const configDir = envConfigDir || path.join(homeDir, '.config/opencode'); - - // Helper to generate bootstrap content (cached after first call) - const getBootstrapContent = () => { - // Return cached result on subsequent calls - if (_bootstrapCache !== undefined) return _bootstrapCache; - - // Try to load using-superpowers skill - const skillPath = path.join(superpowersSkillsDir, 'using-superpowers', 'SKILL.md'); - if (!fs.existsSync(skillPath)) { - _bootstrapCache = null; - return null; - } +// Helper to generate bootstrap content (cached after first call) +const getBootstrapContent = () => { + // Return cached result on subsequent calls + if (_bootstrapCache !== undefined) return _bootstrapCache; - const fullContent = fs.readFileSync(skillPath, 'utf8'); - const { content } = extractAndStripFrontmatter(fullContent); + // Try to load using-superpowers skill + const skillPath = path.join(superpowersSkillsDir, 'using-superpowers', 'SKILL.md'); + if (!fs.existsSync(skillPath)) { + _bootstrapCache = null; + return null; + } - const toolMapping = `**Tool Mapping for OpenCode:** + const fullContent = fs.readFileSync(skillPath, 'utf8'); + const { content } = extractAndStripFrontmatter(fullContent); + + const toolMapping = `**Tool Mapping for OpenCode:** When skills request actions, substitute OpenCode equivalents: - Create or update todos → \`todowrite\` -- \`Subagent (general-purpose):\` → \`task\` with \`subagent_type: "general"\` +- \`Subagent (general-purpose):\` → \`task\` with \`subagent_type: "general"\` (or \`"explore"\` for codebase exploration) - Invoke a skill → OpenCode's native \`skill\` tool - Read files → \`read\` -- Create, edit, or delete files → \`apply_patch\` +- Create a file → \`write\`; edit a file → \`edit\` - Run shell commands → \`bash\` -- Search files → \`grep\`, \`glob\` +- Search file contents / find files by name → \`grep\`, \`glob\` - Fetch a URL → \`webfetch\` Use OpenCode's native \`skill\` tool to list and load skills.`; - _bootstrapCache = ` + _bootstrapCache = ` You have superpowers. **IMPORTANT: The using-superpowers skill content is included below. It is ALREADY LOADED - you are currently following it. Do NOT use the skill tool to load "using-superpowers" again - that would be redundant.** @@ -96,44 +89,51 @@ ${content} ${toolMapping} `; - return _bootstrapCache; - }; - - return { - // Inject skills path into live config so OpenCode discovers superpowers skills - // without requiring manual symlinks or config file edits. - // This works because Config.get() returns a cached singleton — modifications - // here are visible when skills are lazily discovered later. - config: async (config) => { - config.skills = config.skills || {}; - config.skills.paths = config.skills.paths || []; - if (!config.skills.paths.includes(superpowersSkillsDir)) { - config.skills.paths.push(superpowersSkillsDir); + return _bootstrapCache; +}; + +export default { + id: 'superpowers', + + setup: async (ctx) => { + // Register the bundled skills directory as a skill source so OpenCode + // discovers superpowers skills without symlinks or manual config edits. + // Guard against re-adding the same directory if setup runs more than once. + await ctx.skill.transform((draft) => { + const alreadyRegistered = draft + .list() + .some((source) => source.type === 'directory' + && path.resolve(source.path) === superpowersSkillsDir); + if (!alreadyRegistered) { + draft.source({ type: 'directory', path: superpowersSkillsDir }); } - }, + }); + await ctx.skill.reload(); - // Inject bootstrap into the first user message of each session. + // Inject bootstrap into the first user message of each request. // Using a user message instead of a system message avoids: // 1. Token bloat from system messages repeated every turn (#750) // 2. Multiple system messages breaking Qwen and other models (#894) // - // The hook fires on every agent step (not just every turn) because - // opencode's prompt.ts reloads messages from DB each step. Fresh message - // arrays may need injection again, so getBootstrapContent() must not do - // repeated disk work. - 'experimental.chat.messages.transform': async (_input, output) => { + // The hook fires before every model dispatch, so getBootstrapContent() + // must avoid repeated disk work (it caches at module level). + await ctx.session.hook('context', (input) => { const bootstrap = getBootstrapContent(); - if (!bootstrap || !output.messages.length) return; - const firstUser = output.messages.find(m => m.info.role === 'user'); - if (!firstUser || !firstUser.parts.length) return; - - // Guard: skip if first user message already contains bootstrap. - // This prevents double injection when OpenCode passes an already - // transformed in-memory message array through the hook again. - if (firstUser.parts.some(p => p.type === 'text' && p.text.includes('EXTREMELY_IMPORTANT'))) return; + if (!bootstrap || !input.messages || !input.messages.length) return; + + const firstUser = input.messages.find((m) => m.role === 'user'); + if (!firstUser || !Array.isArray(firstUser.content) || !firstUser.content.length) return; + + // Guard: skip if the first user message already contains the bootstrap. + // This prevents double injection when the hook re-runs over a message + // array that was already transformed in a previous dispatch. + if (firstUser.content.some((part) => part.type === 'text' + && typeof part.text === 'string' + && part.text.includes('EXTREMELY_IMPORTANT'))) { + return; + } - const ref = firstUser.parts[0]; - firstUser.parts.unshift({ ...ref, type: 'text', text: bootstrap }); - } - }; + firstUser.content.unshift({ type: 'text', text: bootstrap }); + }); + }, }; diff --git a/docs/README.opencode.md b/docs/README.opencode.md index 11da85425a..fdc8cabf0d 100644 --- a/docs/README.opencode.md +++ b/docs/README.opencode.md @@ -1,14 +1,21 @@ # Superpowers for OpenCode -Complete guide for using Superpowers with [OpenCode.ai](https://opencode.ai). +Complete guide for using Superpowers with [OpenCode v2](https://v2.opencode.ai). + +> **OpenCode v2 only.** This plugin targets the OpenCode v2 plugin API +> (default `{ id, setup }` export, `ctx.skill.transform`, `ctx.session.hook`). +> OpenCode states that v1 plugins do not run under v2, so use an older +> Superpowers release if you are still on OpenCode v1. ## Installation -Add superpowers to the `plugin` array in your `opencode.json` (global or project-level): +Add superpowers to the `plugins` array in your `opencode.json` (global or +project-level). `plugins` is the canonical v2 key; the legacy singular `plugin` +key still loads for now but may be removed: ```json { - "plugin": ["superpowers@git+https://github.com/obra/superpowers.git"] + "plugins": ["superpowers@git+https://github.com/obra/superpowers.git"] } ``` @@ -91,39 +98,44 @@ To pin a specific version, use a branch or tag: ```json { - "plugin": ["superpowers@git+https://github.com/obra/superpowers.git#v5.0.3"] + "plugins": ["superpowers@git+https://github.com/obra/superpowers.git#v5.0.3"] } ``` ## How It Works -The plugin does two things: +The plugin's `setup(ctx)` does two things: -1. **Injects bootstrap context** via the `experimental.chat.messages.transform` hook, adding superpowers awareness to every conversation. -2. **Registers the skills directory** via the `config` hook, so OpenCode discovers all superpowers skills without symlinks or manual config. +1. **Registers the skills directory** via `ctx.skill.transform()`, adding the + bundled `skills/` folder as a directory skill source so OpenCode discovers + all superpowers skills without symlinks or manual `skills.paths` config. +2. **Injects bootstrap context** via `ctx.session.hook("context", ...)`, + prepending superpowers awareness to the first user message before each model + dispatch. (Using a user message rather than a system message avoids token + bloat and multi-system-message issues on some models.) ### Tool Mapping -Skills speak in actions rather than naming any one runtime's tools. On OpenCode these resolve to: +Skills speak in actions rather than naming any one runtime's tools. On OpenCode v2 these resolve to: - "Create a todo" / "mark complete in todo list" → `todowrite` - `Subagent (general-purpose):` template → OpenCode's `task` tool with `subagent_type: "general"` (or `"explore"` for codebase exploration) - "Invoke a skill" → OpenCode's native `skill` tool - "Read a file" → `read` -- "Create a file" / "edit a file" / "delete a file" → `apply_patch` +- "Create a file" → `write`; "edit a file" → `edit` - "Run a shell command" → `bash` - "Search file contents" / "find files by name" → `grep`, `glob` - "Fetch a URL" → `webfetch` -(Verified against the installed OpenCode CLI's tool inventory.) +(Verified against the installed OpenCode v2 CLI's tool inventory.) ## Troubleshooting ### Plugin not loading -1. Check OpenCode logs: `opencode run --print-logs "hello" 2>&1 | grep -i superpowers` -2. Verify the plugin line in your `opencode.json` is correct -3. Make sure you're running a recent version of OpenCode +1. Check OpenCode logs: `grep -i "loading plugin" ~/.local/share/opencode/log/opencode.log` (OpenCode v2 logs to a file; the CLI `run` command no longer has a `--print-logs` flag). If the shared background service is stuck, run `opencode service restart`. +2. Verify the plugin line in your `opencode.json` is correct and uses the `plugins` key +3. Make sure you're running OpenCode v2 ### Windows install issues @@ -141,7 +153,7 @@ Then use the installed package path in `opencode.json`: ```json { - "plugin": ["~/.config/opencode/node_modules/superpowers"] + "plugins": ["~/.config/opencode/node_modules/superpowers"] } ``` @@ -153,11 +165,11 @@ Then use the installed package path in `opencode.json`: ### Bootstrap not appearing -1. Check OpenCode version supports `experimental.chat.messages.transform` hook -2. Restart OpenCode after config changes +1. Make sure you are on OpenCode v2 (the plugin uses the v2 `ctx.session.hook("context", ...)` API) +2. Restart OpenCode after config changes (`opencode service restart` if the background service is stuck) ## Getting Help - Report issues: https://github.com/obra/superpowers/issues - Main documentation: https://github.com/obra/superpowers -- OpenCode docs: https://opencode.ai/docs/ +- OpenCode docs: https://v2.opencode.ai/ diff --git a/docs/porting-to-a-new-harness.md b/docs/porting-to-a-new-harness.md index 4ae9603def..a1442bda38 100644 --- a/docs/porting-to-a-new-harness.md +++ b/docs/porting-to-a-new-harness.md @@ -157,10 +157,11 @@ A port is finished when **all** of these are true: A quick smoke check before the full acceptance test: start a session and ask the model to describe its superpowers. If the bootstrap injected, it knows it has -them. (OpenCode's install doc uses `opencode run --print-logs "hello" 2>&1 | -grep -i superpowers` for the same goal via a different mechanism — log-grep -rather than asking the model; the `2>&1` matters because logs go to stderr. Find -your harness's equivalent.) +them. (OpenCode's install doc uses `grep -i "loading plugin" +~/.local/share/opencode/log/opencode.log` for the same goal via a different +mechanism — log-grep rather than asking the model; OpenCode v2 writes logs to +that file rather than exposing a `--print-logs` CLI flag. Find your harness's +equivalent.) --- @@ -520,7 +521,7 @@ honors the rule rather than breaking it. Distinguish three cases: 2. **Native skill *discovery* but no `Skill` tool** (pi, Antigravity): the harness can find and list skills, but the model can't call a tool to load one. Get the skills installed where the harness scans (pi registers via `resources_discover` - → `skillPaths`; OpenCode via its `config` hook; `agy plugin install` copies + → `skillPaths`; OpenCode via its `ctx.skill.transform` hook; `agy plugin install` copies them in), and tell the model to load a skill by **reading its `SKILL.md` with the file-read tool when the skill applies** — the sanctioned mechanism here, the way `references/pi-tools.md` states it. @@ -790,7 +791,7 @@ Use this as the live index; when in doubt, read the files, not this table. | Copilot CLI | (shares Claude Code hook path; `COPILOT_CLI` env) | shell hook → `hooks/session-start` (`additionalContext`) | none needed (Claude Code–compatible tool surface) | `tests/hooks/` | — | | Gemini CLI | `gemini-extension.json` + `GEMINI.md` | instructions file `@`-includes bootstrap + mapping | `references/gemini-tools.md` | — | `gemini extensions install` | | Kimi Code | `.kimi-plugin/plugin.json` | manifest `sessionStart.skill` loads `using-superpowers` | inline `skillInstructions` in manifest | `tests/kimi/` | marketplace or `/plugins install` GitHub URL | -| OpenCode | `.opencode/plugins/superpowers.js` (declared via root `package.json` `main`) | in-process: `config` hook registers skills dir; `experimental.chat.messages.transform` injects user message | inline in `superpowers.js` | `tests/opencode/` | `opencode.json` plugin git URL | +| OpenCode (v2) | `.opencode/plugins/superpowers.js` (declared via root `package.json` `main`) | in-process v2 plugin (`{ id, setup }` default export): `ctx.skill.transform` registers skills dir; `ctx.session.hook("context", ...)` injects user message | inline in `superpowers.js` | `tests/opencode/` | `opencode.json` `plugins` git URL | | pi | `.pi/extensions/superpowers.ts` | in-process: `resources_discover` registers skills; `context` event injects user message; lifecycle-flag + compaction-aware | `piToolMapping()` inline **and** `references/pi-tools.md` | `tests/pi/` | repo-root `package.json` fields | ## Appendix B — Gotchas that have bitten porters diff --git a/tests/opencode/test-bootstrap-caching.mjs b/tests/opencode/test-bootstrap-caching.mjs index 32149aed33..e1e05b34dc 100644 --- a/tests/opencode/test-bootstrap-caching.mjs +++ b/tests/opencode/test-bootstrap-caching.mjs @@ -29,25 +29,54 @@ fs.readFileSync = function (...args) { }; const mod = await import(pathToFileURL(pluginPath).href); -const plugin = await mod.SuperpowersPlugin({ client: {}, directory: '.' }); -const transform = plugin['experimental.chat.messages.transform']; +const plugin = mod.default; -const firstOutput = makeOutput(`${scenario} bootstrap first step`); -await transform({}, firstOutput); +if (!plugin || typeof plugin.setup !== 'function') { + console.error('FAIL: plugin default export is missing a setup(ctx) function (OpenCode v2 API)'); + process.exit(1); +} + +// Drive the v2 plugin: run setup() with a mock context that captures the +// "context" session hook, then invoke that hook the way OpenCode does before +// each model dispatch. +let contextHook; +const mockCtx = { + skill: { + transform: async (cb) => cb({ source: () => {}, list: () => [] }), + reload: async () => {}, + }, + session: { + hook: async (name, cb) => { + if (name === 'context') contextHook = cb; + }, + }, +}; + +await plugin.setup(mockCtx); + +if (typeof contextHook !== 'function') { + console.error('FAIL: plugin.setup did not register a "context" session hook'); + process.exit(1); +} + +const firstInput = makeInput(`${scenario} bootstrap first step`); +contextHook(firstInput); const afterFirst = { existsCount, readCount }; -const secondOutput = makeOutput(`${scenario} bootstrap second step`); -await transform({}, secondOutput); +const secondInput = makeInput(`${scenario} bootstrap second step`); +contextHook(secondInput); const afterSecond = { existsCount, readCount }; const result = { scenario, - firstBootstrapParts: countBootstrapParts(firstOutput), - secondBootstrapParts: countBootstrapParts(secondOutput), - staleMentionMapping: bootstrapText(firstOutput).includes('@mention'), - staleTaskMapping: bootstrapText(firstOutput).includes('`Task` tool with subagents'), - mapsSubagentToTask: bootstrapText(firstOutput).includes('`task` with `subagent_type: "general"`'), - mapsMutationToApplyPatch: bootstrapText(firstOutput).includes('`apply_patch`'), + firstBootstrapParts: countBootstrapParts(firstInput), + secondBootstrapParts: countBootstrapParts(secondInput), + staleMentionMapping: bootstrapText(firstInput).includes('@mention'), + staleTaskMapping: bootstrapText(firstInput).includes('`Task` tool with subagents'), + staleApplyPatchMapping: bootstrapText(firstInput).includes('apply_patch'), + mapsSubagentToTask: bootstrapText(firstInput).includes('`task` with `subagent_type: "general"`'), + mapsMutationToWriteEdit: bootstrapText(firstInput).includes('`write`') + && bootstrapText(firstInput).includes('`edit`'), firstReadCount: afterFirst.readCount, secondReadCount: afterSecond.readCount, firstExistsCount: afterFirst.existsCount, @@ -72,23 +101,26 @@ function isBootstrapSkillPath(filePath) { return String(filePath).replaceAll('\\', '/').includes('using-superpowers/SKILL.md'); } -function makeOutput(text) { +// OpenCode v2 message shape: { role, content: [{ type: 'text', text }] } +function makeInput(text) { return { + system: [], + tools: {}, messages: [{ - info: { role: 'user' }, - parts: [{ type: 'text', text }], + role: 'user', + content: [{ type: 'text', text }], }], }; } -function countBootstrapParts(output) { - return output.messages[0].parts.filter( +function countBootstrapParts(input) { + return input.messages[0].content.filter( (part) => part.type === 'text' && part.text.includes('EXTREMELY_IMPORTANT') ).length; } -function bootstrapText(output) { - return output.messages[0].parts.find( +function bootstrapText(input) { + return input.messages[0].content.find( (part) => part.type === 'text' && part.text.includes('EXTREMELY_IMPORTANT') )?.text || ''; } @@ -96,19 +128,19 @@ function bootstrapText(output) { function assertPresentBootstrap(result) { const failures = []; if (result.firstBootstrapParts !== 1) { - failures.push(`expected first transform to inject one bootstrap part, got ${result.firstBootstrapParts}`); + failures.push(`expected first hook to inject one bootstrap part, got ${result.firstBootstrapParts}`); } if (result.secondBootstrapParts !== 1) { - failures.push(`expected second transform to inject one bootstrap part, got ${result.secondBootstrapParts}`); + failures.push(`expected second hook to inject one bootstrap part, got ${result.secondBootstrapParts}`); } if (result.firstReadCount !== 1) { - failures.push(`expected first transform to read SKILL.md once, got ${result.firstReadCount}`); + failures.push(`expected first hook to read SKILL.md once, got ${result.firstReadCount}`); } if (result.secondReadCount !== result.firstReadCount) { - failures.push(`expected cached second transform to do no additional reads, got ${result.secondReadCount - result.firstReadCount}`); + failures.push(`expected cached second hook to do no additional reads, got ${result.secondReadCount - result.firstReadCount}`); } if (result.secondExistsCount !== result.firstExistsCount) { - failures.push(`expected cached second transform to do no additional exists checks, got ${result.secondExistsCount - result.firstExistsCount}`); + failures.push(`expected cached second hook to do no additional exists checks, got ${result.secondExistsCount - result.firstExistsCount}`); } if (result.staleMentionMapping) { failures.push('expected OpenCode bootstrap not to teach @mention subagent syntax'); @@ -116,11 +148,14 @@ function assertPresentBootstrap(result) { if (result.staleTaskMapping) { failures.push('expected OpenCode bootstrap not to teach stale Task-tool mapping'); } + if (result.staleApplyPatchMapping) { + failures.push('expected OpenCode v2 bootstrap not to reference the removed apply_patch tool'); + } if (!result.mapsSubagentToTask) { failures.push('expected OpenCode bootstrap to map general-purpose subagents to task with subagent_type'); } - if (!result.mapsMutationToApplyPatch) { - failures.push('expected OpenCode bootstrap to map file mutation to apply_patch'); + if (!result.mapsMutationToWriteEdit) { + failures.push('expected OpenCode v2 bootstrap to map file mutation to write/edit'); } return failures; } @@ -131,13 +166,13 @@ function assertMissingBootstrap(result) { failures.push(`expected no bootstrap when SKILL.md is missing, got ${result.firstBootstrapParts}`); } if (result.secondBootstrapParts !== 0) { - failures.push(`expected no bootstrap on second missing-file transform, got ${result.secondBootstrapParts}`); + failures.push(`expected no bootstrap on second missing-file hook, got ${result.secondBootstrapParts}`); } if (result.firstReadCount !== 0 || result.secondReadCount !== 0) { failures.push(`expected missing file path to avoid reads, got ${result.secondReadCount}`); } if (result.firstExistsCount < 1) { - failures.push('expected first transform to check whether SKILL.md exists'); + failures.push('expected first hook to check whether SKILL.md exists'); } if (result.secondExistsCount !== result.firstExistsCount) { failures.push(`expected missing-file result to be cached, got ${result.secondExistsCount - result.firstExistsCount} extra exists checks`); diff --git a/tests/opencode/test-priority.sh b/tests/opencode/test-priority.sh index b9f3e08a8e..5af9bba726 100755 --- a/tests/opencode/test-priority.sh +++ b/tests/opencode/test-priority.sh @@ -107,7 +107,7 @@ run_opencode() { local exit_code set +e - command_output=$(cd "$dir" && timeout "${OPENCODE_TEST_TIMEOUT_SECONDS}s" opencode run --print-logs --format json "$prompt" 2>&1) + command_output=$(cd "$dir" && timeout "${OPENCODE_TEST_TIMEOUT_SECONDS}s" opencode run --format json "$prompt" 2>&1) exit_code=$? set -e diff --git a/tests/opencode/test-tools.sh b/tests/opencode/test-tools.sh index da79f3d5ed..918268b23b 100755 --- a/tests/opencode/test-tools.sh +++ b/tests/opencode/test-tools.sh @@ -31,7 +31,7 @@ run_opencode() { local exit_code set +e - command_output=$(cd "$dir" && timeout "${OPENCODE_TEST_TIMEOUT_SECONDS}s" opencode run --print-logs --format json "$prompt" 2>&1) + command_output=$(cd "$dir" && timeout "${OPENCODE_TEST_TIMEOUT_SECONDS}s" opencode run --format json "$prompt" 2>&1) exit_code=$? set -e