@codemcp/workflows 4.7.0 → 4.8.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.
- package/.turbo/turbo-build.log +1 -1
- package/dist/components/beads/beads-instruction-generator.d.ts +48 -0
- package/dist/components/beads/beads-instruction-generator.d.ts.map +1 -0
- package/dist/components/beads/beads-instruction-generator.js +182 -0
- package/dist/components/beads/beads-instruction-generator.js.map +1 -0
- package/dist/components/beads/beads-plan-manager.d.ts +66 -0
- package/dist/components/beads/beads-plan-manager.d.ts.map +1 -0
- package/dist/components/beads/beads-plan-manager.js +288 -0
- package/dist/components/beads/beads-plan-manager.js.map +1 -0
- package/dist/components/beads/beads-task-backend-client.d.ts +43 -0
- package/dist/components/beads/beads-task-backend-client.d.ts.map +1 -0
- package/dist/components/beads/beads-task-backend-client.js +178 -0
- package/dist/components/beads/beads-task-backend-client.js.map +1 -0
- package/dist/components/server-components-factory.d.ts +39 -0
- package/dist/components/server-components-factory.d.ts.map +1 -0
- package/dist/components/server-components-factory.js +62 -0
- package/dist/components/server-components-factory.js.map +1 -0
- package/dist/server-config.d.ts.map +1 -1
- package/dist/server-config.js +8 -4
- package/dist/server-config.js.map +1 -1
- package/dist/server-implementation.d.ts +1 -1
- package/dist/tool-handlers/proceed-to-phase.d.ts +5 -0
- package/dist/tool-handlers/proceed-to-phase.d.ts.map +1 -1
- package/dist/tool-handlers/proceed-to-phase.js +95 -0
- package/dist/tool-handlers/proceed-to-phase.js.map +1 -1
- package/dist/tool-handlers/start-development.d.ts.map +1 -1
- package/dist/tool-handlers/start-development.js +7 -3
- package/dist/tool-handlers/start-development.js.map +1 -1
- package/dist/tool-handlers/whats-next.d.ts +0 -12
- package/dist/tool-handlers/whats-next.d.ts.map +1 -1
- package/dist/tool-handlers/whats-next.js +1 -88
- package/dist/tool-handlers/whats-next.js.map +1 -1
- package/dist/types.d.ts +7 -4
- package/dist/types.d.ts.map +1 -1
- package/package.json +2 -2
- package/src/components/beads/beads-instruction-generator.ts +261 -0
- package/src/components/beads/beads-plan-manager.ts +358 -0
- package/src/components/beads/beads-task-backend-client.ts +232 -0
- package/src/components/server-components-factory.ts +86 -0
- package/src/server-config.ts +9 -4
- package/src/tool-handlers/proceed-to-phase.ts +140 -0
- package/src/tool-handlers/start-development.ts +14 -3
- package/src/tool-handlers/whats-next.ts +4 -117
- package/src/types.ts +7 -4
- package/test/e2e/component-substitution.test.ts +208 -0
- package/test/unit/beads-instruction-generator.test.ts +847 -0
- package/test/unit/beads-phase-task-id-integration.test.ts +557 -0
- package/test/unit/server-components-factory.test.ts +279 -0
- package/test/unit/setup-project-docs-handler.test.ts +3 -2
- package/test/utils/e2e-test-setup.ts +0 -1
- package/test/utils/temp-files.ts +12 -0
- package/tsconfig.build.tsbuildinfo +1 -1
|
@@ -0,0 +1,358 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Beads Plan Manager
|
|
3
|
+
*
|
|
4
|
+
* Beads-specific implementation of IPlanManager.
|
|
5
|
+
* Manages plan files optimized for beads task management workflow.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import {
|
|
9
|
+
type IPlanManager,
|
|
10
|
+
type PlanFileInfo,
|
|
11
|
+
type YamlStateMachine,
|
|
12
|
+
type TaskBackendConfig,
|
|
13
|
+
createLogger,
|
|
14
|
+
} from '@codemcp/workflows-core';
|
|
15
|
+
import { writeFile, readFile, access } from 'node:fs/promises';
|
|
16
|
+
import { dirname } from 'node:path';
|
|
17
|
+
import { mkdir } from 'node:fs/promises';
|
|
18
|
+
|
|
19
|
+
const logger = createLogger('BeadsPlanManager');
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Beads-specific plan manager implementation
|
|
23
|
+
*/
|
|
24
|
+
export class BeadsPlanManager implements IPlanManager {
|
|
25
|
+
private stateMachine: YamlStateMachine | null = null;
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Set the state machine definition for dynamic plan generation
|
|
29
|
+
*/
|
|
30
|
+
setStateMachine(stateMachine: YamlStateMachine): void {
|
|
31
|
+
this.stateMachine = stateMachine;
|
|
32
|
+
logger.debug('State machine set for beads plan manager', {
|
|
33
|
+
name: stateMachine.name,
|
|
34
|
+
phases: Object.keys(stateMachine.states),
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Set the task backend configuration
|
|
40
|
+
*/
|
|
41
|
+
setTaskBackend(taskBackend: TaskBackendConfig): void {
|
|
42
|
+
// Task backend is implicit for beads plan manager (always beads)
|
|
43
|
+
logger.debug('Task backend set for beads plan manager', {
|
|
44
|
+
backend: taskBackend.backend,
|
|
45
|
+
available: taskBackend.isAvailable,
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Get plan file information
|
|
51
|
+
*/
|
|
52
|
+
async getPlanFileInfo(planFilePath: string): Promise<PlanFileInfo> {
|
|
53
|
+
try {
|
|
54
|
+
await access(planFilePath);
|
|
55
|
+
const content = await readFile(planFilePath, 'utf-8');
|
|
56
|
+
return {
|
|
57
|
+
path: planFilePath,
|
|
58
|
+
exists: true,
|
|
59
|
+
content,
|
|
60
|
+
};
|
|
61
|
+
} catch (_error) {
|
|
62
|
+
return {
|
|
63
|
+
path: planFilePath,
|
|
64
|
+
exists: false,
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Create initial plan file if it doesn't exist
|
|
71
|
+
*/
|
|
72
|
+
async ensurePlanFile(
|
|
73
|
+
planFilePath: string,
|
|
74
|
+
projectPath: string,
|
|
75
|
+
gitBranch: string
|
|
76
|
+
): Promise<void> {
|
|
77
|
+
logger.debug('Ensuring beads plan file exists', {
|
|
78
|
+
planFilePath,
|
|
79
|
+
projectPath,
|
|
80
|
+
gitBranch,
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
const planInfo = await this.getPlanFileInfo(planFilePath);
|
|
84
|
+
|
|
85
|
+
if (!planInfo.exists) {
|
|
86
|
+
logger.info('Plan file not found, creating beads-optimized plan', {
|
|
87
|
+
planFilePath,
|
|
88
|
+
});
|
|
89
|
+
await this.createInitialBeadsPlanFile(
|
|
90
|
+
planFilePath,
|
|
91
|
+
projectPath,
|
|
92
|
+
gitBranch
|
|
93
|
+
);
|
|
94
|
+
logger.info('Beads plan file created successfully', { planFilePath });
|
|
95
|
+
} else {
|
|
96
|
+
logger.debug('Plan file already exists', { planFilePath });
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Create initial plan file optimized for beads workflow
|
|
102
|
+
*/
|
|
103
|
+
private async createInitialBeadsPlanFile(
|
|
104
|
+
planFilePath: string,
|
|
105
|
+
projectPath: string,
|
|
106
|
+
gitBranch: string
|
|
107
|
+
): Promise<void> {
|
|
108
|
+
logger.debug('Creating beads-optimized plan file', { planFilePath });
|
|
109
|
+
|
|
110
|
+
try {
|
|
111
|
+
// Ensure directory exists
|
|
112
|
+
await mkdir(dirname(planFilePath), { recursive: true });
|
|
113
|
+
logger.debug('Plan file directory ensured', {
|
|
114
|
+
directory: dirname(planFilePath),
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
const projectName = projectPath.split('/').pop() || 'Unknown Project';
|
|
118
|
+
const branchInfo = gitBranch !== 'no-git' ? ` (${gitBranch} branch)` : '';
|
|
119
|
+
|
|
120
|
+
const initialContent = this.generateBeadsInitialPlanContent(
|
|
121
|
+
projectName,
|
|
122
|
+
branchInfo
|
|
123
|
+
);
|
|
124
|
+
|
|
125
|
+
await writeFile(planFilePath, initialContent, 'utf-8');
|
|
126
|
+
logger.info('Beads plan file written successfully', {
|
|
127
|
+
planFilePath,
|
|
128
|
+
contentLength: initialContent.length,
|
|
129
|
+
projectName,
|
|
130
|
+
});
|
|
131
|
+
} catch (error) {
|
|
132
|
+
logger.error('Failed to create beads plan file', error as Error, {
|
|
133
|
+
planFilePath,
|
|
134
|
+
});
|
|
135
|
+
throw error;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Generate initial plan file content optimized for beads workflow
|
|
141
|
+
*/
|
|
142
|
+
private generateBeadsInitialPlanContent(
|
|
143
|
+
projectName: string,
|
|
144
|
+
branchInfo: string
|
|
145
|
+
): string {
|
|
146
|
+
const timestamp = new Date().toISOString().split('T')[0];
|
|
147
|
+
|
|
148
|
+
if (!this.stateMachine) {
|
|
149
|
+
throw new Error(
|
|
150
|
+
'State machine not set. This should not happen as state machine is always loaded.'
|
|
151
|
+
);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
const phases = Object.keys(this.stateMachine.states);
|
|
155
|
+
const initialPhase = this.stateMachine.initial_state;
|
|
156
|
+
|
|
157
|
+
const documentationUrl = this.generateWorkflowDocumentationUrl(
|
|
158
|
+
this.stateMachine.name
|
|
159
|
+
);
|
|
160
|
+
|
|
161
|
+
let content = `# Development Plan: ${projectName}${branchInfo}
|
|
162
|
+
|
|
163
|
+
*Generated on ${timestamp} by Vibe Feature MCP*
|
|
164
|
+
*Workflow: ${
|
|
165
|
+
documentationUrl
|
|
166
|
+
? '[' + this.stateMachine.name + ']' + '(' + documentationUrl + ')'
|
|
167
|
+
: this.stateMachine.name
|
|
168
|
+
}*
|
|
169
|
+
*Task Management: Beads Issue Tracker*
|
|
170
|
+
|
|
171
|
+
## Goal
|
|
172
|
+
*Define what you're building or fixing - this will be updated as requirements are gathered*
|
|
173
|
+
|
|
174
|
+
## ${this.capitalizePhase(initialPhase)}
|
|
175
|
+
<!-- beads-phase-id: TBD -->
|
|
176
|
+
### Tasks
|
|
177
|
+
|
|
178
|
+
**🔧 TASK MANAGEMENT VIA CLI TOOL bd**
|
|
179
|
+
|
|
180
|
+
Tasks are managed via bd CLI tool. Use bd commands to create and manage tasks with proper hierarchy:
|
|
181
|
+
|
|
182
|
+
- \`bd list --parent <phase-task-id> --status open\`
|
|
183
|
+
- \`bd create "Task title" --parent <phase-task-id> -p 2\`
|
|
184
|
+
- \`bd close <task-id>\`
|
|
185
|
+
|
|
186
|
+
**Never use [ ] or [x] checkboxes - use bd commands only!**
|
|
187
|
+
|
|
188
|
+
### Completed
|
|
189
|
+
- [x] Created development plan file
|
|
190
|
+
|
|
191
|
+
`;
|
|
192
|
+
|
|
193
|
+
// Generate sections for each phase with beads-specific guidance
|
|
194
|
+
for (const phase of phases) {
|
|
195
|
+
if (phase !== initialPhase) {
|
|
196
|
+
content += `## ${this.capitalizePhase(phase)}
|
|
197
|
+
<!-- beads-phase-id: TBD -->
|
|
198
|
+
### Tasks
|
|
199
|
+
|
|
200
|
+
**🔧 TASK MANAGEMENT VIA CLI TOOL bd**
|
|
201
|
+
|
|
202
|
+
Tasks are managed via bd CLI tool. Use bd commands to create and manage tasks with proper hierarchy:
|
|
203
|
+
|
|
204
|
+
- \`bd list --parent <phase-task-id> --status open\`
|
|
205
|
+
- \`bd create "Task title" --parent <phase-task-id> -p 2\`
|
|
206
|
+
- \`bd close <task-id>\`
|
|
207
|
+
|
|
208
|
+
**Never use [ ] or [x] checkboxes - use bd commands only!**
|
|
209
|
+
|
|
210
|
+
### Completed
|
|
211
|
+
*None yet*
|
|
212
|
+
|
|
213
|
+
`;
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
content += `## Key Decisions
|
|
218
|
+
*Important decisions will be documented here as they are made*
|
|
219
|
+
|
|
220
|
+
## Notes
|
|
221
|
+
*Additional context and observations*
|
|
222
|
+
|
|
223
|
+
---
|
|
224
|
+
*This plan is maintained by the LLM and uses beads CLI for task management. Tool responses provide guidance on which bd commands to use for task management.*
|
|
225
|
+
`;
|
|
226
|
+
|
|
227
|
+
return content;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* Update plan file with new content
|
|
232
|
+
*/
|
|
233
|
+
async updatePlanFile(planFilePath: string, content: string): Promise<void> {
|
|
234
|
+
// Ensure directory exists
|
|
235
|
+
await mkdir(dirname(planFilePath), { recursive: true });
|
|
236
|
+
|
|
237
|
+
await writeFile(planFilePath, content, 'utf-8');
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* Get plan file content for LLM context
|
|
242
|
+
*/
|
|
243
|
+
async getPlanFileContent(planFilePath: string): Promise<string> {
|
|
244
|
+
const planInfo = await this.getPlanFileInfo(planFilePath);
|
|
245
|
+
|
|
246
|
+
if (!planInfo.exists) {
|
|
247
|
+
return 'Plan file does not exist yet. It will be created when the LLM updates it.';
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
return planInfo.content || '';
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/**
|
|
254
|
+
* Generate phase-specific plan file guidance optimized for beads
|
|
255
|
+
*/
|
|
256
|
+
generatePlanFileGuidance(phase: string): string {
|
|
257
|
+
if (!this.stateMachine) {
|
|
258
|
+
throw new Error(
|
|
259
|
+
'State machine not set. This should not happen as state machine is always loaded.'
|
|
260
|
+
);
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
const phaseDefinition = this.stateMachine.states[phase];
|
|
264
|
+
if (!phaseDefinition) {
|
|
265
|
+
logger.warn('Unknown phase for beads plan file guidance', { phase });
|
|
266
|
+
return `Update the ${this.capitalizePhase(phase)} section with current progress. Use bd CLI for all task management.`;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
const capitalizedPhase = this.capitalizePhase(phase);
|
|
270
|
+
|
|
271
|
+
return `Update the ${capitalizedPhase} section with progress. Use bd CLI exclusively for task management - never use checkboxes. Document important decisions in the Key Decisions section.`;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
/**
|
|
275
|
+
* Delete plan file
|
|
276
|
+
*/
|
|
277
|
+
async deletePlanFile(planFilePath: string): Promise<boolean> {
|
|
278
|
+
logger.debug('Deleting beads plan file', { planFilePath });
|
|
279
|
+
|
|
280
|
+
try {
|
|
281
|
+
// Check if file exists first
|
|
282
|
+
await access(planFilePath);
|
|
283
|
+
|
|
284
|
+
// Import unlink dynamically to avoid issues
|
|
285
|
+
const { unlink } = await import('node:fs/promises');
|
|
286
|
+
await unlink(planFilePath);
|
|
287
|
+
|
|
288
|
+
logger.info('Beads plan file deleted successfully', { planFilePath });
|
|
289
|
+
return true;
|
|
290
|
+
} catch (error: unknown) {
|
|
291
|
+
if ((error as NodeJS.ErrnoException).code === 'ENOENT') {
|
|
292
|
+
logger.debug('Beads plan file does not exist, nothing to delete', {
|
|
293
|
+
planFilePath,
|
|
294
|
+
});
|
|
295
|
+
return true; // Consider it successful if file doesn't exist
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
logger.error('Failed to delete beads plan file', error as Error, {
|
|
299
|
+
planFilePath,
|
|
300
|
+
});
|
|
301
|
+
throw error;
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
/**
|
|
306
|
+
* Ensure plan file is deleted (verify deletion)
|
|
307
|
+
*/
|
|
308
|
+
async ensurePlanFileDeleted(planFilePath: string): Promise<boolean> {
|
|
309
|
+
logger.debug('Ensuring beads plan file is deleted', { planFilePath });
|
|
310
|
+
|
|
311
|
+
try {
|
|
312
|
+
await access(planFilePath);
|
|
313
|
+
// If we reach here, file still exists
|
|
314
|
+
logger.warn('Beads plan file still exists after deletion attempt', {
|
|
315
|
+
planFilePath,
|
|
316
|
+
});
|
|
317
|
+
return false;
|
|
318
|
+
} catch (error: unknown) {
|
|
319
|
+
if ((error as NodeJS.ErrnoException).code === 'ENOENT') {
|
|
320
|
+
logger.debug('Beads plan file successfully deleted (does not exist)', {
|
|
321
|
+
planFilePath,
|
|
322
|
+
});
|
|
323
|
+
return true;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
// Some other error occurred
|
|
327
|
+
logger.error('Error checking beads plan file deletion', error as Error, {
|
|
328
|
+
planFilePath,
|
|
329
|
+
});
|
|
330
|
+
throw error;
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
/**
|
|
335
|
+
* Capitalize phase name for display
|
|
336
|
+
*/
|
|
337
|
+
private capitalizePhase(phase: string): string {
|
|
338
|
+
return phase
|
|
339
|
+
.split('_')
|
|
340
|
+
.map(word => word.charAt(0).toUpperCase() + word.slice(1))
|
|
341
|
+
.join(' ');
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
/**
|
|
345
|
+
* Generate workflow documentation URL for predefined workflows
|
|
346
|
+
*/
|
|
347
|
+
private generateWorkflowDocumentationUrl(
|
|
348
|
+
workflowName: string
|
|
349
|
+
): string | undefined {
|
|
350
|
+
// Don't generate URL for custom workflows
|
|
351
|
+
if (workflowName === 'custom') {
|
|
352
|
+
return undefined;
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
// Generate URL for predefined workflows
|
|
356
|
+
return `https://mrsimpson.github.io/responsible-vibe-mcp/workflows/${workflowName}`;
|
|
357
|
+
}
|
|
358
|
+
}
|
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Beads Task Backend Client
|
|
3
|
+
*
|
|
4
|
+
* Implementation of ITaskBackendClient for beads task management system.
|
|
5
|
+
* Handles CLI operations and task validation for beads backend.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import {
|
|
9
|
+
type ITaskBackendClient,
|
|
10
|
+
type BackendTask,
|
|
11
|
+
type TaskValidationResult,
|
|
12
|
+
} from '@codemcp/workflows-core';
|
|
13
|
+
import { execSync } from 'node:child_process';
|
|
14
|
+
import { createLogger } from '@codemcp/workflows-core';
|
|
15
|
+
|
|
16
|
+
const logger = createLogger('BeadsTaskBackendClient');
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Beads-specific implementation of task backend client
|
|
20
|
+
*/
|
|
21
|
+
export class BeadsTaskBackendClient implements ITaskBackendClient {
|
|
22
|
+
private projectPath: string;
|
|
23
|
+
|
|
24
|
+
constructor(projectPath: string) {
|
|
25
|
+
this.projectPath = projectPath;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Execute a beads command safely
|
|
30
|
+
*/
|
|
31
|
+
private async executeBeadsCommand(
|
|
32
|
+
args: string[]
|
|
33
|
+
): Promise<{ success: boolean; stdout?: string; stderr?: string }> {
|
|
34
|
+
try {
|
|
35
|
+
const command = `bd ${args.join(' ')}`;
|
|
36
|
+
logger.debug('Executing beads command', {
|
|
37
|
+
command,
|
|
38
|
+
projectPath: this.projectPath,
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
const stdout = execSync(`bd ${args.join(' ')}`, {
|
|
42
|
+
cwd: this.projectPath,
|
|
43
|
+
encoding: 'utf-8',
|
|
44
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
return { success: true, stdout };
|
|
48
|
+
} catch (error: unknown) {
|
|
49
|
+
const execError = error as {
|
|
50
|
+
stderr?: string;
|
|
51
|
+
stdout?: string;
|
|
52
|
+
status?: number;
|
|
53
|
+
};
|
|
54
|
+
logger.warn('Beads command failed', {
|
|
55
|
+
args,
|
|
56
|
+
error: error instanceof Error ? error.message : String(error),
|
|
57
|
+
stderr: execError.stderr,
|
|
58
|
+
stdout: execError.stdout,
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
return {
|
|
62
|
+
success: false,
|
|
63
|
+
stderr:
|
|
64
|
+
execError.stderr ||
|
|
65
|
+
(error instanceof Error ? error.message : String(error)),
|
|
66
|
+
stdout: execError.stdout,
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Check if beads backend is available
|
|
73
|
+
*/
|
|
74
|
+
async isAvailable(): Promise<boolean> {
|
|
75
|
+
try {
|
|
76
|
+
const result = await this.executeBeadsCommand(['--version']);
|
|
77
|
+
return result.success;
|
|
78
|
+
} catch (_error) {
|
|
79
|
+
return false;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Get all open tasks for a given parent task
|
|
85
|
+
*/
|
|
86
|
+
async getOpenTasks(parentTaskId: string): Promise<BackendTask[]> {
|
|
87
|
+
try {
|
|
88
|
+
const result = await this.executeBeadsCommand([
|
|
89
|
+
'list',
|
|
90
|
+
'--parent',
|
|
91
|
+
parentTaskId,
|
|
92
|
+
'--status',
|
|
93
|
+
'open',
|
|
94
|
+
]);
|
|
95
|
+
|
|
96
|
+
if (!result.success || !result.stdout) {
|
|
97
|
+
return [];
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// Parse text output from beads CLI (since JSON format may not be available)
|
|
101
|
+
// This is a simplified parser - would need to be adjusted based on actual beads output format
|
|
102
|
+
const lines = result.stdout.trim().split('\n');
|
|
103
|
+
const tasks: BackendTask[] = [];
|
|
104
|
+
|
|
105
|
+
for (const line of lines) {
|
|
106
|
+
if (
|
|
107
|
+
line.trim() &&
|
|
108
|
+
!line.startsWith('○') &&
|
|
109
|
+
!line.startsWith('●') &&
|
|
110
|
+
!line.includes('Tip:')
|
|
111
|
+
) {
|
|
112
|
+
// Extract task ID and title from beads output format
|
|
113
|
+
// This is based on observed beads CLI output format
|
|
114
|
+
const match = line.match(/^○?\s*([^\s]+)\s+.*?\s+-\s+(.+)$/);
|
|
115
|
+
if (match) {
|
|
116
|
+
tasks.push({
|
|
117
|
+
id: match[1] || '',
|
|
118
|
+
title: match[2] || '',
|
|
119
|
+
status: 'open',
|
|
120
|
+
priority: 2,
|
|
121
|
+
parent: parentTaskId,
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
return tasks;
|
|
128
|
+
} catch (_error) {
|
|
129
|
+
return [];
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Validate that all tasks under a parent are completed
|
|
135
|
+
*/
|
|
136
|
+
async validateTasksCompleted(
|
|
137
|
+
parentTaskId: string
|
|
138
|
+
): Promise<TaskValidationResult> {
|
|
139
|
+
const openTasks = await this.getOpenTasks(parentTaskId);
|
|
140
|
+
|
|
141
|
+
return {
|
|
142
|
+
valid: openTasks.length === 0,
|
|
143
|
+
openTasks,
|
|
144
|
+
message:
|
|
145
|
+
openTasks.length > 0
|
|
146
|
+
? `Found ${openTasks.length} incomplete task(s). All tasks must be completed before proceeding to the next phase.`
|
|
147
|
+
: 'All tasks completed.',
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Create a new task under a parent
|
|
153
|
+
*/
|
|
154
|
+
async createTask(
|
|
155
|
+
title: string,
|
|
156
|
+
parentTaskId: string,
|
|
157
|
+
priority = 2
|
|
158
|
+
): Promise<string> {
|
|
159
|
+
const result = await this.executeBeadsCommand([
|
|
160
|
+
'create',
|
|
161
|
+
`"${title}"`,
|
|
162
|
+
'--parent',
|
|
163
|
+
parentTaskId,
|
|
164
|
+
'-p',
|
|
165
|
+
priority.toString(),
|
|
166
|
+
]);
|
|
167
|
+
|
|
168
|
+
if (!result.success) {
|
|
169
|
+
throw new Error(
|
|
170
|
+
`Failed to create task: ${result.stderr || 'Unknown error'}`
|
|
171
|
+
);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// Extract task ID from beads output
|
|
175
|
+
// Based on beads CLI output format: "✓ Created issue: task-id"
|
|
176
|
+
const match =
|
|
177
|
+
result.stdout?.match(/✓ Created issue: ([\w\d.-]+)/) ||
|
|
178
|
+
result.stdout?.match(/Created issue: ([\w\d.-]+)/) ||
|
|
179
|
+
result.stdout?.match(/Created (bd-[\w\d.]+)/);
|
|
180
|
+
|
|
181
|
+
if (!match) {
|
|
182
|
+
throw new Error(
|
|
183
|
+
`Failed to extract task ID from beads output: ${result.stdout || 'No output'}`
|
|
184
|
+
);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
return match[1] || '';
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* Update task status
|
|
192
|
+
*/
|
|
193
|
+
async updateTaskStatus(
|
|
194
|
+
taskId: string,
|
|
195
|
+
status: 'open' | 'in_progress' | 'completed' | 'cancelled'
|
|
196
|
+
): Promise<void> {
|
|
197
|
+
const beadsStatus = this.mapStatusToBeads(status);
|
|
198
|
+
|
|
199
|
+
const result = await this.executeBeadsCommand([
|
|
200
|
+
'update',
|
|
201
|
+
taskId,
|
|
202
|
+
'--status',
|
|
203
|
+
beadsStatus,
|
|
204
|
+
]);
|
|
205
|
+
|
|
206
|
+
if (!result.success) {
|
|
207
|
+
throw new Error(
|
|
208
|
+
`Failed to update task status: ${result.stderr || 'Unknown error'}`
|
|
209
|
+
);
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* Map our status enum to beads CLI status values
|
|
215
|
+
*/
|
|
216
|
+
private mapStatusToBeads(
|
|
217
|
+
status: 'open' | 'in_progress' | 'completed' | 'cancelled'
|
|
218
|
+
): string {
|
|
219
|
+
switch (status) {
|
|
220
|
+
case 'open':
|
|
221
|
+
return 'open';
|
|
222
|
+
case 'in_progress':
|
|
223
|
+
return 'in_progress';
|
|
224
|
+
case 'completed':
|
|
225
|
+
return 'completed';
|
|
226
|
+
case 'cancelled':
|
|
227
|
+
return 'cancelled';
|
|
228
|
+
default:
|
|
229
|
+
return 'open';
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Server Components Factory
|
|
3
|
+
*
|
|
4
|
+
* Factory for creating server components based on configuration.
|
|
5
|
+
* Implements strategy pattern to enable component substitution based on task backend.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import {
|
|
9
|
+
TaskBackendManager,
|
|
10
|
+
PlanManager,
|
|
11
|
+
InstructionGenerator,
|
|
12
|
+
type TaskBackendConfig,
|
|
13
|
+
} from '@codemcp/workflows-core';
|
|
14
|
+
|
|
15
|
+
// Beads implementations
|
|
16
|
+
import { BeadsPlanManager } from './beads/beads-plan-manager.js';
|
|
17
|
+
import { BeadsInstructionGenerator } from './beads/beads-instruction-generator.js';
|
|
18
|
+
import { BeadsTaskBackendClient } from './beads/beads-task-backend-client.js';
|
|
19
|
+
|
|
20
|
+
export interface ComponentFactoryOptions {
|
|
21
|
+
taskBackend?: TaskBackendConfig;
|
|
22
|
+
projectPath?: string;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Factory class for creating server components with appropriate strategy implementations
|
|
27
|
+
*/
|
|
28
|
+
export class ServerComponentsFactory {
|
|
29
|
+
private taskBackend: TaskBackendConfig;
|
|
30
|
+
private projectPath?: string;
|
|
31
|
+
|
|
32
|
+
constructor(options: ComponentFactoryOptions = {}) {
|
|
33
|
+
this.taskBackend =
|
|
34
|
+
options.taskBackend || TaskBackendManager.detectTaskBackend();
|
|
35
|
+
this.projectPath = options.projectPath;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Create the appropriate plan manager implementation
|
|
40
|
+
*/
|
|
41
|
+
createPlanManager(): PlanManager | BeadsPlanManager {
|
|
42
|
+
if (this.taskBackend.backend === 'beads' && this.taskBackend.isAvailable) {
|
|
43
|
+
return new BeadsPlanManager();
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Default markdown-based plan manager
|
|
47
|
+
return new PlanManager();
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Create the appropriate instruction generator implementation
|
|
52
|
+
*/
|
|
53
|
+
createInstructionGenerator():
|
|
54
|
+
| InstructionGenerator
|
|
55
|
+
| BeadsInstructionGenerator {
|
|
56
|
+
if (this.taskBackend.backend === 'beads' && this.taskBackend.isAvailable) {
|
|
57
|
+
return new BeadsInstructionGenerator();
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Default markdown-based instruction generator
|
|
61
|
+
return new InstructionGenerator(new PlanManager());
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Create the appropriate task backend client implementation
|
|
66
|
+
*/
|
|
67
|
+
createTaskBackendClient(): BeadsTaskBackendClient | null {
|
|
68
|
+
if (
|
|
69
|
+
this.taskBackend.backend === 'beads' &&
|
|
70
|
+
this.taskBackend.isAvailable &&
|
|
71
|
+
this.projectPath
|
|
72
|
+
) {
|
|
73
|
+
return new BeadsTaskBackendClient(this.projectPath);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// No task backend client for markdown mode
|
|
77
|
+
return null;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Get the current task backend configuration
|
|
82
|
+
*/
|
|
83
|
+
getTaskBackend(): TaskBackendConfig {
|
|
84
|
+
return this.taskBackend;
|
|
85
|
+
}
|
|
86
|
+
}
|
package/src/server-config.ts
CHANGED
|
@@ -15,8 +15,6 @@ import { FileStorage } from '@codemcp/workflows-core';
|
|
|
15
15
|
import type { IPersistence } from '@codemcp/workflows-core';
|
|
16
16
|
import { ConversationManager } from '@codemcp/workflows-core';
|
|
17
17
|
import { TransitionEngine } from '@codemcp/workflows-core';
|
|
18
|
-
import { InstructionGenerator } from '@codemcp/workflows-core';
|
|
19
|
-
import { PlanManager } from '@codemcp/workflows-core';
|
|
20
18
|
import { InteractionLogger } from '@codemcp/workflows-core';
|
|
21
19
|
import { WorkflowManager } from '@codemcp/workflows-core';
|
|
22
20
|
import { GitManager } from '@codemcp/workflows-core';
|
|
@@ -36,6 +34,7 @@ import {
|
|
|
36
34
|
generateWorkflowDescription,
|
|
37
35
|
} from './server-helpers.js';
|
|
38
36
|
import { notificationService } from './notification-service.js';
|
|
37
|
+
import { ServerComponentsFactory } from './components/server-components-factory.js';
|
|
39
38
|
|
|
40
39
|
const logger = createLogger('ServerConfig');
|
|
41
40
|
|
|
@@ -113,8 +112,14 @@ export async function initializeServerComponents(
|
|
|
113
112
|
);
|
|
114
113
|
const transitionEngine = new TransitionEngine(projectPath);
|
|
115
114
|
transitionEngine.setConversationManager(conversationManager);
|
|
116
|
-
|
|
117
|
-
|
|
115
|
+
|
|
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();
|
|
118
123
|
|
|
119
124
|
// Always create interaction logger as it's critical for transition engine logic
|
|
120
125
|
// (determining first call from initial state)
|