@codemcp/workflows-core 3.1.22 → 3.2.1
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/package.json +8 -3
- package/resources/templates/architecture/arc42/arc42-template-EN.md +1077 -0
- package/resources/templates/architecture/arc42/images/01_2_iso-25010-topics-EN.drawio-2023.png +0 -0
- package/resources/templates/architecture/arc42/images/01_2_iso-25010-topics-EN.drawio.png +0 -0
- package/resources/templates/architecture/arc42/images/05_building_blocks-EN.png +0 -0
- package/resources/templates/architecture/arc42/images/08-concepts-EN.drawio.png +0 -0
- package/resources/templates/architecture/arc42/images/arc42-logo.png +0 -0
- package/resources/templates/architecture/c4.md +224 -0
- package/resources/templates/architecture/freestyle.md +53 -0
- package/resources/templates/architecture/none.md +17 -0
- package/resources/templates/design/comprehensive.md +207 -0
- package/resources/templates/design/freestyle.md +37 -0
- package/resources/templates/design/none.md +17 -0
- package/resources/templates/requirements/ears.md +90 -0
- package/resources/templates/requirements/freestyle.md +42 -0
- package/resources/templates/requirements/none.md +17 -0
- package/resources/workflows/big-bang-conversion.yaml +539 -0
- package/resources/workflows/boundary-testing.yaml +334 -0
- package/resources/workflows/bugfix.yaml +185 -0
- package/resources/workflows/business-analysis.yaml +671 -0
- package/resources/workflows/c4-analysis.yaml +485 -0
- package/resources/workflows/epcc.yaml +161 -0
- package/resources/workflows/greenfield.yaml +189 -0
- package/resources/workflows/minor.yaml +127 -0
- package/resources/workflows/posts.yaml +207 -0
- package/resources/workflows/slides.yaml +256 -0
- package/resources/workflows/tdd.yaml +157 -0
- package/resources/workflows/waterfall.yaml +195 -0
- package/.turbo/turbo-build.log +0 -4
- package/src/config-manager.ts +0 -96
- package/src/conversation-manager.ts +0 -489
- package/src/database.ts +0 -427
- package/src/file-detection-manager.ts +0 -302
- package/src/git-manager.ts +0 -64
- package/src/index.ts +0 -28
- package/src/instruction-generator.ts +0 -210
- package/src/interaction-logger.ts +0 -109
- package/src/logger.ts +0 -353
- package/src/path-validation-utils.ts +0 -261
- package/src/plan-manager.ts +0 -323
- package/src/project-docs-manager.ts +0 -523
- package/src/state-machine-loader.ts +0 -365
- package/src/state-machine-types.ts +0 -72
- package/src/state-machine.ts +0 -370
- package/src/system-prompt-generator.ts +0 -122
- package/src/template-manager.ts +0 -328
- package/src/transition-engine.ts +0 -386
- package/src/types.ts +0 -60
- package/src/workflow-manager.ts +0 -606
- package/test/unit/conversation-manager.test.ts +0 -179
- package/test/unit/custom-workflow-loading.test.ts +0 -174
- package/test/unit/directory-linking-and-extensions.test.ts +0 -338
- package/test/unit/file-linking-integration.test.ts +0 -256
- package/test/unit/git-commit-integration.test.ts +0 -91
- package/test/unit/git-manager.test.ts +0 -86
- package/test/unit/install-workflow.test.ts +0 -138
- package/test/unit/instruction-generator.test.ts +0 -247
- package/test/unit/list-workflows-filtering.test.ts +0 -68
- package/test/unit/none-template-functionality.test.ts +0 -224
- package/test/unit/project-docs-manager.test.ts +0 -337
- package/test/unit/state-machine-loader.test.ts +0 -234
- package/test/unit/template-manager.test.ts +0 -217
- package/test/unit/validate-workflow-name.test.ts +0 -150
- package/test/unit/workflow-domain-filtering.test.ts +0 -75
- package/test/unit/workflow-enum-generation.test.ts +0 -92
- package/test/unit/workflow-manager-enhanced-path-resolution.test.ts +0 -369
- package/test/unit/workflow-manager-path-resolution.test.ts +0 -150
- package/test/unit/workflow-migration.test.ts +0 -155
- package/test/unit/workflow-override-by-name.test.ts +0 -116
- package/test/unit/workflow-prioritization.test.ts +0 -38
- package/test/unit/workflow-validation.test.ts +0 -303
- package/test/utils/e2e-test-setup.ts +0 -453
- package/test/utils/run-server-in-dir.sh +0 -27
- package/test/utils/temp-files.ts +0 -308
- package/test/utils/test-access.ts +0 -79
- package/test/utils/test-helpers.ts +0 -286
- package/test/utils/test-setup.ts +0 -78
- package/tsconfig.build.json +0 -21
- package/tsconfig.json +0 -8
- package/vitest.config.ts +0 -18
package/src/template-manager.ts
DELETED
@@ -1,328 +0,0 @@
|
|
1
|
-
/**
|
2
|
-
* Template Manager
|
3
|
-
*
|
4
|
-
* Handles loading and rendering of project document templates.
|
5
|
-
* Supports different template formats for architecture, requirements, and design documents.
|
6
|
-
*/
|
7
|
-
|
8
|
-
import { readFile, readdir, stat } from 'node:fs/promises';
|
9
|
-
import { join, dirname } from 'node:path';
|
10
|
-
import { fileURLToPath } from 'node:url';
|
11
|
-
import { createRequire } from 'node:module';
|
12
|
-
import { createLogger } from './logger.js';
|
13
|
-
|
14
|
-
const logger = createLogger('TemplateManager');
|
15
|
-
|
16
|
-
// Dynamic template types - will be populated from file system
|
17
|
-
export type ArchitectureTemplate = string;
|
18
|
-
export type RequirementsTemplate = string;
|
19
|
-
export type DesignTemplate = string;
|
20
|
-
|
21
|
-
export interface TemplateOptions {
|
22
|
-
architecture?: ArchitectureTemplate;
|
23
|
-
requirements?: RequirementsTemplate;
|
24
|
-
design?: DesignTemplate;
|
25
|
-
}
|
26
|
-
|
27
|
-
export interface TemplateResult {
|
28
|
-
content: string;
|
29
|
-
additionalFiles?: Array<{
|
30
|
-
relativePath: string;
|
31
|
-
content: Buffer;
|
32
|
-
}>;
|
33
|
-
}
|
34
|
-
|
35
|
-
export class TemplateManager {
|
36
|
-
private templatesPath: string;
|
37
|
-
|
38
|
-
constructor() {
|
39
|
-
this.templatesPath = this.resolveTemplatesPath();
|
40
|
-
}
|
41
|
-
|
42
|
-
/**
|
43
|
-
* Resolve templates path using similar strategy as WorkflowManager
|
44
|
-
*/
|
45
|
-
private resolveTemplatesPath(): string {
|
46
|
-
const strategies: string[] = [];
|
47
|
-
|
48
|
-
// Strategy 1: Local resources directory (symlinked from root)
|
49
|
-
strategies.push(
|
50
|
-
join(dirname(fileURLToPath(import.meta.url)), '../resources/templates')
|
51
|
-
);
|
52
|
-
|
53
|
-
// Strategy 2: From compiled dist directory
|
54
|
-
const currentFileUrl = import.meta.url;
|
55
|
-
if (currentFileUrl.startsWith('file://')) {
|
56
|
-
const currentFilePath = fileURLToPath(currentFileUrl);
|
57
|
-
// From dist/template-manager.js -> ../resources/templates
|
58
|
-
strategies.push(join(dirname(currentFilePath), '../resources/templates'));
|
59
|
-
}
|
60
|
-
|
61
|
-
// Strategy 3: Current working directory (for development)
|
62
|
-
strategies.push(join(process.cwd(), 'resources/templates'));
|
63
|
-
|
64
|
-
// Strategy 4: From node_modules
|
65
|
-
strategies.push(
|
66
|
-
join(
|
67
|
-
process.cwd(),
|
68
|
-
'node_modules/@codemcp/workflows-core/resources/templates'
|
69
|
-
)
|
70
|
-
);
|
71
|
-
|
72
|
-
// Strategy 5: From package directory (for development)
|
73
|
-
try {
|
74
|
-
const require = createRequire(import.meta.url);
|
75
|
-
const packagePath = require.resolve(
|
76
|
-
'@codemcp/workflows-core/package.json'
|
77
|
-
);
|
78
|
-
const packageDir = dirname(packagePath);
|
79
|
-
strategies.push(join(packageDir, 'resources/templates'));
|
80
|
-
} catch (_error) {
|
81
|
-
// Ignore if package not found
|
82
|
-
}
|
83
|
-
|
84
|
-
// Find the first existing path
|
85
|
-
for (const strategy of strategies) {
|
86
|
-
try {
|
87
|
-
// This will throw if path doesn't exist
|
88
|
-
require('node:fs').accessSync(strategy);
|
89
|
-
logger.debug('Using templates path', { path: strategy });
|
90
|
-
return strategy;
|
91
|
-
} catch (_error) {
|
92
|
-
// Continue to next strategy
|
93
|
-
}
|
94
|
-
}
|
95
|
-
|
96
|
-
// Fallback to first strategy if none found
|
97
|
-
const fallback = strategies[0];
|
98
|
-
logger.warn('No templates directory found, using fallback', {
|
99
|
-
fallback,
|
100
|
-
strategies,
|
101
|
-
});
|
102
|
-
return fallback;
|
103
|
-
}
|
104
|
-
|
105
|
-
/**
|
106
|
-
* Get the default template options based on available templates
|
107
|
-
*/
|
108
|
-
async getDefaults(): Promise<Required<TemplateOptions>> {
|
109
|
-
const availableTemplates = await this.getAvailableTemplates();
|
110
|
-
|
111
|
-
return {
|
112
|
-
architecture: this.getPreferredTemplate(availableTemplates.architecture, [
|
113
|
-
'arc42',
|
114
|
-
'freestyle',
|
115
|
-
]),
|
116
|
-
requirements: this.getPreferredTemplate(availableTemplates.requirements, [
|
117
|
-
'ears',
|
118
|
-
'freestyle',
|
119
|
-
]),
|
120
|
-
design: this.getPreferredTemplate(availableTemplates.design, [
|
121
|
-
'comprehensive',
|
122
|
-
'freestyle',
|
123
|
-
]),
|
124
|
-
};
|
125
|
-
}
|
126
|
-
|
127
|
-
/**
|
128
|
-
* Get preferred template from available options, with fallback preferences
|
129
|
-
*/
|
130
|
-
private getPreferredTemplate(
|
131
|
-
available: string[],
|
132
|
-
preferences: string[]
|
133
|
-
): string {
|
134
|
-
// Try to find the first preference that's available
|
135
|
-
for (const preference of preferences) {
|
136
|
-
if (available.includes(preference)) {
|
137
|
-
return preference;
|
138
|
-
}
|
139
|
-
}
|
140
|
-
|
141
|
-
// If no preferences are available, return the first available template
|
142
|
-
return available[0] || preferences[0];
|
143
|
-
}
|
144
|
-
|
145
|
-
/**
|
146
|
-
* Load and render a template
|
147
|
-
*/
|
148
|
-
async loadTemplate(
|
149
|
-
type: 'architecture' | 'requirements' | 'design',
|
150
|
-
template: string
|
151
|
-
): Promise<TemplateResult> {
|
152
|
-
const templatePath = join(this.templatesPath, type, template);
|
153
|
-
const templateFilePath = `${templatePath}.md`;
|
154
|
-
|
155
|
-
try {
|
156
|
-
// First try to check if it's a directory (like arc42)
|
157
|
-
try {
|
158
|
-
const stats = await stat(templatePath);
|
159
|
-
if (stats.isDirectory()) {
|
160
|
-
return await this.loadDirectoryTemplate(templatePath);
|
161
|
-
}
|
162
|
-
} catch (_error) {
|
163
|
-
// Not a directory, continue to file check
|
164
|
-
}
|
165
|
-
|
166
|
-
// Try to load as a markdown file
|
167
|
-
const content = await readFile(templateFilePath, 'utf-8');
|
168
|
-
return { content };
|
169
|
-
} catch (error) {
|
170
|
-
logger.error(
|
171
|
-
`Failed to load template: ${type}/${template}`,
|
172
|
-
error as Error
|
173
|
-
);
|
174
|
-
throw new Error(
|
175
|
-
`Template not found: ${type}/${template}. Tried: ${templatePath} (directory) and ${templateFilePath} (file)`
|
176
|
-
);
|
177
|
-
}
|
178
|
-
}
|
179
|
-
|
180
|
-
/**
|
181
|
-
* Load a directory-based template (like arc42 with images)
|
182
|
-
*/
|
183
|
-
private async loadDirectoryTemplate(
|
184
|
-
templatePath: string
|
185
|
-
): Promise<TemplateResult> {
|
186
|
-
const additionalFiles: Array<{ relativePath: string; content: Buffer }> =
|
187
|
-
[];
|
188
|
-
|
189
|
-
// Find the main markdown file
|
190
|
-
const files = await readdir(templatePath);
|
191
|
-
const markdownFile = files.find(file => file.endsWith('.md'));
|
192
|
-
|
193
|
-
if (!markdownFile) {
|
194
|
-
throw new Error(
|
195
|
-
`No markdown file found in template directory: ${templatePath}`
|
196
|
-
);
|
197
|
-
}
|
198
|
-
|
199
|
-
const content = await readFile(join(templatePath, markdownFile), 'utf-8');
|
200
|
-
|
201
|
-
// Load additional files (like images)
|
202
|
-
await this.loadAdditionalFiles(templatePath, '', additionalFiles);
|
203
|
-
|
204
|
-
return {
|
205
|
-
content,
|
206
|
-
additionalFiles: additionalFiles.filter(
|
207
|
-
file => !file.relativePath.endsWith('.md')
|
208
|
-
),
|
209
|
-
};
|
210
|
-
}
|
211
|
-
|
212
|
-
/**
|
213
|
-
* Recursively load additional files from template directory
|
214
|
-
*/
|
215
|
-
private async loadAdditionalFiles(
|
216
|
-
basePath: string,
|
217
|
-
relativePath: string,
|
218
|
-
additionalFiles: Array<{ relativePath: string; content: Buffer }>
|
219
|
-
): Promise<void> {
|
220
|
-
const currentPath = join(basePath, relativePath);
|
221
|
-
const items = await readdir(currentPath);
|
222
|
-
|
223
|
-
for (const item of items) {
|
224
|
-
const itemPath = join(currentPath, item);
|
225
|
-
const itemRelativePath = relativePath ? join(relativePath, item) : item;
|
226
|
-
const stats = await stat(itemPath);
|
227
|
-
|
228
|
-
if (stats.isDirectory()) {
|
229
|
-
// Recursively process subdirectories
|
230
|
-
await this.loadAdditionalFiles(
|
231
|
-
basePath,
|
232
|
-
itemRelativePath,
|
233
|
-
additionalFiles
|
234
|
-
);
|
235
|
-
} else if (!item.endsWith('.md')) {
|
236
|
-
// Load non-markdown files as additional files
|
237
|
-
const content = await readFile(itemPath);
|
238
|
-
additionalFiles.push({
|
239
|
-
relativePath: itemRelativePath,
|
240
|
-
content,
|
241
|
-
});
|
242
|
-
}
|
243
|
-
}
|
244
|
-
}
|
245
|
-
|
246
|
-
/**
|
247
|
-
* Validate template options against available templates
|
248
|
-
*/
|
249
|
-
async validateOptions(options: TemplateOptions): Promise<void> {
|
250
|
-
const availableTemplates = await this.getAvailableTemplates();
|
251
|
-
|
252
|
-
if (
|
253
|
-
options.architecture &&
|
254
|
-
!availableTemplates.architecture.includes(options.architecture)
|
255
|
-
) {
|
256
|
-
throw new Error(
|
257
|
-
`Invalid architecture template: ${options.architecture}. Valid options: ${availableTemplates.architecture.join(', ')}`
|
258
|
-
);
|
259
|
-
}
|
260
|
-
|
261
|
-
if (
|
262
|
-
options.requirements &&
|
263
|
-
!availableTemplates.requirements.includes(options.requirements)
|
264
|
-
) {
|
265
|
-
throw new Error(
|
266
|
-
`Invalid requirements template: ${options.requirements}. Valid options: ${availableTemplates.requirements.join(', ')}`
|
267
|
-
);
|
268
|
-
}
|
269
|
-
|
270
|
-
if (options.design && !availableTemplates.design.includes(options.design)) {
|
271
|
-
throw new Error(
|
272
|
-
`Invalid design template: ${options.design}. Valid options: ${availableTemplates.design.join(', ')}`
|
273
|
-
);
|
274
|
-
}
|
275
|
-
}
|
276
|
-
|
277
|
-
/**
|
278
|
-
* Get available templates for each type by scanning the file system
|
279
|
-
*/
|
280
|
-
async getAvailableTemplates(): Promise<{
|
281
|
-
architecture: string[];
|
282
|
-
requirements: string[];
|
283
|
-
design: string[];
|
284
|
-
}> {
|
285
|
-
const result = {
|
286
|
-
architecture: [] as string[],
|
287
|
-
requirements: [] as string[],
|
288
|
-
design: [] as string[],
|
289
|
-
};
|
290
|
-
|
291
|
-
try {
|
292
|
-
// Scan each template type directory
|
293
|
-
for (const [type, templates] of Object.entries(result)) {
|
294
|
-
const typePath = join(this.templatesPath, type);
|
295
|
-
|
296
|
-
try {
|
297
|
-
const entries = await readdir(typePath, { withFileTypes: true });
|
298
|
-
|
299
|
-
for (const entry of entries) {
|
300
|
-
if (entry.isDirectory()) {
|
301
|
-
// Directory-based template (like arc42)
|
302
|
-
templates.push(entry.name);
|
303
|
-
} else if (entry.isFile() && entry.name.endsWith('.md')) {
|
304
|
-
// File-based template (like freestyle.md)
|
305
|
-
const templateName = entry.name.replace('.md', '');
|
306
|
-
templates.push(templateName);
|
307
|
-
}
|
308
|
-
}
|
309
|
-
|
310
|
-
// Sort templates for consistent ordering
|
311
|
-
templates.sort();
|
312
|
-
} catch (error) {
|
313
|
-
logger.warn(`Failed to scan templates for type: ${type}`, {
|
314
|
-
typePath,
|
315
|
-
error: error instanceof Error ? error.message : String(error),
|
316
|
-
});
|
317
|
-
}
|
318
|
-
}
|
319
|
-
|
320
|
-
logger.debug('Discovered available templates', result);
|
321
|
-
return result;
|
322
|
-
} catch (error) {
|
323
|
-
logger.error('Failed to discover templates', error as Error);
|
324
|
-
// Return empty arrays if discovery fails
|
325
|
-
return result;
|
326
|
-
}
|
327
|
-
}
|
328
|
-
}
|
package/src/transition-engine.ts
DELETED
@@ -1,386 +0,0 @@
|
|
1
|
-
/**
|
2
|
-
* Transition Engine
|
3
|
-
*
|
4
|
-
* Manages the development state machine and determines appropriate phase transitions.
|
5
|
-
* Analyzes conversation context and user input to make intelligent phase decisions.
|
6
|
-
*/
|
7
|
-
|
8
|
-
import { createLogger } from './logger.js';
|
9
|
-
import { StateMachineLoader } from './state-machine-loader.js';
|
10
|
-
import { WorkflowManager } from './workflow-manager.js';
|
11
|
-
import type { ConversationState } from './types.js';
|
12
|
-
|
13
|
-
const logger = createLogger('TransitionEngine');
|
14
|
-
|
15
|
-
export interface TransitionContext {
|
16
|
-
currentPhase: string;
|
17
|
-
projectPath: string;
|
18
|
-
conversationId: string;
|
19
|
-
userInput?: string;
|
20
|
-
context?: string;
|
21
|
-
conversationSummary?: string;
|
22
|
-
recentMessages?: Array<{ role: string; content: string }>;
|
23
|
-
}
|
24
|
-
|
25
|
-
export interface TransitionResult {
|
26
|
-
newPhase: string;
|
27
|
-
instructions: string;
|
28
|
-
transitionReason: string;
|
29
|
-
isModeled: boolean;
|
30
|
-
}
|
31
|
-
|
32
|
-
export class TransitionEngine {
|
33
|
-
private stateMachineLoader: StateMachineLoader;
|
34
|
-
private workflowManager: WorkflowManager;
|
35
|
-
private conversationManager?: {
|
36
|
-
hasInteractions: (conversationId: string) => Promise<boolean>;
|
37
|
-
getConversationState: (
|
38
|
-
conversationId: string
|
39
|
-
) => Promise<ConversationState | null>;
|
40
|
-
};
|
41
|
-
|
42
|
-
constructor(projectPath: string) {
|
43
|
-
this.stateMachineLoader = new StateMachineLoader();
|
44
|
-
this.workflowManager = new WorkflowManager();
|
45
|
-
|
46
|
-
logger.info('TransitionEngine initialized', { projectPath });
|
47
|
-
}
|
48
|
-
|
49
|
-
/**
|
50
|
-
* Set the conversation manager (dependency injection)
|
51
|
-
*/
|
52
|
-
setConversationManager(conversationManager: {
|
53
|
-
hasInteractions: (conversationId: string) => Promise<boolean>;
|
54
|
-
getConversationState: (
|
55
|
-
conversationId: string
|
56
|
-
) => Promise<ConversationState | null>;
|
57
|
-
}) {
|
58
|
-
this.conversationManager = conversationManager;
|
59
|
-
}
|
60
|
-
|
61
|
-
/**
|
62
|
-
* Get the loaded state machine for the current project
|
63
|
-
*/
|
64
|
-
getStateMachine(projectPath: string, workflowName?: string) {
|
65
|
-
// Use WorkflowManager to load the appropriate workflow
|
66
|
-
return this.workflowManager.loadWorkflowForProject(
|
67
|
-
projectPath,
|
68
|
-
workflowName
|
69
|
-
);
|
70
|
-
}
|
71
|
-
|
72
|
-
/**
|
73
|
-
* Check if this is the first call from initial state based on database interactions
|
74
|
-
*/
|
75
|
-
private async isFirstCallFromInitialState(
|
76
|
-
context: TransitionContext
|
77
|
-
): Promise<boolean> {
|
78
|
-
// Get workflow name from conversation state
|
79
|
-
const conversationState =
|
80
|
-
await this.conversationManager?.getConversationState(
|
81
|
-
context.conversationId
|
82
|
-
);
|
83
|
-
const workflowName = conversationState?.workflowName;
|
84
|
-
|
85
|
-
const stateMachine = this.workflowManager.loadWorkflowForProject(
|
86
|
-
context.projectPath,
|
87
|
-
workflowName
|
88
|
-
);
|
89
|
-
const isInitialState = context.currentPhase === stateMachine.initial_state;
|
90
|
-
|
91
|
-
if (!isInitialState) return false;
|
92
|
-
|
93
|
-
// Check database for any previous interactions in this conversation
|
94
|
-
if (!this.conversationManager) {
|
95
|
-
logger.warn('ConversationManager not set, assuming first call');
|
96
|
-
return true;
|
97
|
-
}
|
98
|
-
|
99
|
-
const hasInteractions = await this.conversationManager.hasInteractions(
|
100
|
-
context.conversationId
|
101
|
-
);
|
102
|
-
|
103
|
-
logger.debug('Checking first call from initial state', {
|
104
|
-
isInitialState,
|
105
|
-
hasInteractions,
|
106
|
-
conversationId: context.conversationId,
|
107
|
-
currentPhase: context.currentPhase,
|
108
|
-
});
|
109
|
-
|
110
|
-
return !hasInteractions;
|
111
|
-
}
|
112
|
-
|
113
|
-
/**
|
114
|
-
* Generate instructions for defining phase entrance criteria
|
115
|
-
*/
|
116
|
-
private async generateCriteriaDefinitionInstructions(
|
117
|
-
projectPath: string,
|
118
|
-
conversationId: string
|
119
|
-
): Promise<string> {
|
120
|
-
// Get workflow name from conversation state
|
121
|
-
const conversationState =
|
122
|
-
await this.conversationManager?.getConversationState(conversationId);
|
123
|
-
const workflowName = conversationState?.workflowName;
|
124
|
-
|
125
|
-
const stateMachine = this.workflowManager.loadWorkflowForProject(
|
126
|
-
projectPath,
|
127
|
-
workflowName
|
128
|
-
);
|
129
|
-
const phases = Object.keys(stateMachine.states);
|
130
|
-
|
131
|
-
let instructions = `Welcome to ${stateMachine.name}!
|
132
|
-
|
133
|
-
Before we begin development, let's establish clear entrance criteria for each phase. This will help us make informed decisions about when to transition between phases throughout the development process.
|
134
|
-
|
135
|
-
Please update the plan file with a "Phase Entrance Criteria" section that defines specific, measurable criteria for entering each phase:
|
136
|
-
|
137
|
-
## Phase Entrance Criteria
|
138
|
-
|
139
|
-
`;
|
140
|
-
|
141
|
-
// Generate criteria template for each phase (except initial state)
|
142
|
-
for (const phase of phases) {
|
143
|
-
if (phase === stateMachine.initial_state) continue; // Skip initial state
|
144
|
-
|
145
|
-
const phaseDefinition = stateMachine.states[phase];
|
146
|
-
const capitalizedPhase = this.capitalizePhase(phase);
|
147
|
-
|
148
|
-
instructions += `### ${capitalizedPhase} Phase
|
149
|
-
*${phaseDefinition.description}*
|
150
|
-
|
151
|
-
**Enter when:**
|
152
|
-
- [ ] [Define specific criteria for entering ${phase} phase]
|
153
|
-
- [ ] [Add measurable conditions that must be met]
|
154
|
-
- [ ] [Include any deliverables or milestones required]
|
155
|
-
|
156
|
-
`;
|
157
|
-
}
|
158
|
-
|
159
|
-
instructions += `
|
160
|
-
Once you've defined these criteria, we can begin development. Throughout the process, consult these criteria when considering phase transitions.
|
161
|
-
|
162
|
-
**Remember**: These criteria should be specific and measurable so we can clearly determine when each phase is ready to begin.`;
|
163
|
-
|
164
|
-
return instructions;
|
165
|
-
}
|
166
|
-
|
167
|
-
/**
|
168
|
-
* Get phase-specific instructions for continuing work in current phase
|
169
|
-
*/
|
170
|
-
private async getContinuePhaseInstructions(
|
171
|
-
phase: string,
|
172
|
-
projectPath: string,
|
173
|
-
conversationId: string
|
174
|
-
): Promise<string> {
|
175
|
-
// Get workflow name from conversation state
|
176
|
-
const conversationState =
|
177
|
-
await this.conversationManager?.getConversationState(conversationId);
|
178
|
-
const workflowName = conversationState?.workflowName;
|
179
|
-
|
180
|
-
const stateMachine = this.workflowManager.loadWorkflowForProject(
|
181
|
-
projectPath,
|
182
|
-
workflowName
|
183
|
-
);
|
184
|
-
|
185
|
-
const stateDefinition = stateMachine.states[phase];
|
186
|
-
if (!stateDefinition) {
|
187
|
-
logger.error('Unknown phase', new Error(`Unknown phase: ${phase}`));
|
188
|
-
throw new Error(`Unknown phase: ${phase}`);
|
189
|
-
}
|
190
|
-
|
191
|
-
const continueTransition = stateDefinition.transitions.find(
|
192
|
-
t => t.to === phase
|
193
|
-
);
|
194
|
-
|
195
|
-
if (continueTransition) {
|
196
|
-
// Use the transition instructions if available, otherwise use default + additional
|
197
|
-
if (continueTransition.instructions) {
|
198
|
-
return continueTransition.instructions;
|
199
|
-
} else {
|
200
|
-
let composedInstructions = stateDefinition.default_instructions;
|
201
|
-
if (continueTransition.additional_instructions) {
|
202
|
-
composedInstructions = `${composedInstructions}\n\n**Additional Context:**\n${continueTransition.additional_instructions}`;
|
203
|
-
}
|
204
|
-
return composedInstructions;
|
205
|
-
}
|
206
|
-
}
|
207
|
-
|
208
|
-
// Fall back to default instructions for the phase
|
209
|
-
return stateDefinition.default_instructions;
|
210
|
-
}
|
211
|
-
/**
|
212
|
-
* Get the first development phase from the state machine
|
213
|
-
*/
|
214
|
-
private async getFirstDevelopmentPhase(
|
215
|
-
projectPath: string,
|
216
|
-
conversationId: string
|
217
|
-
): Promise<string> {
|
218
|
-
// Get workflow name from conversation state
|
219
|
-
const conversationState =
|
220
|
-
await this.conversationManager?.getConversationState(conversationId);
|
221
|
-
const workflowName = conversationState?.workflowName;
|
222
|
-
|
223
|
-
const stateMachine = this.workflowManager.loadWorkflowForProject(
|
224
|
-
projectPath,
|
225
|
-
workflowName
|
226
|
-
);
|
227
|
-
const initialState = stateMachine.initial_state;
|
228
|
-
|
229
|
-
// The first development phase IS the initial state - we should stay there
|
230
|
-
// Don't automatically transition to the first transition target
|
231
|
-
return initialState;
|
232
|
-
}
|
233
|
-
|
234
|
-
/**
|
235
|
-
* Analyze context and determine appropriate phase transition
|
236
|
-
*/
|
237
|
-
async analyzePhaseTransition(
|
238
|
-
context: TransitionContext
|
239
|
-
): Promise<TransitionResult> {
|
240
|
-
const {
|
241
|
-
currentPhase,
|
242
|
-
projectPath,
|
243
|
-
conversationId,
|
244
|
-
userInput,
|
245
|
-
context: additionalContext,
|
246
|
-
conversationSummary,
|
247
|
-
} = context;
|
248
|
-
|
249
|
-
// Load the appropriate workflow for this project/conversation
|
250
|
-
|
251
|
-
logger.debug('Analyzing phase transition', {
|
252
|
-
currentPhase,
|
253
|
-
projectPath,
|
254
|
-
hasUserInput: !!userInput,
|
255
|
-
hasContext: !!additionalContext,
|
256
|
-
hasSummary: !!conversationSummary,
|
257
|
-
userInput: userInput
|
258
|
-
? userInput.substring(0, 50) + (userInput.length > 50 ? '...' : '')
|
259
|
-
: undefined,
|
260
|
-
});
|
261
|
-
|
262
|
-
// Check if this is the first call from initial state - transition to first development phase
|
263
|
-
if (await this.isFirstCallFromInitialState(context)) {
|
264
|
-
const firstDevelopmentPhase = await this.getFirstDevelopmentPhase(
|
265
|
-
projectPath,
|
266
|
-
conversationId
|
267
|
-
);
|
268
|
-
|
269
|
-
logger.info(
|
270
|
-
'First call from initial state - transitioning to first development phase with criteria',
|
271
|
-
{
|
272
|
-
currentPhase,
|
273
|
-
firstDevelopmentPhase,
|
274
|
-
projectPath,
|
275
|
-
}
|
276
|
-
);
|
277
|
-
|
278
|
-
// Combine criteria definition with first phase instructions
|
279
|
-
const criteriaInstructions =
|
280
|
-
await this.generateCriteriaDefinitionInstructions(
|
281
|
-
projectPath,
|
282
|
-
conversationId
|
283
|
-
);
|
284
|
-
const phaseInstructions = await this.getContinuePhaseInstructions(
|
285
|
-
firstDevelopmentPhase,
|
286
|
-
projectPath,
|
287
|
-
conversationId
|
288
|
-
);
|
289
|
-
|
290
|
-
return {
|
291
|
-
newPhase: firstDevelopmentPhase, // Transition to first development phase
|
292
|
-
instructions: criteriaInstructions + '\n\n---\n\n' + phaseInstructions,
|
293
|
-
transitionReason:
|
294
|
-
'Starting development - defining criteria and beginning first phase',
|
295
|
-
isModeled: true,
|
296
|
-
};
|
297
|
-
}
|
298
|
-
|
299
|
-
// For all other cases, stay in current phase and let LLM decide based on plan file criteria
|
300
|
-
// The LLM will consult the entrance criteria in the plan file and use proceed_to_phase when ready
|
301
|
-
const continueInstructions = await this.getContinuePhaseInstructions(
|
302
|
-
currentPhase,
|
303
|
-
projectPath,
|
304
|
-
conversationId
|
305
|
-
);
|
306
|
-
|
307
|
-
logger.debug(
|
308
|
-
'Continuing in current phase - LLM will evaluate transition criteria',
|
309
|
-
{
|
310
|
-
currentPhase,
|
311
|
-
projectPath,
|
312
|
-
}
|
313
|
-
);
|
314
|
-
|
315
|
-
return {
|
316
|
-
newPhase: currentPhase,
|
317
|
-
instructions: continueInstructions,
|
318
|
-
transitionReason:
|
319
|
-
'Continue current phase - LLM will evaluate transition criteria from plan file',
|
320
|
-
isModeled: false,
|
321
|
-
};
|
322
|
-
}
|
323
|
-
|
324
|
-
/**
|
325
|
-
* Handle explicit phase transition request
|
326
|
-
*/
|
327
|
-
handleExplicitTransition(
|
328
|
-
currentPhase: string,
|
329
|
-
targetPhase: string,
|
330
|
-
projectPath: string,
|
331
|
-
reason?: string,
|
332
|
-
workflowName?: string
|
333
|
-
): TransitionResult {
|
334
|
-
// Load the appropriate state machine for this project/workflow
|
335
|
-
const stateMachine = this.getStateMachine(projectPath, workflowName);
|
336
|
-
|
337
|
-
logger.debug('Handling explicit phase transition', {
|
338
|
-
currentPhase,
|
339
|
-
targetPhase,
|
340
|
-
projectPath,
|
341
|
-
workflowName,
|
342
|
-
reason,
|
343
|
-
});
|
344
|
-
|
345
|
-
// Validate that the target phase exists in the state machine
|
346
|
-
if (!stateMachine.states[targetPhase]) {
|
347
|
-
const validPhases = Object.keys(stateMachine.states);
|
348
|
-
const errorMsg = `Invalid target phase: "${targetPhase}". Valid phases are: ${validPhases.join(', ')}`;
|
349
|
-
logger.error('Invalid target phase', new Error(errorMsg));
|
350
|
-
throw new Error(errorMsg);
|
351
|
-
}
|
352
|
-
|
353
|
-
// Get default instructions from the target state
|
354
|
-
const targetState = stateMachine.states[targetPhase];
|
355
|
-
const instructions = targetState.default_instructions;
|
356
|
-
const transitionInfo = {
|
357
|
-
instructions: instructions,
|
358
|
-
transitionReason: reason || `Moving to ${targetPhase}`,
|
359
|
-
isModeled: false, // Direct phase transitions are not modeled
|
360
|
-
};
|
361
|
-
|
362
|
-
logger.info('Explicit phase transition processed', {
|
363
|
-
fromPhase: currentPhase,
|
364
|
-
toPhase: targetPhase,
|
365
|
-
reason: transitionInfo.transitionReason,
|
366
|
-
isModeled: transitionInfo.isModeled,
|
367
|
-
});
|
368
|
-
|
369
|
-
return {
|
370
|
-
newPhase: targetPhase,
|
371
|
-
instructions: transitionInfo.instructions,
|
372
|
-
transitionReason: reason || transitionInfo.transitionReason,
|
373
|
-
isModeled: transitionInfo.isModeled,
|
374
|
-
};
|
375
|
-
}
|
376
|
-
|
377
|
-
/**
|
378
|
-
* Capitalize phase name for display
|
379
|
-
*/
|
380
|
-
private capitalizePhase(phase: string): string {
|
381
|
-
return phase
|
382
|
-
.split('_')
|
383
|
-
.map(word => word.charAt(0).toUpperCase() + word.slice(1))
|
384
|
-
.join(' ');
|
385
|
-
}
|
386
|
-
}
|