edsger 0.29.1 → 0.29.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/dist/commands/agent-workflow/processor.js +38 -10
- package/dist/commands/workflow/feature-coordinator.js +5 -3
- package/dist/index.js +0 -0
- package/dist/phases/chat-processor/tools.js +35 -5
- package/dist/phases/feature-analysis/index.js +6 -6
- package/dist/phases/feature-analysis/outcome.d.ts +1 -1
- package/dist/phases/feature-analysis/outcome.js +8 -8
- package/dist/phases/test-cases-analysis/index.js +5 -5
- package/dist/phases/test-cases-analysis/outcome.js +3 -3
- package/dist/phases/user-stories-analysis/index.js +5 -5
- package/dist/phases/user-stories-analysis/outcome.js +4 -4
- package/dist/services/lifecycle-agent/__tests__/phase-criteria.test.d.ts +4 -0
- package/dist/services/lifecycle-agent/__tests__/phase-criteria.test.js +133 -0
- package/dist/services/lifecycle-agent/__tests__/transition-rules.test.d.ts +4 -0
- package/dist/services/lifecycle-agent/__tests__/transition-rules.test.js +336 -0
- package/dist/services/lifecycle-agent/index.d.ts +24 -0
- package/dist/services/lifecycle-agent/index.js +25 -0
- package/dist/services/lifecycle-agent/phase-criteria.d.ts +57 -0
- package/dist/services/lifecycle-agent/phase-criteria.js +335 -0
- package/dist/services/lifecycle-agent/transition-rules.d.ts +60 -0
- package/dist/services/lifecycle-agent/transition-rules.js +184 -0
- package/dist/services/lifecycle-agent/types.d.ts +190 -0
- package/dist/services/lifecycle-agent/types.js +12 -0
- package/dist/types/features.d.ts +2 -2
- package/dist/types/index.d.ts +2 -2
- package/package.json +1 -1
- package/.claude/settings.local.json +0 -28
- package/.env.local +0 -12
- package/dist/api/features/__tests__/regression-prevention.test.d.ts +0 -5
- package/dist/api/features/__tests__/regression-prevention.test.js +0 -338
- package/dist/api/features/__tests__/status-updater.integration.test.d.ts +0 -5
- package/dist/api/features/__tests__/status-updater.integration.test.js +0 -497
- package/dist/commands/workflow/pipeline-runner.d.ts +0 -17
- package/dist/commands/workflow/pipeline-runner.js +0 -393
- package/dist/commands/workflow/runner.d.ts +0 -26
- package/dist/commands/workflow/runner.js +0 -119
- package/dist/commands/workflow/workflow-runner.d.ts +0 -26
- package/dist/commands/workflow/workflow-runner.js +0 -119
- package/dist/phases/code-implementation/analyzer-helpers.d.ts +0 -28
- package/dist/phases/code-implementation/analyzer-helpers.js +0 -177
- package/dist/phases/code-implementation/analyzer.d.ts +0 -32
- package/dist/phases/code-implementation/analyzer.js +0 -629
- package/dist/phases/code-implementation/context-fetcher.d.ts +0 -17
- package/dist/phases/code-implementation/context-fetcher.js +0 -86
- package/dist/phases/code-implementation/mcp-server.d.ts +0 -1
- package/dist/phases/code-implementation/mcp-server.js +0 -93
- package/dist/phases/code-implementation/prompts-improvement.d.ts +0 -5
- package/dist/phases/code-implementation/prompts-improvement.js +0 -108
- package/dist/phases/code-implementation-verification/verifier.d.ts +0 -31
- package/dist/phases/code-implementation-verification/verifier.js +0 -196
- package/dist/phases/code-refine/analyzer.d.ts +0 -41
- package/dist/phases/code-refine/analyzer.js +0 -561
- package/dist/phases/code-refine/context-fetcher.d.ts +0 -94
- package/dist/phases/code-refine/context-fetcher.js +0 -423
- package/dist/phases/code-refine-verification/analysis/llm-analyzer.d.ts +0 -22
- package/dist/phases/code-refine-verification/analysis/llm-analyzer.js +0 -134
- package/dist/phases/code-refine-verification/verifier.d.ts +0 -47
- package/dist/phases/code-refine-verification/verifier.js +0 -597
- package/dist/phases/code-review/analyzer.d.ts +0 -29
- package/dist/phases/code-review/analyzer.js +0 -363
- package/dist/phases/code-review/context-fetcher.d.ts +0 -92
- package/dist/phases/code-review/context-fetcher.js +0 -296
- package/dist/phases/feature-analysis/analyzer-helpers.d.ts +0 -10
- package/dist/phases/feature-analysis/analyzer-helpers.js +0 -47
- package/dist/phases/feature-analysis/analyzer.d.ts +0 -11
- package/dist/phases/feature-analysis/analyzer.js +0 -208
- package/dist/phases/feature-analysis/context-fetcher.d.ts +0 -26
- package/dist/phases/feature-analysis/context-fetcher.js +0 -134
- package/dist/phases/feature-analysis/http-fallback.d.ts +0 -20
- package/dist/phases/feature-analysis/http-fallback.js +0 -95
- package/dist/phases/feature-analysis/mcp-server.d.ts +0 -1
- package/dist/phases/feature-analysis/mcp-server.js +0 -144
- package/dist/phases/feature-analysis/prompts-improvement.d.ts +0 -8
- package/dist/phases/feature-analysis/prompts-improvement.js +0 -109
- package/dist/phases/feature-analysis-verification/verifier.d.ts +0 -37
- package/dist/phases/feature-analysis-verification/verifier.js +0 -147
- package/dist/phases/technical-design/analyzer-helpers.d.ts +0 -25
- package/dist/phases/technical-design/analyzer-helpers.js +0 -39
- package/dist/phases/technical-design/analyzer.d.ts +0 -21
- package/dist/phases/technical-design/analyzer.js +0 -461
- package/dist/phases/technical-design/context-fetcher.d.ts +0 -12
- package/dist/phases/technical-design/context-fetcher.js +0 -39
- package/dist/phases/technical-design/http-fallback.d.ts +0 -17
- package/dist/phases/technical-design/http-fallback.js +0 -151
- package/dist/phases/technical-design/mcp-server.d.ts +0 -1
- package/dist/phases/technical-design/mcp-server.js +0 -157
- package/dist/phases/technical-design/prompts-improvement.d.ts +0 -5
- package/dist/phases/technical-design/prompts-improvement.js +0 -93
- package/dist/phases/technical-design-verification/verifier.d.ts +0 -53
- package/dist/phases/technical-design-verification/verifier.js +0 -170
- package/dist/services/feature-branches.d.ts +0 -77
- package/dist/services/feature-branches.js +0 -205
- package/dist/workflow-runner/config/phase-configs.d.ts +0 -5
- package/dist/workflow-runner/config/phase-configs.js +0 -120
- package/dist/workflow-runner/core/feature-filter.d.ts +0 -16
- package/dist/workflow-runner/core/feature-filter.js +0 -46
- package/dist/workflow-runner/core/index.d.ts +0 -8
- package/dist/workflow-runner/core/index.js +0 -12
- package/dist/workflow-runner/core/pipeline-evaluator.d.ts +0 -24
- package/dist/workflow-runner/core/pipeline-evaluator.js +0 -32
- package/dist/workflow-runner/core/state-manager.d.ts +0 -24
- package/dist/workflow-runner/core/state-manager.js +0 -42
- package/dist/workflow-runner/core/workflow-logger.d.ts +0 -20
- package/dist/workflow-runner/core/workflow-logger.js +0 -65
- package/dist/workflow-runner/executors/phase-executor.d.ts +0 -8
- package/dist/workflow-runner/executors/phase-executor.js +0 -248
- package/dist/workflow-runner/feature-workflow-runner.d.ts +0 -26
- package/dist/workflow-runner/feature-workflow-runner.js +0 -119
- package/dist/workflow-runner/index.d.ts +0 -2
- package/dist/workflow-runner/index.js +0 -2
- package/dist/workflow-runner/pipeline-runner.d.ts +0 -17
- package/dist/workflow-runner/pipeline-runner.js +0 -393
- package/dist/workflow-runner/workflow-processor.d.ts +0 -54
- package/dist/workflow-runner/workflow-processor.js +0 -170
|
@@ -185,11 +185,36 @@ export class AgentWorkflowProcessor {
|
|
|
185
185
|
}
|
|
186
186
|
return;
|
|
187
187
|
}
|
|
188
|
-
// Filter out features already being processed
|
|
189
|
-
const
|
|
188
|
+
// Filter out features already being processed or recently failed
|
|
189
|
+
const COOLDOWN_MS = 5 * 60 * 1000; // 5 minutes between retries
|
|
190
|
+
const newFeatures = features.filter((f) => {
|
|
191
|
+
if (this.activeWorkers.has(f.id))
|
|
192
|
+
return false;
|
|
193
|
+
const state = this.processedFeatures.get(f.id);
|
|
194
|
+
if (state?.status === 'failed') {
|
|
195
|
+
// Allow retry if feature was updated after our last attempt (user intervention)
|
|
196
|
+
if (f.updated_at && new Date(f.updated_at) > state.lastAttempt)
|
|
197
|
+
return true;
|
|
198
|
+
// Skip if max retries exceeded
|
|
199
|
+
if (state.retryCount >= this.options.maxRetries)
|
|
200
|
+
return false;
|
|
201
|
+
// Skip if within cooldown period
|
|
202
|
+
if (Date.now() - state.lastAttempt.getTime() < COOLDOWN_MS)
|
|
203
|
+
return false;
|
|
204
|
+
}
|
|
205
|
+
return true;
|
|
206
|
+
});
|
|
190
207
|
if (newFeatures.length === 0) {
|
|
208
|
+
// Log skipped features for debugging
|
|
209
|
+
const skippedCount = features.filter((f) => {
|
|
210
|
+
const state = this.processedFeatures.get(f.id);
|
|
211
|
+
return state?.status === 'failed' && state.retryCount >= this.options.maxRetries;
|
|
212
|
+
}).length;
|
|
213
|
+
if (skippedCount > 0) {
|
|
214
|
+
logInfo(`${skippedCount} feature(s) skipped (max retries exceeded)`);
|
|
215
|
+
}
|
|
191
216
|
if (this.options.verbose) {
|
|
192
|
-
logInfo('
|
|
217
|
+
logInfo('No features available for processing');
|
|
193
218
|
}
|
|
194
219
|
return;
|
|
195
220
|
}
|
|
@@ -329,13 +354,16 @@ export class AgentWorkflowProcessor {
|
|
|
329
354
|
else {
|
|
330
355
|
this.processedFeatures = updateFeatureState(this.processedFeatures, featureId, (currentState) => createFailedState(featureId, currentState));
|
|
331
356
|
logError(`Feature failed: ${featureName}${error ? ` - ${error}` : ''}`);
|
|
332
|
-
//
|
|
333
|
-
this.
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
357
|
+
// Only notify chat on first failure to avoid flooding with duplicate messages
|
|
358
|
+
const failedState = this.processedFeatures.get(featureId);
|
|
359
|
+
if (failedState && failedState.retryCount <= 1) {
|
|
360
|
+
this.notifyChatWorker({
|
|
361
|
+
type: 'event:phase_failed',
|
|
362
|
+
featureId,
|
|
363
|
+
phase: 'workflow',
|
|
364
|
+
error: error || 'Unknown error',
|
|
365
|
+
});
|
|
366
|
+
}
|
|
339
367
|
}
|
|
340
368
|
// Clear heartbeat feature info
|
|
341
369
|
sendHeartbeat().catch(() => { });
|
|
@@ -47,7 +47,9 @@ function getPendingPhases(workflow) {
|
|
|
47
47
|
*/
|
|
48
48
|
async function executePhase(phaseName, options, config) {
|
|
49
49
|
const { featureId, verbose } = options;
|
|
50
|
-
|
|
50
|
+
// Normalize phase name: kebab-case → snake_case (e.g., code-implementation → code_implementation)
|
|
51
|
+
const normalizedName = phaseName.replace(/-/g, '_');
|
|
52
|
+
const phaseRunner = PHASE_RUNNERS[normalizedName];
|
|
51
53
|
if (!phaseRunner) {
|
|
52
54
|
logWarning(`Unknown workflow phase: ${phaseName}, skipping`);
|
|
53
55
|
return {
|
|
@@ -58,7 +60,7 @@ async function executePhase(phaseName, options, config) {
|
|
|
58
60
|
};
|
|
59
61
|
}
|
|
60
62
|
if (verbose) {
|
|
61
|
-
logInfo(`\n🎯 Executing phase: ${
|
|
63
|
+
logInfo(`\n🎯 Executing phase: ${normalizedName}`);
|
|
62
64
|
}
|
|
63
65
|
// Execute the phase
|
|
64
66
|
const result = await phaseRunner(options, config);
|
|
@@ -66,7 +68,7 @@ async function executePhase(phaseName, options, config) {
|
|
|
66
68
|
logPhaseResult(result, verbose);
|
|
67
69
|
// Mark workflow phase as completed if successful
|
|
68
70
|
if (result.status === 'success') {
|
|
69
|
-
await markWorkflowPhaseCompleted(featureId,
|
|
71
|
+
await markWorkflowPhaseCompleted(featureId, normalizedName, verbose);
|
|
70
72
|
}
|
|
71
73
|
return result;
|
|
72
74
|
}
|
package/dist/index.js
CHANGED
|
File without changes
|
|
@@ -32,7 +32,32 @@ export function createChatMcpServer() {
|
|
|
32
32
|
}),
|
|
33
33
|
tool('update_execution_mode', 'Change the feature execution mode (e.g., from_user_stories_analysis, from_code_implementation).', {
|
|
34
34
|
feature_id: z.string().describe('Feature ID'),
|
|
35
|
-
execution_mode: z.
|
|
35
|
+
execution_mode: z.enum([
|
|
36
|
+
'full_pipeline',
|
|
37
|
+
'only_feature_analysis',
|
|
38
|
+
'only_user_stories_analysis',
|
|
39
|
+
'only_test_cases_analysis',
|
|
40
|
+
'only_technical_design',
|
|
41
|
+
'only_branch_planning',
|
|
42
|
+
'only_code_implementation',
|
|
43
|
+
'only_pr_splitting',
|
|
44
|
+
'only_pr_execution',
|
|
45
|
+
'only_functional_testing',
|
|
46
|
+
'only_code_refine',
|
|
47
|
+
'only_code_review',
|
|
48
|
+
'from_feature_analysis',
|
|
49
|
+
'from_user_stories_analysis',
|
|
50
|
+
'from_test_cases_analysis',
|
|
51
|
+
'from_technical_design',
|
|
52
|
+
'from_branch_planning',
|
|
53
|
+
'from_code_implementation',
|
|
54
|
+
'from_pr_splitting',
|
|
55
|
+
'from_pr_execution',
|
|
56
|
+
'from_functional_testing',
|
|
57
|
+
'from_code_review',
|
|
58
|
+
'custom',
|
|
59
|
+
'autonomous',
|
|
60
|
+
]).describe('New execution mode'),
|
|
36
61
|
}, async (args) => {
|
|
37
62
|
const result = await callMcpEndpoint('features/update', {
|
|
38
63
|
feature_id: args.feature_id,
|
|
@@ -42,18 +67,23 @@ export function createChatMcpServer() {
|
|
|
42
67
|
content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
|
|
43
68
|
};
|
|
44
69
|
}),
|
|
45
|
-
tool('update_workflow', 'Modify workflow phases — reset phases to pending, skip phases, or reorder.', {
|
|
70
|
+
tool('update_workflow', 'Modify workflow phases — reset phases to pending, skip phases, or reorder. Phase names must use snake_case (e.g., code_implementation, NOT code-implementation).', {
|
|
46
71
|
feature_id: z.string().describe('Feature ID'),
|
|
47
72
|
workflow: z
|
|
48
73
|
.array(z.object({
|
|
49
|
-
phase: z.string(),
|
|
74
|
+
phase: z.string().describe('Phase name in snake_case. Valid phases: feature_analysis, user_stories_analysis, test_cases_analysis, technical_design, branch_planning, code_implementation, pr_splitting, pr_execution, functional_testing, code_review, code_refine, autonomous'),
|
|
50
75
|
status: z.enum(['pending', 'completed', 'skipped']),
|
|
51
76
|
}))
|
|
52
77
|
.describe('Updated workflow array'),
|
|
53
78
|
}, async (args) => {
|
|
79
|
+
// Normalize phase names: kebab-case → snake_case
|
|
80
|
+
const normalizedWorkflow = args.workflow.map((p) => ({
|
|
81
|
+
...p,
|
|
82
|
+
phase: p.phase.replace(/-/g, '_'),
|
|
83
|
+
}));
|
|
54
84
|
const result = await callMcpEndpoint('features/update', {
|
|
55
85
|
feature_id: args.feature_id,
|
|
56
|
-
workflow:
|
|
86
|
+
workflow: normalizedWorkflow,
|
|
57
87
|
});
|
|
58
88
|
return {
|
|
59
89
|
content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
|
|
@@ -72,7 +102,7 @@ export function createChatMcpServer() {
|
|
|
72
102
|
.describe('Required for update/delete'),
|
|
73
103
|
title: z.string().optional(),
|
|
74
104
|
description: z.string().optional(),
|
|
75
|
-
status: z.enum(['draft', '
|
|
105
|
+
status: z.enum(['draft', 'pending_approval', 'approved']).optional(),
|
|
76
106
|
})),
|
|
77
107
|
}, async (args) => {
|
|
78
108
|
const results = [];
|
|
@@ -11,7 +11,7 @@ export const analyseFeature = async (options, config, checklistContext) => {
|
|
|
11
11
|
logInfo(`Starting feature analysis for feature ID: ${featureId}`);
|
|
12
12
|
}
|
|
13
13
|
try {
|
|
14
|
-
// Reset
|
|
14
|
+
// Reset pending_approval artifacts to draft so AI can manage them on re-run
|
|
15
15
|
const resetResult = await resetReadyArtifactsToDraft(featureId, verbose);
|
|
16
16
|
if (verbose && (resetResult.resetUserStories > 0 || resetResult.resetTestCases > 0)) {
|
|
17
17
|
logInfo(`✅ Reset ${resetResult.resetUserStories} user stories and ${resetResult.resetTestCases} test cases to draft for re-analysis`);
|
|
@@ -86,10 +86,10 @@ export const analyseFeature = async (options, config, checklistContext) => {
|
|
|
86
86
|
// Perform verification cycle
|
|
87
87
|
const verificationCycle = await performVerificationCycle(structuredAnalysisResult, checklistContext || null, context.featureContext, config, currentIteration, maxIterations, featureId, verbose);
|
|
88
88
|
verificationResult = verificationCycle.verificationResult;
|
|
89
|
-
// If verification passed, update ALL remaining draft artifacts to
|
|
89
|
+
// If verification passed, update ALL remaining draft artifacts to pending_approval and exit
|
|
90
90
|
if (verificationCycle.passed) {
|
|
91
91
|
if (verbose) {
|
|
92
|
-
logInfo('✅ Verification passed! Updating all draft artifacts to
|
|
92
|
+
logInfo('✅ Verification passed! Updating all draft artifacts to pending_approval status...');
|
|
93
93
|
}
|
|
94
94
|
const allDrafts = await getAllDraftArtifactIds(featureId, verbose);
|
|
95
95
|
await updateArtifactsToReady(allDrafts.userStoryIds, allDrafts.testCaseIds, verbose);
|
|
@@ -118,7 +118,7 @@ export const analyseFeature = async (options, config, checklistContext) => {
|
|
|
118
118
|
if (!structuredAnalysisResult) {
|
|
119
119
|
throw new Error('No analysis results received');
|
|
120
120
|
}
|
|
121
|
-
// If no checklist was used, update all draft artifacts to
|
|
121
|
+
// If no checklist was used, update all draft artifacts to pending_approval now
|
|
122
122
|
if (!checklistContext ||
|
|
123
123
|
checklistContext.checklists.length === 0 ||
|
|
124
124
|
!verificationResult) {
|
|
@@ -126,7 +126,7 @@ export const analyseFeature = async (options, config, checklistContext) => {
|
|
|
126
126
|
if (allDrafts.userStoryIds.length > 0 ||
|
|
127
127
|
allDrafts.testCaseIds.length > 0) {
|
|
128
128
|
if (verbose) {
|
|
129
|
-
logInfo('✅ No checklist verification needed. Updating all draft artifacts to
|
|
129
|
+
logInfo('✅ No checklist verification needed. Updating all draft artifacts to pending_approval status...');
|
|
130
130
|
}
|
|
131
131
|
await updateArtifactsToReady(allDrafts.userStoryIds, allDrafts.testCaseIds, verbose);
|
|
132
132
|
}
|
|
@@ -143,7 +143,7 @@ export const analyseFeature = async (options, config, checklistContext) => {
|
|
|
143
143
|
throw new Error(`Checklist verification failed after ${currentIteration} iterations`);
|
|
144
144
|
}
|
|
145
145
|
// Return success result
|
|
146
|
-
// Note: Artifacts have already been saved and updated to '
|
|
146
|
+
// Note: Artifacts have already been saved and updated to 'pending_approval' status (if verification passed)
|
|
147
147
|
// or remain as draft (if verification failed)
|
|
148
148
|
return buildAnalysisResult(featureId, context.featureContext, structuredAnalysisResult, currentIteration);
|
|
149
149
|
}
|
|
@@ -24,7 +24,7 @@ export declare function deleteArtifacts(userStoryIds: string[], testCaseIds: str
|
|
|
24
24
|
*/
|
|
25
25
|
export declare function deleteSpecificArtifacts(featureId: string, deletedUserStoryIds: string[], deletedTestCaseIds: string[], deletionReasons: Record<string, string>, verbose?: boolean): Promise<void>;
|
|
26
26
|
/**
|
|
27
|
-
* Update artifacts to
|
|
27
|
+
* Update artifacts to pending_approval status after successful verification
|
|
28
28
|
*/
|
|
29
29
|
export declare function updateArtifactsToReady(userStoryIds: string[], testCaseIds: string[], verbose?: boolean): Promise<void>;
|
|
30
30
|
/**
|
|
@@ -10,10 +10,10 @@ export async function resetReadyArtifactsToDraft(featureId, verbose) {
|
|
|
10
10
|
getTestCases(featureId, false),
|
|
11
11
|
]);
|
|
12
12
|
const readyStoryIds = stories
|
|
13
|
-
.filter((s) => s.status === '
|
|
13
|
+
.filter((s) => s.status === 'pending_approval')
|
|
14
14
|
.map((s) => s.id);
|
|
15
15
|
const readyTestCaseIds = testCases
|
|
16
|
-
.filter((tc) => tc.status === '
|
|
16
|
+
.filter((tc) => tc.status === 'pending_approval')
|
|
17
17
|
.map((tc) => tc.id);
|
|
18
18
|
if (readyStoryIds.length > 0) {
|
|
19
19
|
if (verbose) {
|
|
@@ -144,20 +144,20 @@ export async function deleteSpecificArtifacts(featureId, deletedUserStoryIds, de
|
|
|
144
144
|
}
|
|
145
145
|
}
|
|
146
146
|
/**
|
|
147
|
-
* Update artifacts to
|
|
147
|
+
* Update artifacts to pending_approval status after successful verification
|
|
148
148
|
*/
|
|
149
149
|
export async function updateArtifactsToReady(userStoryIds, testCaseIds, verbose) {
|
|
150
150
|
if (userStoryIds.length > 0) {
|
|
151
151
|
if (verbose) {
|
|
152
|
-
logInfo(`Updating ${userStoryIds.length} user stories from draft to
|
|
152
|
+
logInfo(`Updating ${userStoryIds.length} user stories from draft to pending_approval...`);
|
|
153
153
|
}
|
|
154
|
-
await batchUpdateUserStoryStatus(userStoryIds, '
|
|
154
|
+
await batchUpdateUserStoryStatus(userStoryIds, 'pending_approval', verbose);
|
|
155
155
|
}
|
|
156
156
|
if (testCaseIds.length > 0) {
|
|
157
157
|
if (verbose) {
|
|
158
|
-
logInfo(`Updating ${testCaseIds.length} test cases from draft to
|
|
158
|
+
logInfo(`Updating ${testCaseIds.length} test cases from draft to pending_approval...`);
|
|
159
159
|
}
|
|
160
|
-
await batchUpdateTestCaseStatus(testCaseIds, '
|
|
160
|
+
await batchUpdateTestCaseStatus(testCaseIds, 'pending_approval', verbose);
|
|
161
161
|
}
|
|
162
162
|
}
|
|
163
163
|
/**
|
|
@@ -230,7 +230,7 @@ export function buildAnalysisResult(featureId, context, structuredAnalysisResult
|
|
|
230
230
|
featureInfo: context.feature,
|
|
231
231
|
existingUserStories: context.existing_user_stories.map((story) => ({
|
|
232
232
|
...story,
|
|
233
|
-
status: (story.status === '
|
|
233
|
+
status: (story.status === 'pending_approval' ? 'pending_approval' : story.status === 'approved' ? 'approved' : 'draft'),
|
|
234
234
|
created_at: story.created_at || new Date().toISOString(),
|
|
235
235
|
updated_at: story.updated_at || new Date().toISOString(),
|
|
236
236
|
})),
|
|
@@ -11,10 +11,10 @@ export const analyseTestCases = async (options, config, checklistContext) => {
|
|
|
11
11
|
logInfo(`Starting test cases analysis for feature ID: ${featureId}`);
|
|
12
12
|
}
|
|
13
13
|
try {
|
|
14
|
-
// Reset
|
|
14
|
+
// Reset pending_approval test cases to draft so AI can manage them on re-run
|
|
15
15
|
const resetCount = await resetReadyTestCasesToDraft(featureId, verbose);
|
|
16
16
|
if (verbose && resetCount > 0) {
|
|
17
|
-
logInfo(`✅ Reset ${resetCount}
|
|
17
|
+
logInfo(`✅ Reset ${resetCount} pending_approval test cases to draft for re-analysis`);
|
|
18
18
|
}
|
|
19
19
|
const context = await prepareTestCasesAnalysisContext(featureId, checklistContext, verbose);
|
|
20
20
|
const systemPrompt = createTestCasesAnalysisSystemPrompt();
|
|
@@ -83,7 +83,7 @@ export const analyseTestCases = async (options, config, checklistContext) => {
|
|
|
83
83
|
verificationResult = verificationCycle.verificationResult;
|
|
84
84
|
if (verificationCycle.passed) {
|
|
85
85
|
if (verbose) {
|
|
86
|
-
logInfo('✅ Verification passed! Updating all draft test cases to
|
|
86
|
+
logInfo('✅ Verification passed! Updating all draft test cases to pending_approval status...');
|
|
87
87
|
}
|
|
88
88
|
// Update ALL remaining draft test cases (both kept old ones and newly created)
|
|
89
89
|
const allDraftIds = await getAllDraftTestCaseIds(featureId, verbose);
|
|
@@ -107,14 +107,14 @@ export const analyseTestCases = async (options, config, checklistContext) => {
|
|
|
107
107
|
if (!structuredAnalysisResult) {
|
|
108
108
|
throw new Error('No analysis results received');
|
|
109
109
|
}
|
|
110
|
-
// If no checklist was used, update all draft artifacts to
|
|
110
|
+
// If no checklist was used, update all draft artifacts to pending_approval
|
|
111
111
|
if (!checklistContext ||
|
|
112
112
|
checklistContext.checklists.length === 0 ||
|
|
113
113
|
!verificationResult) {
|
|
114
114
|
const allDraftIds = await getAllDraftTestCaseIds(featureId, verbose);
|
|
115
115
|
if (allDraftIds.length > 0) {
|
|
116
116
|
if (verbose) {
|
|
117
|
-
logInfo('✅ No checklist verification needed. Updating all draft test cases to
|
|
117
|
+
logInfo('✅ No checklist verification needed. Updating all draft test cases to pending_approval status...');
|
|
118
118
|
}
|
|
119
119
|
await updateTestCasesToReady(allDraftIds, verbose);
|
|
120
120
|
}
|
|
@@ -4,7 +4,7 @@ import { callMcpEndpoint } from '../../api/mcp-client.js';
|
|
|
4
4
|
export async function resetReadyTestCasesToDraft(featureId, verbose) {
|
|
5
5
|
const testCases = await getTestCases(featureId, false);
|
|
6
6
|
const readyIds = testCases
|
|
7
|
-
.filter((tc) => tc.status === '
|
|
7
|
+
.filter((tc) => tc.status === 'pending_approval')
|
|
8
8
|
.map((tc) => tc.id);
|
|
9
9
|
if (readyIds.length === 0) {
|
|
10
10
|
return 0;
|
|
@@ -70,9 +70,9 @@ export async function deleteSpecificTestCases(featureId, deletedTestCaseIds, del
|
|
|
70
70
|
export async function updateTestCasesToReady(testCaseIds, verbose) {
|
|
71
71
|
if (testCaseIds.length > 0) {
|
|
72
72
|
if (verbose) {
|
|
73
|
-
logInfo(`Updating ${testCaseIds.length} test cases from draft to
|
|
73
|
+
logInfo(`Updating ${testCaseIds.length} test cases from draft to pending_approval...`);
|
|
74
74
|
}
|
|
75
|
-
await batchUpdateTestCaseStatus(testCaseIds, '
|
|
75
|
+
await batchUpdateTestCaseStatus(testCaseIds, 'pending_approval', verbose);
|
|
76
76
|
}
|
|
77
77
|
}
|
|
78
78
|
export async function saveTestCasesAsDraft(featureId, created_test_cases, verbose) {
|
|
@@ -11,10 +11,10 @@ export const analyseUserStories = async (options, config, checklistContext) => {
|
|
|
11
11
|
logInfo(`Starting user stories analysis for feature ID: ${featureId}`);
|
|
12
12
|
}
|
|
13
13
|
try {
|
|
14
|
-
// Reset
|
|
14
|
+
// Reset pending_approval user stories to draft so AI can manage them on re-run
|
|
15
15
|
const resetCount = await resetReadyUserStoriesToDraft(featureId, verbose);
|
|
16
16
|
if (verbose && resetCount > 0) {
|
|
17
|
-
logInfo(`✅ Reset ${resetCount}
|
|
17
|
+
logInfo(`✅ Reset ${resetCount} pending_approval user stories to draft for re-analysis`);
|
|
18
18
|
}
|
|
19
19
|
const context = await prepareUserStoriesAnalysisContext(featureId, checklistContext, verbose);
|
|
20
20
|
const systemPrompt = createUserStoriesAnalysisSystemPrompt();
|
|
@@ -83,7 +83,7 @@ export const analyseUserStories = async (options, config, checklistContext) => {
|
|
|
83
83
|
verificationResult = verificationCycle.verificationResult;
|
|
84
84
|
if (verificationCycle.passed) {
|
|
85
85
|
if (verbose) {
|
|
86
|
-
logInfo('✅ Verification passed! Updating all draft user stories to
|
|
86
|
+
logInfo('✅ Verification passed! Updating all draft user stories to pending_approval status...');
|
|
87
87
|
}
|
|
88
88
|
// Update ALL remaining draft stories (both kept old ones and newly created)
|
|
89
89
|
const allDraftIds = await getAllDraftUserStoryIds(featureId, verbose);
|
|
@@ -107,14 +107,14 @@ export const analyseUserStories = async (options, config, checklistContext) => {
|
|
|
107
107
|
if (!structuredAnalysisResult) {
|
|
108
108
|
throw new Error('No analysis results received');
|
|
109
109
|
}
|
|
110
|
-
// If no checklist was used, update all draft artifacts to
|
|
110
|
+
// If no checklist was used, update all draft artifacts to pending_approval
|
|
111
111
|
if (!checklistContext ||
|
|
112
112
|
checklistContext.checklists.length === 0 ||
|
|
113
113
|
!verificationResult) {
|
|
114
114
|
const allDraftIds = await getAllDraftUserStoryIds(featureId, verbose);
|
|
115
115
|
if (allDraftIds.length > 0) {
|
|
116
116
|
if (verbose) {
|
|
117
|
-
logInfo('✅ No checklist verification needed. Updating all draft user stories to
|
|
117
|
+
logInfo('✅ No checklist verification needed. Updating all draft user stories to pending_approval status...');
|
|
118
118
|
}
|
|
119
119
|
await updateUserStoriesToReady(allDraftIds, verbose);
|
|
120
120
|
}
|
|
@@ -4,7 +4,7 @@ import { callMcpEndpoint } from '../../api/mcp-client.js';
|
|
|
4
4
|
export async function resetReadyUserStoriesToDraft(featureId, verbose) {
|
|
5
5
|
const stories = await getUserStories(featureId, false);
|
|
6
6
|
const readyStoryIds = stories
|
|
7
|
-
.filter((story) => story.status === '
|
|
7
|
+
.filter((story) => story.status === 'pending_approval')
|
|
8
8
|
.map((story) => story.id);
|
|
9
9
|
if (readyStoryIds.length === 0) {
|
|
10
10
|
return 0;
|
|
@@ -70,9 +70,9 @@ export async function deleteSpecificUserStories(featureId, deletedUserStoryIds,
|
|
|
70
70
|
export async function updateUserStoriesToReady(userStoryIds, verbose) {
|
|
71
71
|
if (userStoryIds.length > 0) {
|
|
72
72
|
if (verbose) {
|
|
73
|
-
logInfo(`Updating ${userStoryIds.length} user stories from draft to
|
|
73
|
+
logInfo(`Updating ${userStoryIds.length} user stories from draft to pending_approval...`);
|
|
74
74
|
}
|
|
75
|
-
await batchUpdateUserStoryStatus(userStoryIds, '
|
|
75
|
+
await batchUpdateUserStoryStatus(userStoryIds, 'pending_approval', verbose);
|
|
76
76
|
}
|
|
77
77
|
}
|
|
78
78
|
export async function saveUserStoriesAsDraft(featureId, created_user_stories, verbose) {
|
|
@@ -111,7 +111,7 @@ export function buildUserStoriesAnalysisResult(featureId, context, structuredAna
|
|
|
111
111
|
featureInfo: context.feature,
|
|
112
112
|
existingUserStories: context.existing_user_stories.map((story) => ({
|
|
113
113
|
...story,
|
|
114
|
-
status: (story.status === '
|
|
114
|
+
status: (story.status === 'pending_approval' ? 'pending_approval' : 'draft'),
|
|
115
115
|
created_at: story.created_at || new Date().toISOString(),
|
|
116
116
|
updated_at: story.updated_at || new Date().toISOString(),
|
|
117
117
|
})),
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Unit tests for phase quality criteria definitions
|
|
3
|
+
*/
|
|
4
|
+
import { describe, it } from 'node:test';
|
|
5
|
+
import assert from 'node:assert';
|
|
6
|
+
import { DEFAULT_PHASE_CRITERIA, USER_STORIES_ANALYSIS_CRITERIA, TEST_CASES_ANALYSIS_CRITERIA, TECHNICAL_DESIGN_CRITERIA, BRANCH_PLANNING_CRITERIA, CODE_IMPLEMENTATION_CRITERIA, FUNCTIONAL_TESTING_CRITERIA, CODE_REVIEW_CRITERIA, getPhaseQualityCriteria, } from '../phase-criteria.js';
|
|
7
|
+
describe('Phase Quality Criteria', () => {
|
|
8
|
+
describe('DEFAULT_PHASE_CRITERIA', () => {
|
|
9
|
+
it('should cover all evaluable phases', () => {
|
|
10
|
+
const expectedPhases = [
|
|
11
|
+
'user_stories_analysis',
|
|
12
|
+
'test_cases_analysis',
|
|
13
|
+
'technical_design',
|
|
14
|
+
'branch_planning',
|
|
15
|
+
'code_implementation',
|
|
16
|
+
'functional_testing',
|
|
17
|
+
'code_review',
|
|
18
|
+
];
|
|
19
|
+
for (const phase of expectedPhases) {
|
|
20
|
+
assert.ok(phase in DEFAULT_PHASE_CRITERIA, `Should have criteria for phase: ${phase}`);
|
|
21
|
+
}
|
|
22
|
+
});
|
|
23
|
+
it('should have consistent phase names in criteria objects', () => {
|
|
24
|
+
for (const [key, criteria] of Object.entries(DEFAULT_PHASE_CRITERIA)) {
|
|
25
|
+
assert.strictEqual(criteria.phase, key, `Criteria for ${key} should have matching phase name`);
|
|
26
|
+
}
|
|
27
|
+
});
|
|
28
|
+
});
|
|
29
|
+
describe('Criteria Structural Validity', () => {
|
|
30
|
+
const allCriteria = [
|
|
31
|
+
USER_STORIES_ANALYSIS_CRITERIA,
|
|
32
|
+
TEST_CASES_ANALYSIS_CRITERIA,
|
|
33
|
+
TECHNICAL_DESIGN_CRITERIA,
|
|
34
|
+
BRANCH_PLANNING_CRITERIA,
|
|
35
|
+
CODE_IMPLEMENTATION_CRITERIA,
|
|
36
|
+
FUNCTIONAL_TESTING_CRITERIA,
|
|
37
|
+
CODE_REVIEW_CRITERIA,
|
|
38
|
+
];
|
|
39
|
+
for (const phaseCriteria of allCriteria) {
|
|
40
|
+
describe(phaseCriteria.phase, () => {
|
|
41
|
+
it('should have advanceThreshold > escalateThreshold', () => {
|
|
42
|
+
assert.ok(phaseCriteria.advanceThreshold > phaseCriteria.escalateThreshold, `advanceThreshold (${phaseCriteria.advanceThreshold}) should be > escalateThreshold (${phaseCriteria.escalateThreshold})`);
|
|
43
|
+
});
|
|
44
|
+
it('should have thresholds in valid range (0-100)', () => {
|
|
45
|
+
assert.ok(phaseCriteria.advanceThreshold >= 0);
|
|
46
|
+
assert.ok(phaseCriteria.advanceThreshold <= 100);
|
|
47
|
+
assert.ok(phaseCriteria.escalateThreshold >= 0);
|
|
48
|
+
assert.ok(phaseCriteria.escalateThreshold <= 100);
|
|
49
|
+
});
|
|
50
|
+
it('should have maxAutoRetries >= 1', () => {
|
|
51
|
+
assert.ok(phaseCriteria.maxAutoRetries >= 1, `maxAutoRetries should be >= 1, got ${phaseCriteria.maxAutoRetries}`);
|
|
52
|
+
});
|
|
53
|
+
it('should have at least one criterion', () => {
|
|
54
|
+
assert.ok(phaseCriteria.criteria.length > 0, 'Should have at least one criterion');
|
|
55
|
+
});
|
|
56
|
+
it('should have criteria weights that approximately sum to 1', () => {
|
|
57
|
+
const totalWeight = phaseCriteria.criteria.reduce((sum, c) => sum + c.weight, 0);
|
|
58
|
+
assert.ok(Math.abs(totalWeight - 1.0) < 0.01, `Weights should sum to ~1.0, got ${totalWeight}`);
|
|
59
|
+
});
|
|
60
|
+
it('should have unique criterion IDs', () => {
|
|
61
|
+
const ids = phaseCriteria.criteria.map((c) => c.id);
|
|
62
|
+
const uniqueIds = new Set(ids);
|
|
63
|
+
assert.strictEqual(ids.length, uniqueIds.size, 'Criterion IDs should be unique');
|
|
64
|
+
});
|
|
65
|
+
it('should have valid criterion weights (0 < weight <= 1)', () => {
|
|
66
|
+
for (const criterion of phaseCriteria.criteria) {
|
|
67
|
+
assert.ok(criterion.weight > 0 && criterion.weight <= 1, `Weight for ${criterion.id} should be between 0 and 1, got ${criterion.weight}`);
|
|
68
|
+
}
|
|
69
|
+
});
|
|
70
|
+
it('should have valid minimum scores (0-100)', () => {
|
|
71
|
+
for (const criterion of phaseCriteria.criteria) {
|
|
72
|
+
assert.ok(criterion.minimumScore >= 0 && criterion.minimumScore <= 100, `minimumScore for ${criterion.id} should be 0-100, got ${criterion.minimumScore}`);
|
|
73
|
+
}
|
|
74
|
+
});
|
|
75
|
+
it('should have non-empty evaluation guidance', () => {
|
|
76
|
+
for (const criterion of phaseCriteria.criteria) {
|
|
77
|
+
assert.ok(criterion.evaluationGuidance.length > 0, `Criterion ${criterion.id} should have evaluation guidance`);
|
|
78
|
+
}
|
|
79
|
+
});
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
});
|
|
83
|
+
describe('getPhaseQualityCriteria', () => {
|
|
84
|
+
it('should return default criteria for known phases', () => {
|
|
85
|
+
const criteria = getPhaseQualityCriteria('user_stories_analysis');
|
|
86
|
+
assert.ok(criteria);
|
|
87
|
+
assert.strictEqual(criteria.phase, 'user_stories_analysis');
|
|
88
|
+
assert.strictEqual(criteria.advanceThreshold, USER_STORIES_ANALYSIS_CRITERIA.advanceThreshold);
|
|
89
|
+
});
|
|
90
|
+
it('should return null for unknown phases', () => {
|
|
91
|
+
const criteria = getPhaseQualityCriteria('nonexistent_phase');
|
|
92
|
+
assert.strictEqual(criteria, null);
|
|
93
|
+
});
|
|
94
|
+
it('should apply overrides when provided', () => {
|
|
95
|
+
const criteria = getPhaseQualityCriteria('user_stories_analysis', {
|
|
96
|
+
user_stories_analysis: {
|
|
97
|
+
advanceThreshold: 90,
|
|
98
|
+
maxAutoRetries: 5,
|
|
99
|
+
},
|
|
100
|
+
});
|
|
101
|
+
assert.ok(criteria);
|
|
102
|
+
assert.strictEqual(criteria.advanceThreshold, 90);
|
|
103
|
+
assert.strictEqual(criteria.maxAutoRetries, 5);
|
|
104
|
+
// Non-overridden values should remain from defaults
|
|
105
|
+
assert.strictEqual(criteria.escalateThreshold, USER_STORIES_ANALYSIS_CRITERIA.escalateThreshold);
|
|
106
|
+
});
|
|
107
|
+
it('should return defaults when override map does not include the phase', () => {
|
|
108
|
+
const criteria = getPhaseQualityCriteria('technical_design', {
|
|
109
|
+
user_stories_analysis: { advanceThreshold: 90 },
|
|
110
|
+
});
|
|
111
|
+
assert.ok(criteria);
|
|
112
|
+
assert.strictEqual(criteria.advanceThreshold, TECHNICAL_DESIGN_CRITERIA.advanceThreshold);
|
|
113
|
+
});
|
|
114
|
+
it('should override criteria array when provided', () => {
|
|
115
|
+
const customCriteria = [
|
|
116
|
+
{
|
|
117
|
+
id: 'custom_1',
|
|
118
|
+
name: 'Custom',
|
|
119
|
+
description: 'A custom criterion',
|
|
120
|
+
weight: 1.0,
|
|
121
|
+
minimumScore: 50,
|
|
122
|
+
evaluationGuidance: 'Custom guidance',
|
|
123
|
+
},
|
|
124
|
+
];
|
|
125
|
+
const criteria = getPhaseQualityCriteria('technical_design', {
|
|
126
|
+
technical_design: { criteria: customCriteria },
|
|
127
|
+
});
|
|
128
|
+
assert.ok(criteria);
|
|
129
|
+
assert.strictEqual(criteria.criteria.length, 1);
|
|
130
|
+
assert.strictEqual(criteria.criteria[0].id, 'custom_1');
|
|
131
|
+
});
|
|
132
|
+
});
|
|
133
|
+
});
|