@codemcp/workflows 4.10.0 → 4.10.2

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 (74) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/dist/components/beads/beads-instruction-generator.d.ts +3 -4
  3. package/dist/components/beads/beads-instruction-generator.d.ts.map +1 -1
  4. package/dist/components/beads/beads-instruction-generator.js +12 -7
  5. package/dist/components/beads/beads-instruction-generator.js.map +1 -1
  6. package/dist/components/beads/beads-task-backend-client.d.ts.map +1 -1
  7. package/dist/components/beads/beads-task-backend-client.js +1 -4
  8. package/dist/components/beads/beads-task-backend-client.js.map +1 -1
  9. package/dist/plugin-system/beads-plugin.d.ts +70 -0
  10. package/dist/plugin-system/beads-plugin.d.ts.map +1 -0
  11. package/dist/plugin-system/beads-plugin.js +459 -0
  12. package/dist/plugin-system/beads-plugin.js.map +1 -0
  13. package/dist/plugin-system/index.d.ts +9 -0
  14. package/dist/plugin-system/index.d.ts.map +1 -0
  15. package/dist/plugin-system/index.js +9 -0
  16. package/dist/plugin-system/index.js.map +1 -0
  17. package/dist/plugin-system/plugin-interfaces.d.ts +99 -0
  18. package/dist/plugin-system/plugin-interfaces.d.ts.map +1 -0
  19. package/dist/plugin-system/plugin-interfaces.js +9 -0
  20. package/dist/plugin-system/plugin-interfaces.js.map +1 -0
  21. package/dist/plugin-system/plugin-registry.d.ts +44 -0
  22. package/dist/plugin-system/plugin-registry.d.ts.map +1 -0
  23. package/dist/plugin-system/plugin-registry.js +132 -0
  24. package/dist/plugin-system/plugin-registry.js.map +1 -0
  25. package/dist/server-config.d.ts.map +1 -1
  26. package/dist/server-config.js +28 -8
  27. package/dist/server-config.js.map +1 -1
  28. package/dist/tool-handlers/conduct-review.d.ts.map +1 -1
  29. package/dist/tool-handlers/conduct-review.js +1 -2
  30. package/dist/tool-handlers/conduct-review.js.map +1 -1
  31. package/dist/tool-handlers/proceed-to-phase.d.ts +0 -5
  32. package/dist/tool-handlers/proceed-to-phase.d.ts.map +1 -1
  33. package/dist/tool-handlers/proceed-to-phase.js +15 -93
  34. package/dist/tool-handlers/proceed-to-phase.js.map +1 -1
  35. package/dist/tool-handlers/start-development.d.ts +0 -13
  36. package/dist/tool-handlers/start-development.d.ts.map +1 -1
  37. package/dist/tool-handlers/start-development.js +29 -124
  38. package/dist/tool-handlers/start-development.js.map +1 -1
  39. package/dist/tool-handlers/whats-next.d.ts.map +1 -1
  40. package/dist/tool-handlers/whats-next.js +1 -0
  41. package/dist/tool-handlers/whats-next.js.map +1 -1
  42. package/dist/types.d.ts +2 -0
  43. package/dist/types.d.ts.map +1 -1
  44. package/package.json +2 -2
  45. package/src/components/beads/beads-instruction-generator.ts +12 -12
  46. package/src/components/beads/beads-task-backend-client.ts +1 -4
  47. package/src/plugin-system/beads-plugin.ts +641 -0
  48. package/src/plugin-system/index.ts +20 -0
  49. package/src/plugin-system/plugin-interfaces.ts +154 -0
  50. package/src/plugin-system/plugin-registry.ts +190 -0
  51. package/src/server-config.ts +30 -8
  52. package/src/tool-handlers/conduct-review.ts +1 -2
  53. package/src/tool-handlers/proceed-to-phase.ts +19 -135
  54. package/src/tool-handlers/start-development.ts +35 -205
  55. package/src/tool-handlers/whats-next.ts +1 -0
  56. package/src/types.ts +2 -0
  57. package/test/e2e/beads-plugin-integration.test.ts +1609 -0
  58. package/test/e2e/plugin-system-integration.test.ts +1729 -0
  59. package/test/unit/beads-plugin-behavioral.test.ts +512 -0
  60. package/test/unit/beads-plugin.test.ts +94 -0
  61. package/test/unit/plugin-error-handling.test.ts +240 -0
  62. package/test/unit/proceed-to-phase-plugin-integration.test.ts +150 -0
  63. package/test/unit/server-config-plugin-registry.test.ts +81 -0
  64. package/test/unit/start-development-goal-extraction.test.ts +22 -16
  65. package/test/utils/test-helpers.ts +3 -1
  66. package/tsconfig.build.tsbuildinfo +1 -1
  67. package/dist/components/server-components-factory.d.ts +0 -39
  68. package/dist/components/server-components-factory.d.ts.map +0 -1
  69. package/dist/components/server-components-factory.js +0 -62
  70. package/dist/components/server-components-factory.js.map +0 -1
  71. package/src/components/server-components-factory.ts +0 -86
  72. package/test/e2e/component-substitution.test.ts +0 -208
  73. package/test/unit/beads-integration-filename.test.ts +0 -93
  74. package/test/unit/server-components-factory.test.ts +0 -279
@@ -0,0 +1,154 @@
1
+ /**
2
+ * Plugin system interfaces for extending the responsible-vibe-mcp server
3
+ *
4
+ * Core Principle: Plugins receive only read-only context data and cannot
5
+ * directly manipulate core server components. They extend behavior through
6
+ * semantic lifecycle hooks only.
7
+ */
8
+
9
+ import type { YamlState } from '@codemcp/workflows-core';
10
+
11
+ /**
12
+ * Plugin interface - all plugins must implement this
13
+ */
14
+ export interface IPlugin {
15
+ /** Unique plugin name */
16
+ getName(): string;
17
+
18
+ /** Execution sequence (lower numbers execute first) */
19
+ getSequence(): number;
20
+
21
+ /** Whether plugin is enabled (typically based on environment) */
22
+ isEnabled(): boolean;
23
+
24
+ /** Lifecycle hooks this plugin provides */
25
+ getHooks(): PluginHooks;
26
+ }
27
+
28
+ /**
29
+ * Lifecycle hooks that plugins can implement
30
+ * All hooks receive standardized PluginHookContext as first parameter
31
+ */
32
+ export interface PluginHooks {
33
+ /** Called before development workflow starts */
34
+ beforeStartDevelopment?: (
35
+ context: PluginHookContext,
36
+ args: StartDevelopmentArgs
37
+ ) => Promise<void>;
38
+
39
+ /** Called after development workflow has started */
40
+ afterStartDevelopment?: (
41
+ context: PluginHookContext,
42
+ args: StartDevelopmentArgs,
43
+ result: StartDevelopmentResult
44
+ ) => Promise<void>;
45
+
46
+ /** Called after plan file is created - can modify content */
47
+ afterPlanFileCreated?: (
48
+ context: PluginHookContext,
49
+ planFilePath: string,
50
+ content: string
51
+ ) => Promise<string>;
52
+
53
+ /** Called before phase transition (can block by throwing) */
54
+ beforePhaseTransition?: (
55
+ context: PluginHookContext,
56
+ currentPhase: string,
57
+ targetPhase: string
58
+ ) => Promise<void>;
59
+
60
+ /** Called after instructions are generated - can modify them */
61
+ afterInstructionsGenerated?: (
62
+ context: PluginHookContext,
63
+ instructions: GeneratedInstructions
64
+ ) => Promise<GeneratedInstructions>;
65
+ }
66
+
67
+ /**
68
+ * Standardized context provided to all plugin hooks
69
+ * Contains ONLY read-only data - no server components
70
+ */
71
+ export interface PluginHookContext {
72
+ /** Current conversation ID */
73
+ conversationId: string;
74
+
75
+ /** Path to the plan file */
76
+ planFilePath: string;
77
+
78
+ /** Current development phase */
79
+ currentPhase: string;
80
+
81
+ /** Active workflow name */
82
+ workflow: string;
83
+
84
+ /** Project directory path */
85
+ projectPath: string;
86
+
87
+ /** Git branch name */
88
+ gitBranch: string;
89
+
90
+ /** Target phase (only available in phase transitions) */
91
+ targetPhase?: string;
92
+
93
+ /** Workflow state machine definition (read-only) - available in afterStartDevelopment */
94
+ stateMachine?: {
95
+ readonly name: string;
96
+ readonly description: string;
97
+ readonly initial_state: string;
98
+ readonly states: Record<string, YamlState>;
99
+ };
100
+
101
+ // EXPLICITLY EXCLUDED: No access to core server components like:
102
+ // - conversationManager (could manipulate conversations)
103
+ // - transitionEngine (could force transitions)
104
+ // - planManager (could bypass hook system)
105
+ // - instructionGenerator (could generate instructions outside flow)
106
+ }
107
+
108
+ /**
109
+ * Plugin registry interface for managing and executing plugins
110
+ */
111
+ export interface IPluginRegistry {
112
+ /** Register a plugin */
113
+ registerPlugin(plugin: IPlugin): void;
114
+
115
+ /** Get all enabled plugins sorted by sequence */
116
+ getEnabledPlugins(): IPlugin[];
117
+
118
+ /** Execute a specific hook on all plugins that implement it */
119
+ executeHook<T extends keyof PluginHooks>(
120
+ hookName: T,
121
+ ...args: Parameters<NonNullable<PluginHooks[T]>>
122
+ ): Promise<unknown>;
123
+
124
+ /** Check if any plugin has a specific hook */
125
+ hasHook(hookName: keyof PluginHooks): boolean;
126
+
127
+ /** Get names of all registered plugins */
128
+ getPluginNames(): string[];
129
+
130
+ /** Clear all plugins (mainly for testing) */
131
+ clear(): void;
132
+ }
133
+
134
+ // Supporting interfaces for hook parameters
135
+
136
+ export interface StartDevelopmentArgs {
137
+ workflow: string;
138
+ commit_behaviour: string;
139
+ require_reviews?: boolean;
140
+ project_path?: string;
141
+ }
142
+
143
+ export interface StartDevelopmentResult {
144
+ conversationId: string;
145
+ planFilePath: string;
146
+ phase: string;
147
+ workflow: string;
148
+ }
149
+
150
+ export interface GeneratedInstructions {
151
+ instructions: string;
152
+ planFilePath: string;
153
+ phase: string;
154
+ }
@@ -0,0 +1,190 @@
1
+ /**
2
+ * Core PluginRegistry implementation for managing plugins and executing lifecycle hooks
3
+ */
4
+
5
+ import type {
6
+ IPlugin,
7
+ IPluginRegistry,
8
+ PluginHooks,
9
+ } from './plugin-interfaces.js';
10
+
11
+ export class PluginRegistry implements IPluginRegistry {
12
+ private plugins: Map<string, IPlugin> = new Map();
13
+
14
+ /**
15
+ * Register a plugin if it's enabled
16
+ */
17
+ registerPlugin(plugin: IPlugin): void {
18
+ if (!plugin.isEnabled()) {
19
+ return;
20
+ }
21
+
22
+ const name = plugin.getName();
23
+ if (this.plugins.has(name)) {
24
+ throw new Error(`Plugin with name '${name}' is already registered`);
25
+ }
26
+
27
+ this.plugins.set(name, plugin);
28
+ }
29
+
30
+ /**
31
+ * Get all enabled plugins sorted by execution sequence
32
+ */
33
+ getEnabledPlugins(): IPlugin[] {
34
+ return Array.from(this.plugins.values())
35
+ .filter(plugin => plugin.isEnabled())
36
+ .sort((a, b) => a.getSequence() - b.getSequence());
37
+ }
38
+
39
+ /**
40
+ * Execute a specific hook on all plugins that implement it
41
+ * Plugins are executed in sequence order
42
+ *
43
+ * Error Handling Strategy:
44
+ * - Validation hooks (beforePhaseTransition): Always re-throw to block invalid transitions
45
+ * - Critical startup hooks: Re-throw to fail fast and show critical errors
46
+ * - Non-critical hooks: Log error and continue execution to enable graceful degradation
47
+ * - Multiple plugins: If one plugin fails on non-critical hook, continue with next plugin
48
+ */
49
+ async executeHook<T extends keyof PluginHooks>(
50
+ hookName: T,
51
+ ...args: Parameters<NonNullable<PluginHooks[T]>>
52
+ ): Promise<unknown> {
53
+ const enabledPlugins = this.getEnabledPlugins();
54
+ let result: unknown = undefined;
55
+
56
+ for (const plugin of enabledPlugins) {
57
+ const hooks = plugin.getHooks();
58
+ const hook = hooks[hookName];
59
+
60
+ if (hook) {
61
+ try {
62
+ // Type-safe hook execution using dispatch pattern
63
+ result = await this.executeTypedHook(hookName, hook, args, result);
64
+ } catch (error) {
65
+ const pluginName = plugin.getName();
66
+ const errorMessage =
67
+ error instanceof Error ? error.message : String(error);
68
+
69
+ // Validation hooks (beforePhaseTransition) should ALWAYS re-throw
70
+ // These are intentional blocking errors, not graceful degradation
71
+ if (hookName === 'beforePhaseTransition') {
72
+ console.error(
73
+ `Plugin '${pluginName}' validation failed for hook '${hookName}':`,
74
+ errorMessage
75
+ );
76
+ throw error;
77
+ }
78
+
79
+ // For non-critical hooks, log the error but continue execution
80
+ // This enables graceful degradation: the app continues even if a plugin hook fails
81
+ console.warn(
82
+ `Plugin '${pluginName}' hook '${hookName}' failed with non-critical error:`,
83
+ errorMessage
84
+ );
85
+ console.warn(
86
+ `Continuing with remaining plugins for hook '${hookName}' (graceful degradation enabled)`
87
+ );
88
+
89
+ // Continue to next plugin for non-critical errors
90
+ // This allows multiple plugins to execute even if one fails
91
+ }
92
+ }
93
+ }
94
+
95
+ return result;
96
+ }
97
+
98
+ /**
99
+ * Type-safe hook execution dispatcher
100
+ * Handles the differences in hook signatures without type coercion
101
+ */
102
+ private async executeTypedHook<T extends keyof PluginHooks>(
103
+ hookName: T,
104
+ hook: NonNullable<PluginHooks[T]>,
105
+ args: Parameters<NonNullable<PluginHooks[T]>>,
106
+ previousResult: unknown
107
+ ): Promise<unknown> {
108
+ if (hookName === 'afterPlanFileCreated') {
109
+ // Content-chaining hook: replaces the content parameter with previous result
110
+ const typedHook = hook as NonNullable<
111
+ PluginHooks['afterPlanFileCreated']
112
+ >;
113
+ const [context, planFilePath, content] = args as Parameters<
114
+ typeof typedHook
115
+ >;
116
+ const contentToUse = ((previousResult as string | undefined) ??
117
+ content) as string;
118
+ return typedHook(context, planFilePath, contentToUse);
119
+ }
120
+
121
+ if (hookName === 'afterInstructionsGenerated') {
122
+ // Content-chaining hook: replaces the instructions parameter with previous result
123
+ const typedHook = hook as NonNullable<
124
+ PluginHooks['afterInstructionsGenerated']
125
+ >;
126
+ const [context, instructions] = args as Parameters<typeof typedHook>;
127
+ const instructionsToUse = (
128
+ previousResult !== undefined
129
+ ? (previousResult as Parameters<typeof typedHook>[1])
130
+ : instructions
131
+ ) as Parameters<typeof typedHook>[1];
132
+ return typedHook(context, instructionsToUse);
133
+ }
134
+
135
+ if (hookName === 'beforeStartDevelopment') {
136
+ const typedHook = hook as NonNullable<
137
+ PluginHooks['beforeStartDevelopment']
138
+ >;
139
+ const [context, startArgs] = args as Parameters<typeof typedHook>;
140
+ return typedHook(context, startArgs);
141
+ }
142
+
143
+ if (hookName === 'afterStartDevelopment') {
144
+ const typedHook = hook as NonNullable<
145
+ PluginHooks['afterStartDevelopment']
146
+ >;
147
+ const [context, startArgs, result] = args as Parameters<typeof typedHook>;
148
+ return typedHook(context, startArgs, result);
149
+ }
150
+
151
+ if (hookName === 'beforePhaseTransition') {
152
+ const typedHook = hook as NonNullable<
153
+ PluginHooks['beforePhaseTransition']
154
+ >;
155
+ const [context, currentPhase, targetPhase] = args as Parameters<
156
+ typeof typedHook
157
+ >;
158
+ return typedHook(context, currentPhase, targetPhase);
159
+ }
160
+
161
+ // This should never be reached due to type system, but ensures exhaustiveness
162
+ const exhaustiveCheck: never = hookName;
163
+ throw new Error(`Unknown hook: ${exhaustiveCheck}`);
164
+ }
165
+
166
+ /**
167
+ * Check if any enabled plugin implements a specific hook
168
+ */
169
+ hasHook(hookName: keyof PluginHooks): boolean {
170
+ const enabledPlugins = this.getEnabledPlugins();
171
+ return enabledPlugins.some(plugin => {
172
+ const hooks = plugin.getHooks();
173
+ return hooks[hookName] !== undefined;
174
+ });
175
+ }
176
+
177
+ /**
178
+ * Get names of all registered plugins (for debugging)
179
+ */
180
+ getPluginNames(): string[] {
181
+ return Array.from(this.plugins.keys());
182
+ }
183
+
184
+ /**
185
+ * Clear all plugins (mainly for testing)
186
+ */
187
+ clear(): void {
188
+ this.plugins.clear();
189
+ }
190
+ }
@@ -34,7 +34,11 @@ import {
34
34
  generateWorkflowDescription,
35
35
  } from './server-helpers.js';
36
36
  import { notificationService } from './notification-service.js';
37
- import { ServerComponentsFactory } from './components/server-components-factory.js';
37
+ import { PlanManager, InstructionGenerator } from '@codemcp/workflows-core';
38
+ import { PluginRegistry } from './plugin-system/plugin-registry.js';
39
+ import { BeadsPlugin } from './plugin-system/beads-plugin.js';
40
+ import { BeadsPlanManager } from './components/beads/beads-plan-manager.js';
41
+ import { BeadsInstructionGenerator } from './components/beads/beads-instruction-generator.js';
38
42
 
39
43
  const logger = createLogger('ServerConfig');
40
44
 
@@ -113,18 +117,35 @@ export async function initializeServerComponents(
113
117
  const transitionEngine = new TransitionEngine(projectPath);
114
118
  transitionEngine.setConversationManager(conversationManager);
115
119
 
116
- // Use factory pattern for strategy-based component creation
117
- const componentsFactory = new ServerComponentsFactory({
118
- projectPath,
119
- taskBackend: config.taskBackend, // Pass through task backend config if provided
120
- });
121
- const planManager = componentsFactory.createPlanManager();
122
- const instructionGenerator = componentsFactory.createInstructionGenerator();
120
+ // Use beads components if TASK_BACKEND=beads, otherwise use defaults
121
+ // This enables beads features like plan file markers and beads CLI instructions
122
+ const isBeadsBackend = process.env.TASK_BACKEND === 'beads';
123
+ const planManager = isBeadsBackend
124
+ ? new BeadsPlanManager()
125
+ : new PlanManager();
126
+ const instructionGenerator = isBeadsBackend
127
+ ? new BeadsInstructionGenerator()
128
+ : new InstructionGenerator(planManager as InstanceType<typeof PlanManager>);
123
129
 
124
130
  // Always create interaction logger as it's critical for transition engine logic
125
131
  // (determining first call from initial state)
126
132
  const interactionLogger = new InteractionLogger(database);
127
133
 
134
+ // Initialize plugin registry and register plugins
135
+ const pluginRegistry = new PluginRegistry();
136
+
137
+ // Register BeadsPlugin if beads backend is configured
138
+ if (process.env.TASK_BACKEND === 'beads') {
139
+ const beadsPlugin = new BeadsPlugin({ projectPath });
140
+ if (beadsPlugin.isEnabled()) {
141
+ pluginRegistry.registerPlugin(beadsPlugin);
142
+ logger.info('BeadsPlugin registered successfully', {
143
+ enabled: beadsPlugin.isEnabled(),
144
+ sequence: beadsPlugin.getSequence(),
145
+ });
146
+ }
147
+ }
148
+
128
149
  // Create server context
129
150
  const context: ServerContext = {
130
151
  conversationManager,
@@ -134,6 +155,7 @@ export async function initializeServerComponents(
134
155
  workflowManager,
135
156
  interactionLogger,
136
157
  projectPath,
158
+ pluginRegistry,
137
159
  };
138
160
 
139
161
  // Initialize database
@@ -143,8 +143,7 @@ export class ConductReviewHandler extends ConversationRequiredToolHandler<
143
143
  perspectives: Array<{ perspective: string; prompt: string }>,
144
144
  conversationContext: ConversationContext
145
145
  ): Promise<ConductReviewResult> {
146
- // TODO: Implement automated review when sampling tools are available
147
- // For now, fall back to guided instructions
146
+ // Falls back to guided instructions until automated review is implemented
148
147
  return this.generateReviewInstructions(
149
148
  perspectives,
150
149
  conversationContext.currentPhase,
@@ -8,8 +8,6 @@
8
8
  import { ConversationRequiredToolHandler } from './base-tool-handler.js';
9
9
  import { validateRequiredArgs } from '../server-helpers.js';
10
10
  import type { ConversationContext } from '@codemcp/workflows-core';
11
- import { BeadsStateManager } from '@codemcp/workflows-core';
12
- import { ServerComponentsFactory } from '../components/server-components-factory.js';
13
11
  import { ServerContext } from '../types.js';
14
12
 
15
13
  /**
@@ -80,13 +78,26 @@ export class ProceedToPhaseHandler extends ConversationRequiredToolHandler<
80
78
  context
81
79
  );
82
80
 
83
- // Validate beads task completion if in beads mode
84
- await this.validateBeadsTaskCompletion(
81
+ // Execute plugin hooks before phase transition (replaces if-statement pattern)
82
+ const pluginContext = {
85
83
  conversationId,
84
+ planFilePath: conversationContext.planFilePath,
86
85
  currentPhase,
87
- target_phase,
88
- conversationContext.projectPath
89
- );
86
+ workflow: conversationContext.workflowName,
87
+ projectPath: conversationContext.projectPath,
88
+ gitBranch: conversationContext.gitBranch,
89
+ targetPhase: target_phase,
90
+ };
91
+
92
+ // Execute plugin hooks safely - guard against missing plugin registry
93
+ if (context.pluginRegistry) {
94
+ await context.pluginRegistry.executeHook(
95
+ 'beforePhaseTransition',
96
+ pluginContext,
97
+ currentPhase,
98
+ target_phase
99
+ );
100
+ }
90
101
 
91
102
  // Ensure state machine is loaded for this project
92
103
  this.ensureStateMachineForProject(context, conversationContext.projectPath);
@@ -136,6 +147,7 @@ export class ProceedToPhaseHandler extends ConversationRequiredToolHandler<
136
147
  transitionReason: transitionResult.transitionReason,
137
148
  isModeled: transitionResult.isModeled,
138
149
  planFileExists: planInfo.exists,
150
+ instructionSource: 'proceed_to_phase',
139
151
  }
140
152
  );
141
153
 
@@ -295,132 +307,4 @@ export class ProceedToPhaseHandler extends ConversationRequiredToolHandler<
295
307
  }
296
308
  }
297
309
  }
298
-
299
- /**
300
- * Validate that all beads tasks in the current phase are completed
301
- * before proceeding to the next phase. Only applies when beads mode is active.
302
- */
303
- private async validateBeadsTaskCompletion(
304
- conversationId: string,
305
- currentPhase: string,
306
- targetPhase: string,
307
- projectPath: string
308
- ): Promise<void> {
309
- try {
310
- // Use factory to create task backend client (strategy pattern)
311
- const factory = new ServerComponentsFactory({ projectPath });
312
- const taskBackendClient = factory.createTaskBackendClient();
313
-
314
- if (!taskBackendClient) {
315
- // Not in beads mode or beads not available, skip validation
316
- this.logger.debug(
317
- 'Skipping beads task validation - no task backend client available',
318
- {
319
- conversationId,
320
- currentPhase,
321
- targetPhase,
322
- }
323
- );
324
- return;
325
- }
326
-
327
- // Get beads state for this conversation
328
- const beadsStateManager = new BeadsStateManager(projectPath);
329
- const currentPhaseTaskId = await beadsStateManager.getPhaseTaskId(
330
- conversationId,
331
- currentPhase
332
- );
333
-
334
- if (!currentPhaseTaskId) {
335
- // No beads state found for this conversation - fallback to graceful handling
336
- this.logger.debug('No beads phase task ID found for current phase', {
337
- conversationId,
338
- currentPhase,
339
- targetPhase,
340
- projectPath,
341
- });
342
- return;
343
- }
344
-
345
- this.logger.debug(
346
- 'Checking for incomplete beads tasks using task backend client',
347
- {
348
- conversationId,
349
- currentPhase,
350
- currentPhaseTaskId,
351
- }
352
- );
353
-
354
- // Use task backend client to validate task completion (strategy pattern)
355
- const validationResult =
356
- await taskBackendClient.validateTasksCompleted(currentPhaseTaskId);
357
-
358
- if (!validationResult.valid) {
359
- // Get the incomplete tasks from the validation result
360
- const incompleteTasks = validationResult.openTasks;
361
-
362
- // Create detailed error message with incomplete tasks
363
- const taskDetails = incompleteTasks
364
- .map(task => ` • ${task.id} - ${task.title || 'Untitled task'}`)
365
- .join('\n');
366
-
367
- const errorMessage = `Cannot proceed to ${targetPhase} - ${incompleteTasks.length} incomplete task(s) in current phase "${currentPhase}":
368
-
369
- ${taskDetails}
370
-
371
- To proceed, check the in-progress-tasks using:
372
-
373
- bd list --parent ${currentPhaseTaskId} --status open
374
-
375
- You can also defer tasks if they're no longer needed:
376
- bd defer <task-id> --until tomorrow`;
377
-
378
- this.logger.info(
379
- 'Blocking phase transition due to incomplete beads tasks',
380
- {
381
- conversationId,
382
- currentPhase,
383
- targetPhase,
384
- currentPhaseTaskId,
385
- incompleteTaskCount: incompleteTasks.length,
386
- incompleteTaskIds: incompleteTasks.map(t => t.id),
387
- }
388
- );
389
-
390
- throw new Error(errorMessage);
391
- }
392
-
393
- this.logger.info(
394
- 'All beads tasks completed in current phase, allowing transition',
395
- {
396
- conversationId,
397
- currentPhase,
398
- targetPhase,
399
- currentPhaseTaskId,
400
- }
401
- );
402
- } catch (error) {
403
- // Re-throw validation errors (incomplete tasks)
404
- if (
405
- error instanceof Error &&
406
- error.message.includes('Cannot proceed to')
407
- ) {
408
- throw error;
409
- }
410
-
411
- // Log other errors but allow transition (graceful degradation)
412
- const errorMessage =
413
- error instanceof Error ? error.message : String(error);
414
- this.logger.warn(
415
- 'Beads task validation failed, allowing transition to proceed',
416
- {
417
- error: errorMessage,
418
- conversationId,
419
- currentPhase,
420
- targetPhase,
421
- projectPath,
422
- }
423
- );
424
- }
425
- }
426
310
  }