@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.
- package/.turbo/turbo-build.log +1 -1
- package/dist/components/beads/beads-instruction-generator.d.ts +3 -4
- package/dist/components/beads/beads-instruction-generator.d.ts.map +1 -1
- package/dist/components/beads/beads-instruction-generator.js +12 -7
- package/dist/components/beads/beads-instruction-generator.js.map +1 -1
- package/dist/components/beads/beads-task-backend-client.d.ts.map +1 -1
- package/dist/components/beads/beads-task-backend-client.js +1 -4
- package/dist/components/beads/beads-task-backend-client.js.map +1 -1
- package/dist/plugin-system/beads-plugin.d.ts +70 -0
- package/dist/plugin-system/beads-plugin.d.ts.map +1 -0
- package/dist/plugin-system/beads-plugin.js +459 -0
- package/dist/plugin-system/beads-plugin.js.map +1 -0
- package/dist/plugin-system/index.d.ts +9 -0
- package/dist/plugin-system/index.d.ts.map +1 -0
- package/dist/plugin-system/index.js +9 -0
- package/dist/plugin-system/index.js.map +1 -0
- package/dist/plugin-system/plugin-interfaces.d.ts +99 -0
- package/dist/plugin-system/plugin-interfaces.d.ts.map +1 -0
- package/dist/plugin-system/plugin-interfaces.js +9 -0
- package/dist/plugin-system/plugin-interfaces.js.map +1 -0
- package/dist/plugin-system/plugin-registry.d.ts +44 -0
- package/dist/plugin-system/plugin-registry.d.ts.map +1 -0
- package/dist/plugin-system/plugin-registry.js +132 -0
- package/dist/plugin-system/plugin-registry.js.map +1 -0
- package/dist/server-config.d.ts.map +1 -1
- package/dist/server-config.js +28 -8
- package/dist/server-config.js.map +1 -1
- package/dist/tool-handlers/conduct-review.d.ts.map +1 -1
- package/dist/tool-handlers/conduct-review.js +1 -2
- package/dist/tool-handlers/conduct-review.js.map +1 -1
- package/dist/tool-handlers/proceed-to-phase.d.ts +0 -5
- package/dist/tool-handlers/proceed-to-phase.d.ts.map +1 -1
- package/dist/tool-handlers/proceed-to-phase.js +15 -93
- package/dist/tool-handlers/proceed-to-phase.js.map +1 -1
- package/dist/tool-handlers/start-development.d.ts +0 -13
- package/dist/tool-handlers/start-development.d.ts.map +1 -1
- package/dist/tool-handlers/start-development.js +29 -124
- package/dist/tool-handlers/start-development.js.map +1 -1
- package/dist/tool-handlers/whats-next.d.ts.map +1 -1
- package/dist/tool-handlers/whats-next.js +1 -0
- package/dist/tool-handlers/whats-next.js.map +1 -1
- package/dist/types.d.ts +2 -0
- package/dist/types.d.ts.map +1 -1
- package/package.json +2 -2
- package/src/components/beads/beads-instruction-generator.ts +12 -12
- package/src/components/beads/beads-task-backend-client.ts +1 -4
- package/src/plugin-system/beads-plugin.ts +641 -0
- package/src/plugin-system/index.ts +20 -0
- package/src/plugin-system/plugin-interfaces.ts +154 -0
- package/src/plugin-system/plugin-registry.ts +190 -0
- package/src/server-config.ts +30 -8
- package/src/tool-handlers/conduct-review.ts +1 -2
- package/src/tool-handlers/proceed-to-phase.ts +19 -135
- package/src/tool-handlers/start-development.ts +35 -205
- package/src/tool-handlers/whats-next.ts +1 -0
- package/src/types.ts +2 -0
- package/test/e2e/beads-plugin-integration.test.ts +1609 -0
- package/test/e2e/plugin-system-integration.test.ts +1729 -0
- package/test/unit/beads-plugin-behavioral.test.ts +512 -0
- package/test/unit/beads-plugin.test.ts +94 -0
- package/test/unit/plugin-error-handling.test.ts +240 -0
- package/test/unit/proceed-to-phase-plugin-integration.test.ts +150 -0
- package/test/unit/server-config-plugin-registry.test.ts +81 -0
- package/test/unit/start-development-goal-extraction.test.ts +22 -16
- package/test/utils/test-helpers.ts +3 -1
- package/tsconfig.build.tsbuildinfo +1 -1
- package/dist/components/server-components-factory.d.ts +0 -39
- package/dist/components/server-components-factory.d.ts.map +0 -1
- package/dist/components/server-components-factory.js +0 -62
- package/dist/components/server-components-factory.js.map +0 -1
- package/src/components/server-components-factory.ts +0 -86
- package/test/e2e/component-substitution.test.ts +0 -208
- package/test/unit/beads-integration-filename.test.ts +0 -93
- 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
|
+
}
|
package/src/server-config.ts
CHANGED
|
@@ -34,7 +34,11 @@ import {
|
|
|
34
34
|
generateWorkflowDescription,
|
|
35
35
|
} from './server-helpers.js';
|
|
36
36
|
import { notificationService } from './notification-service.js';
|
|
37
|
-
import {
|
|
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
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
const instructionGenerator =
|
|
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
|
-
//
|
|
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
|
-
//
|
|
84
|
-
|
|
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
|
-
|
|
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
|
}
|