Skip to content

Commit 1bf74b1

Browse files
committed
fix: embed workflow descriptions in tool description instead of .describe()
OpenCode does not propagate Zod parameter .describe() text to the LLM. Confirmed by canary test - descriptions are invisible regardless of zod instance or registry. The tool description IS propagated, so embed the workflow enum choices and their descriptions there directly. Also removes the tool.definition hook and globalRegistry bridging code which were solving the wrong problem.
1 parent df8ebe3 commit 1bf74b1

4 files changed

Lines changed: 152 additions & 57 deletions

File tree

.vibe/development-plan-fix-opencode-plugin-parameter-descriptions.md

Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -161,12 +161,21 @@ The core challenge: from within the plugin, we needed a reference to OpenCode's
161161

162162
### Final Working Solution
163163

164-
Dynamic `import('zod')` in the `tool.definition` hook + `globalRegistry.add()` for each field schema:
165-
- Works because in production (peerDep, no local copy) the import resolves to host's zod module cache
166-
- `output.parameters._zod.def.shape` gives access to the original plugin field schemas
167-
- `fieldSchema.description` reads from plugin's registry cross-instance (it's just a getter calling `core.globalRegistry.get(inst)`)
168-
- `hostRegistry.add(fieldSchema, { description })` makes the SAME schema object findable in host's registry
169-
- All 64 tests pass
164+
**Embed workflow descriptions in the tool `description` string** (not in `.describe()` on the arg schema).
165+
166+
All previous approaches (`globalRegistry` bridging, `tool.definition` hook, dynamic zod import) were abandoned after confirming via LLM inspection that OpenCode **never propagates Zod parameter `.describe()` text to the LLM at all** — confirmed by embedding a canary string `DESCRIPTION_TEST_CANARY_12345` via `.describe()` which was invisible to the LLM even after a fresh session.
167+
168+
The tool `description` field IS propagated. Fix: embed the workflow enum descriptions directly there:
169+
```
170+
Start a development workflow.
171+
172+
workflow parameter — available values:
173+
- epcc: EPCC — <description>
174+
- bugfix: Bugfix — <description>
175+
- custom: Use a custom workflow name
176+
```
177+
178+
All 286 tests pass. `generateWorkflowDescription()` import removed from `start-development.ts` (no longer needed). `tool.definition` hook removed from `plugin.ts`. `.describe()` calls on args left in place (harmless).
170179

171180
## Implementation Plan (Code Phase Tasks)
172181

opencode/opencode.json

Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
1+
{
2+
"$schema": "https://opencode.ai/config.json",
3+
"plugin": [
4+
"@ex-machina/opencode-anthropic-auth@1.8.0",
5+
"/Users/oliverjaegle/projects/privat/codemcp/workflows/packages/opencode-plugin/dist/index.js"
6+
],
7+
"provider": {
8+
"@ai-sdk/openai-compatible": {
9+
"name": "llama.cpp",
10+
"options": {
11+
"baseURL": "http://flinker:8080/v1"
12+
},
13+
"models": {
14+
"Qwen3-Coder-30B-A3B-Instruct-UD-Q8_K_XL.gguf": {
15+
"name": "Qwen3-Coder"
16+
},
17+
"gpt-oss-120b-F16.gguf": {
18+
"name": "gpt-oss-120b"
19+
},
20+
"devstral2-small": {
21+
"name": "Devstral-Small-2-24B-Instruct-2512-UD-Q8_K_XL.gguf"
22+
}
23+
}
24+
}
25+
},
26+
"mcp": {
27+
"workflows": {
28+
"command": ["npx", "@codemcp/workflows@latest"],
29+
"type": "local",
30+
"environment": {
31+
"COMMIT_BEHAVIOR": "end"
32+
},
33+
"enabled": false
34+
},
35+
"knowledge": {
36+
"command": ["npx", "@codemcp/knowledge@latest"],
37+
"type": "local"
38+
},
39+
"quiet_shell": {
40+
"command": ["npx", "@codemcp/quiet-shell@latest"],
41+
"type": "local",
42+
"enabled": true
43+
},
44+
"prompts": {
45+
"command": ["npx", "@codemcp/prompts@latest"],
46+
"type": "local"
47+
},
48+
"crowd-mcp-local": {
49+
"command": [
50+
"node",
51+
"/Users/oliverjaegle/projects/privat/mcp-server/crowd/packages/server/dist/index.js"
52+
],
53+
"environment": {
54+
"CROWD_DEMO_MODE": "true",
55+
"OPERATOR_NAME": "Oliver",
56+
"CROWD_LOG_LEVEL": "WARN"
57+
},
58+
"type": "local",
59+
"enabled": false
60+
},
61+
"kinderspiel": {
62+
"command": ["npx", "@codemcp/workflows@latest"],
63+
"type": "local",
64+
"enabled": false,
65+
"environment": {
66+
"VIBE_WORKFLOW_DOMAINS": "children"
67+
}
68+
}
69+
},
70+
"permission": {
71+
"proceed_to_phase": "ask"
72+
},
73+
"agent": {
74+
"vibe": {
75+
"description": "Responsible vibe development agent with structured workflows",
76+
"mode": "primary",
77+
"prompt": "IMPORTANT: ALWAYS use whats_next after every user message to determine the next steps.\n Follow the instructions you get from whats_next exactly!\nIMPORTANT: You may also receive errors. Those errors also contain instructions how to proceed. NEVER ignore errors from the mcp tools, but ALWAYS follow the instructions in the errors.\n",
78+
"tools": { "workflows": true },
79+
"permission": {
80+
"workflows_reset_development": "ask",
81+
"workflows_start_development": "ask",
82+
"workflows_proceed_to_phase": "ask"
83+
}
84+
},
85+
"crowd": {
86+
"description": "Manages subagents",
87+
"mode": "primary",
88+
"prompt": "You are a development project lead for software development projects. Use spawn_agent tool to employ agents for a specific capability. Assign atomic, well-described tasks.",
89+
"tools": {
90+
"workflows*": false,
91+
"crowd-mcp-local*": true
92+
}
93+
},
94+
"research": {
95+
"description": "Research and development",
96+
"mode": "primary",
97+
"prompt": "You are a researcher who knows the docs of particular systems and processes. Always search the docs for the questions you are asked and make sure to give precise, but compact answers and edits. If you don't find anything in the docs, respond clearly that you do not know about this topic",
98+
"tools": {
99+
"knowledge*": true
100+
}
101+
},
102+
"powerpoint": {
103+
"description": "PowerPoint presentation generation",
104+
"mode": "primary",
105+
"prompt": "You are a tool that generates PowerPoint presentations from text prompts. Use the ppt-mcp tools manipulate the slides.",
106+
"tools": {
107+
"ppt-mcp*": true
108+
}
109+
},
110+
"kinderspiel": {
111+
"description": "Ein Agent, der Kindern beim Entwickeln von Spielen hilft",
112+
"mode": "primary",
113+
"prompt": "You are a friendly, patient, and encouraging AI assistant helping a child (ages 8-12) learn game development.\n\n## 🌍 Language (CRITICAL)\n\n**Detect and match the child's language immediately:**\n\n- All responses, documents, and code comments in their language\n- Never switch languages mid-conversation\n\n## 🎨 Your Language and Tone\n\n- **Simple language**, short sentences\n- **Enthusiastic** like an excited older sibling\n- **Patient** - never rushed\n- **Celebratory** - every small win matters\n- **Supportive** - mistakes are learning opportunities\n\n## 🔧 Tools You MUST Use\n\n### Start the Workflow\n\n```\nstart_development({\n workflow: \"game-beginner\",\n require_reviews: true,\n commit_behaviour: \"phase\"\n})\n```\n\nIf you need to create project docs, link docs if they already exist in .vibe/docs\n\n### After Each User Message\n\n```\nwhats_next({\n context: \"Brief summary of current situation\",\n user_input: \"What the user just said\",\n conversation_summary: \"Overall progress\",\n recent_messages: [...]\n})\n```\n\n**Then follow the instructions you receive exactly!**\nYou're inspiring future creators! 🚀\n",
114+
"tools": {
115+
"kinderspiel*": true,
116+
"crowd-mcp-local*": false,
117+
"workflows": false
118+
}
119+
},
120+
"workflow": {
121+
"description": "Metal alignment over autonomy",
122+
"prompt": "You follow a defined workflow that helps you be in sync with the user.",
123+
"permission": {
124+
"start_development": "ask",
125+
"reset_development": "ask",
126+
"proceed_to_phase": "ask"
127+
}
128+
}
129+
}
130+
}

packages/opencode-plugin/src/plugin.ts

Lines changed: 0 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -797,51 +797,6 @@ ACTION REQUIRED: Use proceed_to_phase tool to move to a phase that allows editin
797797
),
798798
};
799799
})(),
800-
801-
/**
802-
* Bridge Zod .describe() descriptions from plugin's registry into host's registry.
803-
*
804-
* Problem: this plugin uses a different zod instance than OpenCode (host). In Zod v4,
805-
* .describe() stores descriptions in a module-level globalRegistry singleton. When
806-
* OpenCode calls z.toJSONSchema(parameters), it reads from its own registry which has
807-
* no entries for plugin schemas — so all parameter descriptions are missing from the
808-
* JSON Schema sent to the LLM.
809-
*
810-
* Solution: dynamically import 'zod' at hook call time. When the plugin is installed
811-
* without its own node_modules/zod (zod is a peerDependency), this import resolves to
812-
* the host's (OpenCode's) zod module — the same instance used when creating
813-
* output.parameters. We then register each field schema's description into the host's
814-
* globalRegistry, making them visible to the subsequent z.toJSONSchema() call.
815-
*/
816-
'tool.definition': async (
817-
_input: { toolID: string },
818-
output: { description: string; parameters: unknown }
819-
): Promise<void> => {
820-
try {
821-
const parameters = output.parameters as {
822-
_zod?: { def?: { shape?: Record<string, { description?: string }> } };
823-
};
824-
const shape = parameters?._zod?.def?.shape;
825-
if (!shape) return;
826-
827-
// Dynamically import zod — in production (installed without local node_modules/zod),
828-
// this resolves to the host's zod instance, sharing the same globalRegistry.
829-
const { globalRegistry: hostRegistry } = await import('zod');
830-
831-
for (const [_key, fieldSchema] of Object.entries(shape)) {
832-
const desc = fieldSchema?.description;
833-
if (desc && typeof desc === 'string') {
834-
(
835-
hostRegistry as {
836-
add(schema: unknown, meta: { description: string }): void;
837-
}
838-
).add(fieldSchema, { description: desc });
839-
}
840-
}
841-
} catch {
842-
// Silently ignore — descriptions are a nice-to-have, not critical
843-
}
844-
},
845800
};
846801
};
847802

packages/opencode-plugin/src/tool-handlers/start-development.ts

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
import { z } from 'zod';
22
import {
33
StartDevelopmentHandler,
4-
generateWorkflowDescription,
54
buildWorkflowEnum,
65
type WhatsNextResult,
76
type ServerContext,
@@ -26,18 +25,20 @@ export function createStartDevelopmentTool(
2625
workflowManager.getAvailableWorkflowsForProject(projectDir);
2726
const workflowNames = availableWorkflows.map(w => w.name);
2827

29-
// Build tool description with workflow list
28+
// Build tool description with full workflow details embedded,
29+
// since OpenCode does not propagate Zod .describe() to the LLM.
30+
const workflowLines = availableWorkflows
31+
.map(w => ` - ${w.name}: ${w.displayName}${w.description}`)
32+
.join('\n');
3033
const toolDescription =
3134
workflowNames.length > 0
32-
? `Start a development workflow. Available: ${workflowNames.join(', ')}`
35+
? `Start a development workflow.\n\nworkflow parameter — available values:\n${workflowLines}\n - custom: Use a custom workflow name`
3336
: 'Start a development workflow (no workflows available - check WORKFLOW_DOMAINS)';
3437

3538
return tool({
3639
description: toolDescription,
3740
args: {
38-
workflow: z
39-
.enum(buildWorkflowEnum(workflowNames))
40-
.describe(generateWorkflowDescription(availableWorkflows)),
41+
workflow: z.enum(buildWorkflowEnum(workflowNames)),
4142
require_reviews: z
4243
.boolean()
4344
.optional()

0 commit comments

Comments
 (0)