From 450fa7d2a71b30ee3fbb130d8490b6212f4b102f Mon Sep 17 00:00:00 2001 From: Zeno Jiricek Date: Fri, 13 Feb 2026 15:07:31 +1030 Subject: [PATCH 1/2] fix(skill-injection): preserve active agent during prompt injection Pass through the tool context agent when injecting skill and resource\nprompts so OpenCode does not fall back to the default agent.\n\nAlso add a regression test for createInstructionInjector and include\nissue analysis notes for issue #28. --- .memory/issue-28-analysis-comment.md | 37 ++++++++++++++++++++++++ src/index.ts | 2 ++ src/lib/OpenCodeChat.test.ts | 43 ++++++++++++++++++++++++++++ src/lib/OpenCodeChat.ts | 3 +- 4 files changed, 84 insertions(+), 1 deletion(-) create mode 100644 .memory/issue-28-analysis-comment.md create mode 100644 src/lib/OpenCodeChat.test.ts diff --git a/.memory/issue-28-analysis-comment.md b/.memory/issue-28-analysis-comment.md new file mode 100644 index 0000000..7edb19d --- /dev/null +++ b/.memory/issue-28-analysis-comment.md @@ -0,0 +1,37 @@ +## Analysis + +Thanks for the detailed report — I validated this in code and agree with the root cause. + +### Summary + +- `skill_use` / skill prompt injection can reset to the default agent. +- The injection path currently sends a silent prompt without `agent`, so OpenCode may fall back to default agent selection. + +### CodeMapper findings + +- `createInstructionInjector` is defined in `src/lib/OpenCodeChat.ts`. +- `sendPrompt` currently calls `ctx.client.session.prompt(...)` with: + - `noReply: true` + - `parts: [{ type: 'text', text }]` + - **no `agent` field** +- Callers of this injection path are in `src/index.ts`: + - `skill_use` execution path + - `skill_resource` execution path + +### LSP / type-level evidence + +- `ToolContext` includes `agent: string` (`@opencode-ai/plugin/dist/tool.d.ts`). +- Session prompt request body supports `agent?: string` (`@opencode-ai/sdk/dist/gen/types.gen.d.ts`). + +So the agent value is available at tool execution time and can be forwarded safely. + +### Proposed fix + +1. Update injector props in `src/lib/OpenCodeChat.ts`: + - from `{ sessionId: string }` + - to `{ sessionId: string; agent: string }` +2. Include `agent: props.agent` in `ctx.client.session.prompt({ body: ... })`. +3. Update call sites in `src/index.ts` (`skill_use`, `skill_resource`) to pass `toolCtx.agent`. +4. Add regression test for injector to ensure silent injected messages preserve agent. + +This should preserve the original/current agent during skill loading and prevent fallback to default agent. diff --git a/src/index.ts b/src/index.ts index 98b90fb..5d4fb2d 100644 --- a/src/index.ts +++ b/src/index.ts @@ -94,6 +94,7 @@ export const SkillsPlugin: Plugin = async (ctx) => { for await (const skill of results.loaded) { await sendPrompt(renderer({ data: skill, type: 'Skill' }), { sessionId: toolCtx.sessionID, + agent: toolCtx.agent, }); } @@ -161,6 +162,7 @@ export const SkillsPlugin: Plugin = async (ctx) => { await sendPrompt(renderer({ data: result.injection, type: 'SkillResource' }), { sessionId: toolCtx.sessionID, + agent: toolCtx.agent, }); return JSON.stringify({ diff --git a/src/lib/OpenCodeChat.test.ts b/src/lib/OpenCodeChat.test.ts new file mode 100644 index 0000000..18629a3 --- /dev/null +++ b/src/lib/OpenCodeChat.test.ts @@ -0,0 +1,43 @@ +import type { PluginInput } from '@opencode-ai/plugin'; +import { describe, expect, it, vi } from 'vitest'; +import { createInstructionInjector } from './OpenCodeChat'; + +type PromptPayload = { + path: { id: string }; + body: { + agent?: string; + noReply?: boolean; + parts: Array<{ type: string; text: string }>; + }; +}; + +describe('createInstructionInjector', () => { + it('sends silent prompt using the original agent', async () => { + const promptMock = vi.fn<(...args: [PromptPayload]) => Promise>(async () => undefined); + + const ctx = { + client: { + session: { + prompt: promptMock, + }, + }, + } as unknown as PluginInput; + + const sendPrompt = createInstructionInjector(ctx); + + await sendPrompt('skill payload', { + sessionId: 'session-123', + agent: 'frontend-developer', + }); + + expect(promptMock).toHaveBeenCalledTimes(1); + expect(promptMock).toHaveBeenCalledWith({ + path: { id: 'session-123' }, + body: { + agent: 'frontend-developer', + noReply: true, + parts: [{ type: 'text', text: 'skill payload' }], + }, + }); + }); +}); diff --git a/src/lib/OpenCodeChat.ts b/src/lib/OpenCodeChat.ts index 96aec43..a22c5f5 100644 --- a/src/lib/OpenCodeChat.ts +++ b/src/lib/OpenCodeChat.ts @@ -2,10 +2,11 @@ import type { PluginInput } from '@opencode-ai/plugin'; export function createInstructionInjector(ctx: PluginInput) { // Message 1: Skill loading header (silent insertion - no AI response) - const sendPrompt = async (text: string, props: { sessionId: string }) => { + const sendPrompt = async (text: string, props: { sessionId: string; agent: string }) => { await ctx.client.session.prompt({ path: { id: props.sessionId }, body: { + agent: props.agent, noReply: true, parts: [{ type: 'text', text }], }, From 328a591a889f158a72d3033c97b27b906c415514 Mon Sep 17 00:00:00 2001 From: Zeno Jiricek Date: Fri, 13 Feb 2026 15:54:09 +1030 Subject: [PATCH 2/2] fix(ci): remove failing mise task files from CI context Remove .mise task scripts from the repository, including the watch task\nthat uses unsupported metadata in GitHub Actions' mise version.\n\nAlso drop pull_request_target from the PR workflow trigger to avoid\nrunning duplicate PR check workflows for the same branch update. --- .github/workflows/pr.yml | 1 - .mise/tasks/dev | 12 ------------ .mise/tasks/format | 3 --- .mise/tasks/prepare | 3 --- .mise/tasks/watch | 12 ------------ 5 files changed, 31 deletions(-) delete mode 100755 .mise/tasks/dev delete mode 100755 .mise/tasks/format delete mode 100755 .mise/tasks/prepare delete mode 100755 .mise/tasks/watch diff --git a/.github/workflows/pr.yml b/.github/workflows/pr.yml index 3a590d3..9de84b3 100644 --- a/.github/workflows/pr.yml +++ b/.github/workflows/pr.yml @@ -2,7 +2,6 @@ name: Pr on: pull_request: - pull_request_target: workflow_dispatch: concurrency: diff --git a/.mise/tasks/dev b/.mise/tasks/dev deleted file mode 100755 index 21548e6..0000000 --- a/.mise/tasks/dev +++ /dev/null @@ -1,12 +0,0 @@ -#!/usr/bin/env bash -#MISE description="Build plugin for development with sourcemaps and vendor splitting" -#MISE sources=["src/**/*"] -#MISE outputs=["dist/index.js", "dist/index.js.map", "dist/vendor.js"] - -bun build ./src/index.ts \ - --outdir dist \ - --target bun \ - --sourcemap=inline \ - --splitting \ - --entry-naming='[dir]/[name].js' \ - --chunk-naming='[name].js' diff --git a/.mise/tasks/format b/.mise/tasks/format deleted file mode 100755 index f2f6734..0000000 --- a/.mise/tasks/format +++ /dev/null @@ -1,3 +0,0 @@ -#!/usr/bin/env bash -#MISE description="Format code with Prettier" -prettier --write "src/**/*.ts" \ No newline at end of file diff --git a/.mise/tasks/prepare b/.mise/tasks/prepare deleted file mode 100755 index 9de48a1..0000000 --- a/.mise/tasks/prepare +++ /dev/null @@ -1,3 +0,0 @@ -#!/usr/bin/env bash -#MISE description="Prepare for publishing" -bun run build \ No newline at end of file diff --git a/.mise/tasks/watch b/.mise/tasks/watch deleted file mode 100755 index a8fa43e..0000000 --- a/.mise/tasks/watch +++ /dev/null @@ -1,12 +0,0 @@ -#!/usr/bin/env bash -#MISE description="Watch mode for plugin development with sourcemaps and vendor splitting" -#MISE watch=["src/**/*"] - -bun build ./src/index.ts \ - --outdir dist \ - --target bun \ - --sourcemap=inline \ - --splitting \ - --entry-naming='[dir]/[name].js' \ - --chunk-naming='[name].js' \ - --watch