@codemcp/workflows-opencode 6.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (39) hide show
  1. package/LICENSE +674 -0
  2. package/README.md +85 -0
  3. package/dist/index.d.ts +9 -0
  4. package/dist/index.js +11 -0
  5. package/dist/index.js.map +1 -0
  6. package/dist/opencode-logger.d.ts +21 -0
  7. package/dist/opencode-logger.js +104 -0
  8. package/dist/opencode-logger.js.map +1 -0
  9. package/dist/plugin.d.ts +23 -0
  10. package/dist/plugin.js +395 -0
  11. package/dist/plugin.js.map +1 -0
  12. package/dist/server-context.d.ts +40 -0
  13. package/dist/server-context.js +96 -0
  14. package/dist/server-context.js.map +1 -0
  15. package/dist/tool-handlers/conduct-review.d.ts +3 -0
  16. package/dist/tool-handlers/conduct-review.js +37 -0
  17. package/dist/tool-handlers/conduct-review.js.map +1 -0
  18. package/dist/tool-handlers/proceed-to-phase.d.ts +3 -0
  19. package/dist/tool-handlers/proceed-to-phase.js +74 -0
  20. package/dist/tool-handlers/proceed-to-phase.js.map +1 -0
  21. package/dist/tool-handlers/reset-development.d.ts +3 -0
  22. package/dist/tool-handlers/reset-development.js +63 -0
  23. package/dist/tool-handlers/reset-development.js.map +1 -0
  24. package/dist/tool-handlers/setup-project-docs.d.ts +3 -0
  25. package/dist/tool-handlers/setup-project-docs.js +74 -0
  26. package/dist/tool-handlers/setup-project-docs.js.map +1 -0
  27. package/dist/tool-handlers/start-development.d.ts +3 -0
  28. package/dist/tool-handlers/start-development.js +69 -0
  29. package/dist/tool-handlers/start-development.js.map +1 -0
  30. package/dist/tool-handlers/tool-helper.d.ts +10 -0
  31. package/dist/tool-handlers/tool-helper.js +7 -0
  32. package/dist/tool-handlers/tool-helper.js.map +1 -0
  33. package/dist/types.d.ts +193 -0
  34. package/dist/types.js +8 -0
  35. package/dist/types.js.map +1 -0
  36. package/dist/utils.d.ts +14 -0
  37. package/dist/utils.js +26 -0
  38. package/dist/utils.js.map +1 -0
  39. package/package.json +52 -0
package/README.md ADDED
@@ -0,0 +1,85 @@
1
+ # @codemcp/workflows-opencode
2
+
3
+ An [OpenCode](https://github.com/opencode-ai/opencode) plugin that enforces structured development workflows for AI coding agents.
4
+
5
+ ## Why?
6
+
7
+ Small and mid-sized LLMs tend to skip process steps, edit code during exploration phases, and lose track of decisions after context compaction. This plugin addresses these problems:
8
+
9
+ | Problem | Solution |
10
+ | ---------------------- | ------------------------------------------------------------------- |
11
+ | **Phase discipline** | Hard-blocks file edits that violate current phase restrictions |
12
+ | **Lost context** | Automatically injects phase instructions on every turn |
13
+ | **Context compaction** | Guides summary to preserve decisions and include phase continuation |
14
+
15
+ ## How it works
16
+
17
+ The plugin hooks into OpenCode's message pipeline:
18
+
19
+ - **`chat.message`** — Injects phase instructions after each user message
20
+ - **`tool.execute.before`** — Blocks disallowed file edits with a clear error (hard enforcement)
21
+ - **`experimental.session.compacting`** — Guides compaction to preserve key info and end with phase continuation
22
+
23
+ This replaces the need for the agent to call `whats_next()` — guidance is injected automatically.
24
+
25
+ ## Architecture: Synthetic Message Injection
26
+
27
+ Unlike the MCP server approach where agents must explicitly call `whats_next()`, this plugin uses **synthetic message injection**:
28
+
29
+ ```
30
+ User Message (as seen by LLM)
31
+ ├── Part 1: User's actual text
32
+ │ └── id: "prt_abc123..."
33
+
34
+ └── Part 2: Workflow guidance (INJECTED)
35
+ └── id: "prt_workflows_{timestamp}"
36
+ ```
37
+
38
+ The plugin intercepts each user message, fetches phase-specific instructions from the workflow engine, and appends them as an additional message part. The LLM sees both parts as coming from the user.
39
+
40
+ ### Principles
41
+
42
+ 1. **Zero tool overhead** — No LLM reasoning tokens spent on "should I call whats_next?"
43
+ 2. **Guaranteed execution** — Every user message triggers guidance injection
44
+ 3. **Invisible to the agent** — Instructions appear naturally in the conversation
45
+ 4. **Consistent task management** — `bd` CLI commands with `--parent` flags are always included
46
+
47
+ ### Efficiency Comparison
48
+
49
+ Measured from real sessions building a todo app:
50
+
51
+ | Metric | Plugin (synthetic) | MCP (tool calls) |
52
+ | ------------------------ | ------------------ | ---------------- |
53
+ | `whats_next` tool calls | 0 | 7 |
54
+ | Synthetic parts injected | 2 | 3 |
55
+ | Total tool calls | 82 | 106 |
56
+ | Files created | 36 | 0 (incomplete) |
57
+ | Task completion | ✅ Full app | ❌ Interrupted |
58
+
59
+ The plugin approach reduces tool call overhead by ~23% while maintaining full workflow compliance. Agents correctly use hierarchical task management (`bd create --parent <phase-id>`) without explicit tool invocations.
60
+
61
+ ## Installation
62
+
63
+ ```json
64
+ // opencode.json
65
+ {
66
+ "plugin": ["@codemcp/workflows-opencode"]
67
+ }
68
+ ```
69
+
70
+ Or for local development:
71
+
72
+ ```json
73
+ {
74
+ "plugin": ["/path/to/responsible-vibe/packages/opencode-plugin"]
75
+ }
76
+ ```
77
+
78
+ ## Status
79
+
80
+ Integrated with `@codemcp/workflows-core` for real state management and phase-based file restrictions.
81
+
82
+ ## Related
83
+
84
+ - [`@codemcp/workflows-core`](../core) — The workflow engine (shared with MCP server)
85
+ - [`@codemcp/workflows-server`](../mcp-server) — MCP server for non-OpenCode hosts (Claude Code, Cline, etc.)
@@ -0,0 +1,9 @@
1
+ /**
2
+ * OpenCode Workflows Plugin
3
+ *
4
+ * Structured development workflows for OpenCode.
5
+ * Replaces the MCP server with native OpenCode integration.
6
+ */
7
+ export { WorkflowsPlugin } from './plugin.js';
8
+ export * from './types.js';
9
+ export { default } from './plugin.js';
package/dist/index.js ADDED
@@ -0,0 +1,11 @@
1
+ /**
2
+ * OpenCode Workflows Plugin
3
+ *
4
+ * Structured development workflows for OpenCode.
5
+ * Replaces the MCP server with native OpenCode integration.
6
+ */
7
+ export { WorkflowsPlugin } from './plugin.js';
8
+ export * from './types.js';
9
+ // Default export for opencode plugin loader
10
+ export { default } from './plugin.js';
11
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;AAC9C,cAAc,YAAY,CAAC;AAE3B,4CAA4C;AAC5C,OAAO,EAAE,OAAO,EAAE,MAAM,aAAa,CAAC"}
@@ -0,0 +1,21 @@
1
+ import { type LoggerFactory } from '@codemcp/workflows-core';
2
+ import type { PluginInput } from './types.js';
3
+ /**
4
+ * Logger interface for structured logging
5
+ */
6
+ export interface Logger {
7
+ debug: (message: string, extra?: Record<string, unknown>) => void;
8
+ info: (message: string, extra?: Record<string, unknown>) => void;
9
+ warn: (message: string, extra?: Record<string, unknown>) => void;
10
+ error: (message: string, extra?: Record<string, unknown>) => void;
11
+ }
12
+ /**
13
+ * Create a logger factory that creates loggers which send output to OpenCode SDK.
14
+ * This factory can be passed to ServerContext so handlers use OpenCode logging.
15
+ */
16
+ export declare function createOpenCodeLoggerFactory(client: PluginInput['client']): LoggerFactory;
17
+ /**
18
+ * Create a logger backed by core's createLogger + a LogSink that delegates
19
+ * log output to the OpenCode SDK client.app.log() API.
20
+ */
21
+ export declare function createOpenCodeLogger(client: PluginInput['client']): Logger;
@@ -0,0 +1,104 @@
1
+ import { createLogger as coreCreateLogger, registerLogSink, } from '@codemcp/workflows-core';
2
+ /**
3
+ * Create a logger factory that creates loggers which send output to OpenCode SDK.
4
+ * This factory can be passed to ServerContext so handlers use OpenCode logging.
5
+ */
6
+ export function createOpenCodeLoggerFactory(client) {
7
+ const openCodeClient = client;
8
+ return (component) => {
9
+ return {
10
+ debug: (message, context) => {
11
+ openCodeClient.app
12
+ .log({
13
+ body: {
14
+ service: component,
15
+ level: 'debug',
16
+ message,
17
+ extra: context,
18
+ },
19
+ })
20
+ .catch(() => { });
21
+ },
22
+ info: (message, context) => {
23
+ openCodeClient.app
24
+ .log({
25
+ body: {
26
+ service: component,
27
+ level: 'info',
28
+ message,
29
+ extra: context,
30
+ },
31
+ })
32
+ .catch(() => { });
33
+ },
34
+ warn: (message, context) => {
35
+ openCodeClient.app
36
+ .log({
37
+ body: {
38
+ service: component,
39
+ level: 'warn',
40
+ message,
41
+ extra: context,
42
+ },
43
+ })
44
+ .catch(() => { });
45
+ },
46
+ error: (message, error, context) => {
47
+ const errorContext = error
48
+ ? { ...context, error: error.message, stack: error.stack }
49
+ : context;
50
+ openCodeClient.app
51
+ .log({
52
+ body: {
53
+ service: component,
54
+ level: 'error',
55
+ message,
56
+ extra: errorContext,
57
+ },
58
+ })
59
+ .catch(() => { });
60
+ },
61
+ };
62
+ };
63
+ }
64
+ /**
65
+ * Create a logger backed by core's createLogger + a LogSink that delegates
66
+ * log output to the OpenCode SDK client.app.log() API.
67
+ */
68
+ export function createOpenCodeLogger(client) {
69
+ const openCodeClient = client;
70
+ const service = 'plugin.workflows';
71
+ // Register a LogSink that forwards core log events to the OpenCode SDK.
72
+ // The core LogSink interface uses 'warning' for warn-level; we map that
73
+ // back to 'warn' for the OpenCode SDK which expects the shorter form.
74
+ const sink = {
75
+ log: (level, _logger, message, context) => {
76
+ const sdkLevel = level === 'warning' ? 'warn' : level;
77
+ try {
78
+ return openCodeClient.app.log({
79
+ body: {
80
+ service,
81
+ level: sdkLevel,
82
+ message,
83
+ extra: context,
84
+ },
85
+ });
86
+ }
87
+ catch {
88
+ return Promise.resolve();
89
+ }
90
+ },
91
+ };
92
+ registerLogSink(sink);
93
+ // Return a Logger that delegates to the core logger.
94
+ // The core error() method has signature (message, error?, context?) so we
95
+ // wrap it to match our simpler (message, extra?) interface.
96
+ const coreLogger = coreCreateLogger('plugin.workflows');
97
+ return {
98
+ debug: (message, extra) => coreLogger.debug(message, extra),
99
+ info: (message, extra) => coreLogger.info(message, extra),
100
+ warn: (message, extra) => coreLogger.warn(message, extra),
101
+ error: (message, extra) => coreLogger.error(message, undefined, extra),
102
+ };
103
+ }
104
+ //# sourceMappingURL=opencode-logger.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"opencode-logger.js","sourceRoot":"","sources":["../src/opencode-logger.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,23 @@
1
+ /**
2
+ * OpenCode Workflows Plugin
3
+ *
4
+ * Integrates workflows-core state management with OpenCode hooks to provide
5
+ * phase-aware development guidance and file edit restrictions.
6
+ *
7
+ * Hooks implemented:
8
+ * 1. chat.message - Add synthetic part with phase instructions after each user message
9
+ * 2. tool.execute.before - Block editing of certain files based on phase
10
+ * 3. experimental.session.compacting - Inject workflow state into compaction context
11
+ *
12
+ * Logs are sent via OpenCode SDK's client.app.log() API
13
+ */
14
+ import type { Plugin } from './types.js';
15
+ /**
16
+ * Main plugin export
17
+ */
18
+ export declare const WorkflowsPlugin: Plugin;
19
+ declare const _default: {
20
+ id: string;
21
+ server: Plugin;
22
+ };
23
+ export default _default;
package/dist/plugin.js ADDED
@@ -0,0 +1,395 @@
1
+ /**
2
+ * OpenCode Workflows Plugin
3
+ *
4
+ * Integrates workflows-core state management with OpenCode hooks to provide
5
+ * phase-aware development guidance and file edit restrictions.
6
+ *
7
+ * Hooks implemented:
8
+ * 1. chat.message - Add synthetic part with phase instructions after each user message
9
+ * 2. tool.execute.before - Block editing of certain files based on phase
10
+ * 3. experimental.session.compacting - Inject workflow state into compaction context
11
+ *
12
+ * Logs are sent via OpenCode SDK's client.app.log() API
13
+ */
14
+ import { createProceedToPhaseTool } from './tool-handlers/proceed-to-phase.js';
15
+ import { createConductReviewTool } from './tool-handlers/conduct-review.js';
16
+ import { createResetDevelopmentTool } from './tool-handlers/reset-development.js';
17
+ import { createStartDevelopmentTool } from './tool-handlers/start-development.js';
18
+ import { createSetupProjectDocsTool } from './tool-handlers/setup-project-docs.js';
19
+ import { createOpenCodeLogger, createOpenCodeLoggerFactory, } from './opencode-logger.js';
20
+ import { PlanManager, InstructionGenerator } from '@codemcp/workflows-core';
21
+ import { WhatsNextHandler, } from '@codemcp/workflows-server';
22
+ import { createServerContext, initializeServerContext, } from './server-context.js';
23
+ import { stripWhatsNextReferences } from './utils.js';
24
+ /**
25
+ * Match a file path against a glob pattern.
26
+ * Supports patterns like:
27
+ * - `**\/*` → matches everything
28
+ * - `**\/*.md` → matches any .md file in any directory
29
+ * - `**\/*.test.ts` → matches test files
30
+ */
31
+ function matchGlobPattern(filePath, pattern) {
32
+ // Normalise to forward slashes
33
+ const normalised = filePath.replace(/\\/g, '/');
34
+ const baseName = normalised.split('/').pop() ?? '';
35
+ // `**/*` means "allow everything"
36
+ if (pattern === '**/*' || pattern === '*') {
37
+ return true;
38
+ }
39
+ // Convert glob pattern to a regex:
40
+ // - Escape regex metacharacters except * and .
41
+ // - `**/` at the start → match any path prefix (or empty)
42
+ // - `**` elsewhere → match any sequence of characters incl. /
43
+ // - `*` → match any sequence of characters excl. /
44
+ // - `.` → literal dot
45
+ const regexSource = pattern
46
+ .replace(/\\/g, '/')
47
+ // Escape regex special chars (except * which we handle separately)
48
+ .replace(/[+?^${}()|[\]]/g, '\\$&')
49
+ // Literal dot
50
+ .replace(/\./g, '\\.')
51
+ // `**/` at the start → optional path prefix
52
+ .replace(/^\*\*\//, '(?:.+\\/)?')
53
+ // remaining `**` → any chars including /
54
+ .replace(/\*\*/g, '.*')
55
+ // remaining `*` → any chars except /
56
+ .replace(/\*/g, '[^/]*');
57
+ const regex = new RegExp(`^${regexSource}$`);
58
+ // Try matching against full normalised path and against basename
59
+ return regex.test(normalised) || regex.test(baseName);
60
+ }
61
+ /**
62
+ * Check if a file edit is allowed based on glob patterns
63
+ */
64
+ function isFileAllowed(filePath, patterns) {
65
+ // If allowed patterns includes '**/*' or '*', all files are allowed
66
+ if (patterns.includes('**/*') || patterns.includes('*')) {
67
+ return true;
68
+ }
69
+ // Check if the file path matches any allowed glob pattern
70
+ return patterns.some(pattern => matchGlobPattern(filePath, pattern));
71
+ }
72
+ /**
73
+ * Main plugin export
74
+ */
75
+ export const WorkflowsPlugin = async (input) => {
76
+ // Initialize logger using OpenCode SDK
77
+ const logger = createOpenCodeLogger(input.client);
78
+ const loggerFactory = createOpenCodeLoggerFactory(input.client);
79
+ logger.info('Plugin initializing', {
80
+ directory: input.directory,
81
+ worktree: input.worktree,
82
+ });
83
+ // Initialize workflows enabled state from environment variable
84
+ const envWorkflows = process.env.WORKFLOWS?.toLowerCase();
85
+ let workflowsEnabled = envWorkflows === 'off' ? false : true; // default: enabled
86
+ logger.info('Workflows state initialized', { workflowsEnabled });
87
+ // Initialize instruction generator
88
+ const planManager = new PlanManager();
89
+ const instructionGenerator = new InstructionGenerator();
90
+ // Cached ServerContext - created once, reused for all requests
91
+ // This avoids creating new WorkflowManager/PluginRegistry instances per request
92
+ let cachedServerContext = null;
93
+ let serverContextInitialized = false;
94
+ // Buffered instructions from tools (proceed_to_phase, start_development).
95
+ // Consumed and cleared by the next chat.message hook call.
96
+ let bufferedInstructions = null;
97
+ /**
98
+ * Set buffered instructions from a tool result.
99
+ * The next chat.message hook will use these instead of calling WhatsNextHandler.
100
+ */
101
+ function setBufferedInstructions(result) {
102
+ bufferedInstructions = {
103
+ phase: result.phase,
104
+ instructions: result.instructions,
105
+ planFilePath: result.plan_file_path,
106
+ allowedFilePatterns: result.allowed_file_patterns,
107
+ };
108
+ }
109
+ // Helper to get an initialized ServerContext for handler delegation
110
+ // Creates once, reuses for all subsequent calls
111
+ async function getServerContext() {
112
+ if (!cachedServerContext) {
113
+ cachedServerContext = createServerContext({
114
+ projectDir: input.directory,
115
+ planManager,
116
+ instructionGenerator,
117
+ loggerFactory,
118
+ });
119
+ }
120
+ if (!serverContextInitialized) {
121
+ await initializeServerContext(cachedServerContext);
122
+ serverContextInitialized = true;
123
+ }
124
+ return cachedServerContext;
125
+ }
126
+ // Log registered plugins at startup (once)
127
+ getServerContext()
128
+ .then(context => {
129
+ const pluginNames = context.pluginRegistry?.getPluginNames() ?? [];
130
+ if (pluginNames.length > 0) {
131
+ logger.info('Registered plugins', { plugins: pluginNames });
132
+ }
133
+ else {
134
+ logger.debug('No plugins registered');
135
+ }
136
+ })
137
+ .catch(() => {
138
+ // Ignore errors during startup plugin check
139
+ });
140
+ /**
141
+ * Read current workflow state from ConversationManager via shared ServerContext.
142
+ * Returns null if no active conversation exists.
143
+ */
144
+ async function getWorkflowState() {
145
+ try {
146
+ const serverContext = await getServerContext();
147
+ const context = await serverContext.conversationManager.getConversationContext();
148
+ const stateMachine = serverContext.workflowManager.loadWorkflowForProject(context.projectPath, context.workflowName);
149
+ const phaseState = stateMachine.states[context.currentPhase];
150
+ return {
151
+ phase: context.currentPhase,
152
+ phaseDescription: phaseState?.description ?? null,
153
+ allowedFilePatterns: phaseState?.allowed_file_patterns ?? ['**/*'],
154
+ workflowName: context.workflowName,
155
+ };
156
+ }
157
+ catch (_error) {
158
+ return null;
159
+ }
160
+ }
161
+ return {
162
+ /**
163
+ * Hook 1: chat.message
164
+ * Fires after user message is created but before LLM processes it.
165
+ * We add a synthetic part with phase instructions.
166
+ */
167
+ 'chat.message': async (hookInput, output) => {
168
+ // Skip if workflows are disabled
169
+ if (!workflowsEnabled) {
170
+ logger.debug('chat.message: Workflows disabled, skipping hook');
171
+ return;
172
+ }
173
+ let result = null;
174
+ // If a tool (proceed_to_phase / start_development) buffered instructions,
175
+ // use those — they are authoritative and avoid potential staleness from
176
+ // re-querying WhatsNextHandler.
177
+ if (bufferedInstructions) {
178
+ logger.debug('chat.message: Using buffered instructions from tool call', { phase: bufferedInstructions.phase });
179
+ result = {
180
+ phase: bufferedInstructions.phase,
181
+ instructions: bufferedInstructions.instructions,
182
+ plan_file_path: bufferedInstructions.planFilePath,
183
+ allowed_file_patterns: bufferedInstructions.allowedFilePatterns,
184
+ };
185
+ // Consume the buffer — next call will fall through to WhatsNextHandler
186
+ bufferedInstructions = null;
187
+ }
188
+ else {
189
+ // No buffered instructions — query WhatsNextHandler (reads from disk)
190
+ try {
191
+ const serverContext = await getServerContext();
192
+ const handler = new WhatsNextHandler();
193
+ const handlerResult = await handler.handle({}, serverContext);
194
+ if (!handlerResult.success || !handlerResult.data) {
195
+ logger.info('chat.message: No active workflow, injecting start prompt');
196
+ output.parts.push({
197
+ id: `prt_workflows_${Date.now()}`,
198
+ sessionID: hookInput.sessionID,
199
+ messageID: hookInput.messageID || output.message.id,
200
+ type: 'text',
201
+ text: `No Active Workflow Use the \`start_development\` tool to begin.`,
202
+ });
203
+ return;
204
+ }
205
+ result = handlerResult.data;
206
+ }
207
+ catch (error) {
208
+ const errorMessage = error instanceof Error ? error.message : String(error);
209
+ if (errorMessage.includes('CONVERSATION_NOT_FOUND')) {
210
+ logger.info('chat.message: No active workflow, injecting start prompt');
211
+ output.parts.push({
212
+ id: `prt_workflows_${Date.now()}`,
213
+ sessionID: hookInput.sessionID,
214
+ messageID: hookInput.messageID || output.message.id,
215
+ type: 'text',
216
+ text: `No Active Workflow Use the \`start_development\` tool to begin.`,
217
+ });
218
+ return;
219
+ }
220
+ logger.error('chat.message: Error delegating to WhatsNextHandler', {
221
+ error: errorMessage,
222
+ });
223
+ return;
224
+ }
225
+ }
226
+ logger.info('chat.message hook fired', {
227
+ sessionID: hookInput.sessionID,
228
+ phase: result.phase,
229
+ });
230
+ // Strip whats_next() references — plugin auto-injects instructions
231
+ const instructionText = stripWhatsNextReferences(result.instructions);
232
+ if (!instructionText.trim()) {
233
+ logger.info('chat.message: No instructions to inject');
234
+ return;
235
+ }
236
+ output.parts.push({
237
+ id: `prt_workflows_${Date.now()}`,
238
+ sessionID: hookInput.sessionID,
239
+ messageID: hookInput.messageID || output.message.id,
240
+ type: 'text',
241
+ text: instructionText,
242
+ });
243
+ logger.info('chat.message: injected phase instructions', {
244
+ phase: result.phase,
245
+ length: instructionText.length,
246
+ preview: instructionText.slice(0, 300),
247
+ });
248
+ },
249
+ /**
250
+ * Hook 2: tool.execute.before
251
+ * Fires before each tool execution. We block disallowed file edits based on phase.
252
+ */
253
+ 'tool.execute.before': async (hookInput, output) => {
254
+ // Skip if workflows are disabled
255
+ if (!workflowsEnabled) {
256
+ logger.debug('tool.execute.before: Workflows disabled, skipping hook');
257
+ return;
258
+ }
259
+ const editTools = ['edit', 'write', 'patch', 'apply_patch', 'multiedit'];
260
+ if (!editTools.includes(hookInput.tool)) {
261
+ return;
262
+ }
263
+ // Read current workflow state from ConversationManager
264
+ const state = await getWorkflowState();
265
+ if (!state) {
266
+ // No active workflow — allow all edits
267
+ return;
268
+ }
269
+ logger.debug('tool.execute.before', {
270
+ tool: hookInput.tool,
271
+ phase: state.phase,
272
+ });
273
+ // Extract file path from tool args
274
+ const args = output.args;
275
+ const filePath = String(args?.filePath || args?.path || '');
276
+ if (!filePath) {
277
+ logger.warn('Edit tool called without filePath', {
278
+ tool: hookInput.tool,
279
+ });
280
+ return;
281
+ }
282
+ if (!isFileAllowed(filePath, state.allowedFilePatterns)) {
283
+ const allowedList = state.allowedFilePatterns
284
+ .map(p => ` • ${p}`)
285
+ .join('\n');
286
+ const error = `BLOCKED: Cannot edit "${filePath}" in ${state.phase} phase.
287
+
288
+ Current phase "${state.phase}" only allows editing:
289
+ ${allowedList}
290
+
291
+ ACTION REQUIRED: Use transition_phase tool to move to a phase that allows editing this file type, OR focus on files matching the allowed patterns above.`;
292
+ logger.error('BLOCKING edit', {
293
+ filePath,
294
+ phase: state.phase,
295
+ allowedPatterns: state.allowedFilePatterns,
296
+ });
297
+ throw new Error(error);
298
+ }
299
+ },
300
+ /**
301
+ * Hook 3: experimental.session.compacting
302
+ * Fires when session is being compacted. We provide minimal guidance on what
303
+ * to preserve and instruct the summary to end with phase continuation.
304
+ */
305
+ 'experimental.session.compacting': async (hookInput, output) => {
306
+ // Skip if workflows are disabled
307
+ if (!workflowsEnabled) {
308
+ logger.debug('experimental.session.compacting: Workflows disabled, skipping hook');
309
+ return;
310
+ }
311
+ logger.debug('experimental.session.compacting hook fired', {
312
+ sessionID: hookInput.sessionID,
313
+ });
314
+ const state = await getWorkflowState();
315
+ if (!state) {
316
+ logger.debug('No active workflow - skipping compaction guidance');
317
+ return;
318
+ }
319
+ output.context.push('Preserve: user intents, key decisions, significant changes and the reasoning why they were made. Remove tool calls, intermediate thoughts, and minor details.');
320
+ output.context.push(`End summary with: "Continue ${state.phase} phase. ${state.phaseDescription || ''}"`);
321
+ logger.info('Injected compaction guidance', { phase: state.phase });
322
+ },
323
+ /**
324
+ * Hook 4: command.execute.before
325
+ * Intercept /workflow and /wf commands to toggle workflows enabled state
326
+ */
327
+ 'command.execute.before': async (hookInput, output) => {
328
+ const cmd = hookInput.command.toLowerCase();
329
+ const args = (hookInput.arguments || '').toLowerCase().trim();
330
+ if (cmd === 'workflow' || cmd === 'wf') {
331
+ if (args === 'on') {
332
+ workflowsEnabled = true;
333
+ output.parts.push({
334
+ id: `prt_workflows_toggle_${Date.now()}`,
335
+ type: 'text',
336
+ text: 'Workflows enabled for this session.',
337
+ });
338
+ logger.info('Workflows toggled via command', { workflowsEnabled });
339
+ }
340
+ else if (args === 'off') {
341
+ workflowsEnabled = false;
342
+ output.parts.push({
343
+ id: `prt_workflows_toggle_${Date.now()}`,
344
+ type: 'text',
345
+ text: 'Workflows disabled for this session. Plugin will not inject instructions or enforce file restrictions.',
346
+ });
347
+ logger.info('Workflows toggled via command', { workflowsEnabled });
348
+ }
349
+ else {
350
+ output.parts.push({
351
+ id: `prt_workflows_toggle_${Date.now()}`,
352
+ type: 'text',
353
+ text: `Usage: /workflow on|off or /wf on|off\nCurrent state: ${workflowsEnabled ? 'enabled' : 'disabled'}`,
354
+ });
355
+ }
356
+ }
357
+ },
358
+ /**
359
+ * Custom tools - matching MCP server tool names for consistency
360
+ */
361
+ tool: {
362
+ /**
363
+ * Tool: start_development
364
+ * Starts a new development workflow in the current project
365
+ */
366
+ start_development: createStartDevelopmentTool(input.directory, getServerContext, setBufferedInstructions),
367
+ /**
368
+ * Tool: proceed_to_phase
369
+ * Transitions to a new workflow phase
370
+ */
371
+ proceed_to_phase: createProceedToPhaseTool(getServerContext, setBufferedInstructions),
372
+ /**
373
+ * Tool: conduct_review
374
+ * Conducts a review before phase transition
375
+ */
376
+ conduct_review: createConductReviewTool(getServerContext),
377
+ /**
378
+ * Tool: reset_development
379
+ * Resets the current workflow and starts fresh
380
+ */
381
+ reset_development: createResetDevelopmentTool(input.directory, getServerContext),
382
+ /**
383
+ * Tool: setup_project_docs
384
+ * Creates project documentation artifacts
385
+ */
386
+ setup_project_docs: await createSetupProjectDocsTool(input.directory, getServerContext),
387
+ },
388
+ };
389
+ };
390
+ // Default export for opencode plugin loader
391
+ export default {
392
+ id: 'workflows',
393
+ server: WorkflowsPlugin,
394
+ };
395
+ //# sourceMappingURL=plugin.js.map