edsger 0.29.0 → 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.
Files changed (116) hide show
  1. package/dist/commands/agent-workflow/processor.js +38 -10
  2. package/dist/commands/workflow/feature-coordinator.js +5 -3
  3. package/dist/index.js +0 -0
  4. package/dist/phases/chat-processor/prompts.d.ts +1 -1
  5. package/dist/phases/chat-processor/prompts.js +24 -1
  6. package/dist/phases/chat-processor/tools.js +40 -5
  7. package/dist/phases/feature-analysis/index.js +6 -6
  8. package/dist/phases/feature-analysis/outcome.d.ts +1 -1
  9. package/dist/phases/feature-analysis/outcome.js +8 -8
  10. package/dist/phases/test-cases-analysis/index.js +5 -5
  11. package/dist/phases/test-cases-analysis/outcome.js +3 -3
  12. package/dist/phases/user-stories-analysis/index.js +5 -5
  13. package/dist/phases/user-stories-analysis/outcome.js +4 -4
  14. package/dist/services/lifecycle-agent/__tests__/phase-criteria.test.d.ts +4 -0
  15. package/dist/services/lifecycle-agent/__tests__/phase-criteria.test.js +133 -0
  16. package/dist/services/lifecycle-agent/__tests__/transition-rules.test.d.ts +4 -0
  17. package/dist/services/lifecycle-agent/__tests__/transition-rules.test.js +336 -0
  18. package/dist/services/lifecycle-agent/index.d.ts +24 -0
  19. package/dist/services/lifecycle-agent/index.js +25 -0
  20. package/dist/services/lifecycle-agent/phase-criteria.d.ts +57 -0
  21. package/dist/services/lifecycle-agent/phase-criteria.js +335 -0
  22. package/dist/services/lifecycle-agent/transition-rules.d.ts +60 -0
  23. package/dist/services/lifecycle-agent/transition-rules.js +184 -0
  24. package/dist/services/lifecycle-agent/types.d.ts +190 -0
  25. package/dist/services/lifecycle-agent/types.js +12 -0
  26. package/dist/types/features.d.ts +2 -2
  27. package/dist/types/index.d.ts +2 -2
  28. package/package.json +1 -1
  29. package/.claude/settings.local.json +0 -28
  30. package/.env.local +0 -12
  31. package/dist/api/features/__tests__/regression-prevention.test.d.ts +0 -5
  32. package/dist/api/features/__tests__/regression-prevention.test.js +0 -338
  33. package/dist/api/features/__tests__/status-updater.integration.test.d.ts +0 -5
  34. package/dist/api/features/__tests__/status-updater.integration.test.js +0 -497
  35. package/dist/commands/workflow/pipeline-runner.d.ts +0 -17
  36. package/dist/commands/workflow/pipeline-runner.js +0 -393
  37. package/dist/commands/workflow/runner.d.ts +0 -26
  38. package/dist/commands/workflow/runner.js +0 -119
  39. package/dist/commands/workflow/workflow-runner.d.ts +0 -26
  40. package/dist/commands/workflow/workflow-runner.js +0 -119
  41. package/dist/phases/code-implementation/analyzer-helpers.d.ts +0 -28
  42. package/dist/phases/code-implementation/analyzer-helpers.js +0 -177
  43. package/dist/phases/code-implementation/analyzer.d.ts +0 -32
  44. package/dist/phases/code-implementation/analyzer.js +0 -629
  45. package/dist/phases/code-implementation/context-fetcher.d.ts +0 -17
  46. package/dist/phases/code-implementation/context-fetcher.js +0 -86
  47. package/dist/phases/code-implementation/mcp-server.d.ts +0 -1
  48. package/dist/phases/code-implementation/mcp-server.js +0 -93
  49. package/dist/phases/code-implementation/prompts-improvement.d.ts +0 -5
  50. package/dist/phases/code-implementation/prompts-improvement.js +0 -108
  51. package/dist/phases/code-implementation-verification/verifier.d.ts +0 -31
  52. package/dist/phases/code-implementation-verification/verifier.js +0 -196
  53. package/dist/phases/code-refine/analyzer.d.ts +0 -41
  54. package/dist/phases/code-refine/analyzer.js +0 -561
  55. package/dist/phases/code-refine/context-fetcher.d.ts +0 -94
  56. package/dist/phases/code-refine/context-fetcher.js +0 -423
  57. package/dist/phases/code-refine-verification/analysis/llm-analyzer.d.ts +0 -22
  58. package/dist/phases/code-refine-verification/analysis/llm-analyzer.js +0 -134
  59. package/dist/phases/code-refine-verification/verifier.d.ts +0 -47
  60. package/dist/phases/code-refine-verification/verifier.js +0 -597
  61. package/dist/phases/code-review/analyzer.d.ts +0 -29
  62. package/dist/phases/code-review/analyzer.js +0 -363
  63. package/dist/phases/code-review/context-fetcher.d.ts +0 -92
  64. package/dist/phases/code-review/context-fetcher.js +0 -296
  65. package/dist/phases/feature-analysis/analyzer-helpers.d.ts +0 -10
  66. package/dist/phases/feature-analysis/analyzer-helpers.js +0 -47
  67. package/dist/phases/feature-analysis/analyzer.d.ts +0 -11
  68. package/dist/phases/feature-analysis/analyzer.js +0 -208
  69. package/dist/phases/feature-analysis/context-fetcher.d.ts +0 -26
  70. package/dist/phases/feature-analysis/context-fetcher.js +0 -134
  71. package/dist/phases/feature-analysis/http-fallback.d.ts +0 -20
  72. package/dist/phases/feature-analysis/http-fallback.js +0 -95
  73. package/dist/phases/feature-analysis/mcp-server.d.ts +0 -1
  74. package/dist/phases/feature-analysis/mcp-server.js +0 -144
  75. package/dist/phases/feature-analysis/prompts-improvement.d.ts +0 -8
  76. package/dist/phases/feature-analysis/prompts-improvement.js +0 -109
  77. package/dist/phases/feature-analysis-verification/verifier.d.ts +0 -37
  78. package/dist/phases/feature-analysis-verification/verifier.js +0 -147
  79. package/dist/phases/technical-design/analyzer-helpers.d.ts +0 -25
  80. package/dist/phases/technical-design/analyzer-helpers.js +0 -39
  81. package/dist/phases/technical-design/analyzer.d.ts +0 -21
  82. package/dist/phases/technical-design/analyzer.js +0 -461
  83. package/dist/phases/technical-design/context-fetcher.d.ts +0 -12
  84. package/dist/phases/technical-design/context-fetcher.js +0 -39
  85. package/dist/phases/technical-design/http-fallback.d.ts +0 -17
  86. package/dist/phases/technical-design/http-fallback.js +0 -151
  87. package/dist/phases/technical-design/mcp-server.d.ts +0 -1
  88. package/dist/phases/technical-design/mcp-server.js +0 -157
  89. package/dist/phases/technical-design/prompts-improvement.d.ts +0 -5
  90. package/dist/phases/technical-design/prompts-improvement.js +0 -93
  91. package/dist/phases/technical-design-verification/verifier.d.ts +0 -53
  92. package/dist/phases/technical-design-verification/verifier.js +0 -170
  93. package/dist/services/feature-branches.d.ts +0 -77
  94. package/dist/services/feature-branches.js +0 -205
  95. package/dist/workflow-runner/config/phase-configs.d.ts +0 -5
  96. package/dist/workflow-runner/config/phase-configs.js +0 -120
  97. package/dist/workflow-runner/core/feature-filter.d.ts +0 -16
  98. package/dist/workflow-runner/core/feature-filter.js +0 -46
  99. package/dist/workflow-runner/core/index.d.ts +0 -8
  100. package/dist/workflow-runner/core/index.js +0 -12
  101. package/dist/workflow-runner/core/pipeline-evaluator.d.ts +0 -24
  102. package/dist/workflow-runner/core/pipeline-evaluator.js +0 -32
  103. package/dist/workflow-runner/core/state-manager.d.ts +0 -24
  104. package/dist/workflow-runner/core/state-manager.js +0 -42
  105. package/dist/workflow-runner/core/workflow-logger.d.ts +0 -20
  106. package/dist/workflow-runner/core/workflow-logger.js +0 -65
  107. package/dist/workflow-runner/executors/phase-executor.d.ts +0 -8
  108. package/dist/workflow-runner/executors/phase-executor.js +0 -248
  109. package/dist/workflow-runner/feature-workflow-runner.d.ts +0 -26
  110. package/dist/workflow-runner/feature-workflow-runner.js +0 -119
  111. package/dist/workflow-runner/index.d.ts +0 -2
  112. package/dist/workflow-runner/index.js +0 -2
  113. package/dist/workflow-runner/pipeline-runner.d.ts +0 -17
  114. package/dist/workflow-runner/pipeline-runner.js +0 -393
  115. package/dist/workflow-runner/workflow-processor.d.ts +0 -54
  116. 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 newFeatures = features.filter((f) => !this.activeWorkers.has(f.id));
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('All available features already being processed');
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
- // Notify chat worker about the failure
333
- this.notifyChatWorker({
334
- type: 'event:phase_failed',
335
- featureId,
336
- phase: 'workflow',
337
- error: error || 'Unknown error',
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
- const phaseRunner = PHASE_RUNNERS[phaseName];
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: ${phaseName}`);
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, phaseName, verbose);
71
+ await markWorkflowPhaseCompleted(featureId, normalizedName, verbose);
70
72
  }
71
73
  return result;
72
74
  }
package/dist/index.js CHANGED
File without changes
@@ -5,7 +5,7 @@
5
5
  * 1. CHAT_RESPONSE_PROMPT — responding to human messages
6
6
  * 2. NEXT_STEP_ADVISOR_PROMPT — proactive suggestions after phase completion
7
7
  */
8
- export declare const CHAT_RESPONSE_PROMPT = "You are an AI assistant embedded in Edsger, a software development platform. You are helping a team develop a feature.\n\n## Your Capabilities\nYou can see the feature's current state, user stories, test cases, workflow phases, and code. You have tools to:\n- Modify feature status, execution mode, and workflow phases\n- Create/update user stories and test cases\n- Read and search source code files\n- Send follow-up messages and present options to the user\n- Trigger phase reruns\n- Create tasks for team members (human) or AI\n- Look up product team members by name\n\n## How to Respond\n1. **Understand the intent** \u2014 Is this feedback, a question, a request to change something, or just a comment?\n2. **Take action if needed** \u2014 Use the appropriate tools to make changes\n3. **Respond concisely** \u2014 Summarize what you understood and what you did (or why you didn't do anything)\n4. **Ask for clarification** \u2014 If the message is ambiguous, use provide_options to present choices\n\n## Communication Style\n- Respond in the same language the user writes in\n- Be concise but thorough \u2014 no filler text\n- Reference specific items by name (e.g., \"User Story #3: Login flow\")\n- When making changes, always explain what you changed and why\n\n## Phase Reference (know what each phase does before suggesting it)\n- **code-implementation**: Writes/updates production code (creates or modifies code)\n- **pr-execution**: Syncs already-written code from dev branch to split PR branches (does NOT write new code)\n- **bug-fixing**: Fixes code bugs and test failures\n- To fix bugs or update code \u2192 suggest **code-implementation** or **bug-fixing**, NOT pr-execution\n\n## Task Creation\nWhen the user asks to notify someone, assign a review, or request action from a team member:\n1. Use list_product_members to find the person by name\n2. Use create_task with executor=\"human\" and the resolved user ID\n3. Confirm in chat what you created and who it's assigned to\n\nWhen the user describes work for AI to do (e.g., \"implement X\", \"fix Y\"):\n1. Use create_task with executor=\"ai\" \u2014 the task worker will pick it up automatically\n\n## Important Rules\n- Never make destructive changes without confirmation (deleting stories, resetting phases)\n- For ambiguous feedback, present options rather than guessing\n- If you can't do something, explain why clearly\n- When creating tasks for people, always confirm the person's identity if the name is ambiguous\n";
8
+ export declare const CHAT_RESPONSE_PROMPT = "You are an AI assistant embedded in Edsger, a software development platform. You are helping a team develop a feature.\n\n## Your Capabilities\nYou can see the feature's current state, user stories, test cases, workflow phases, and code. You have tools to:\n- Modify feature status, execution mode, and workflow phases\n- Create/update user stories and test cases\n- Read and search source code files\n- Send follow-up messages and present options to the user\n- Trigger phase reruns\n- Create tasks for team members (human) or AI\n- Look up product team members by name\n\n## How to Respond\n1. **Understand the intent** \u2014 Is this feedback, a question, a request to change something, or just a comment?\n2. **Take action if needed** \u2014 Use the appropriate tools to make changes\n3. **Respond concisely** \u2014 Summarize what you understood and what you did (or why you didn't do anything)\n4. **Ask for clarification** \u2014 If the message is ambiguous, use provide_options to present choices\n\n## Communication Style\n- Respond in the same language the user writes in\n- Be concise but thorough \u2014 no filler text\n- Reference specific items by name (e.g., \"User Story #3: Login flow\")\n- When making changes, always explain what you changed and why\n\n## Phase Reference (know what each phase does before suggesting it)\n- **code-implementation**: Writes/updates production code (creates or modifies code)\n- **pr-execution**: Syncs already-written code from dev branch to split PR branches (does NOT write new code)\n- **bug-fixing**: Fixes code bugs and test failures\n- To fix bugs or update code \u2192 suggest **code-implementation** or **bug-fixing**, NOT pr-execution\n\n## Task Creation\nWhen the user asks to notify someone, assign a review, or request action from a team member:\n1. Use list_product_members to find the person by name\n2. Use create_task with executor=\"human\", the resolved user ID, and the correct action_url\n3. Confirm in chat what you created and who it's assigned to\n\nWhen the user describes work for AI to do (e.g., \"implement X\", \"fix Y\"):\n1. Use create_task with executor=\"ai\" \u2014 the task worker will pick it up automatically\n\n### Action URL Patterns\nAlways set action_url to link to the most relevant page. Available patterns:\n- Product page: `/products/{product_id}`\n- Feature details: `/products/{product_id}/features/{feature_id}`\n- Feature tab (append ?tab=): `/products/{product_id}/features/{feature_id}?tab={tab}`\n\nAvailable feature tabs:\n- `stories` \u2014 User Stories (use for: review user stories, update stories)\n- `test-cases` \u2014 Test Cases (use for: review test cases, verify tests)\n- `technical-design` \u2014 Technical Design (use for: review design, architecture review)\n- `checklists` \u2014 Checklists (use for: review checklists, quality checks)\n- `branches` \u2014 Branches (use for: code review, branch management)\n- `pull-requests` \u2014 Pull Requests (use for: review PRs, merge requests)\n- `test-reports` \u2014 Test Reports (use for: review test results)\n- `feedbacks` \u2014 Feedbacks (use for: provide feedback)\n- `chat` \u2014 Chat (use for: discussion)\n- `details` \u2014 Feature Details (default)\n\nChoose the tab that best matches the task content. For example:\n- \"review user stories\" \u2192 `?tab=stories`\n- \"review technical design\" \u2192 `?tab=technical-design`\n- \"check test results\" \u2192 `?tab=test-reports`\n\n## Important Rules\n- Never make destructive changes without confirmation (deleting stories, resetting phases)\n- For ambiguous feedback, present options rather than guessing\n- If you can't do something, explain why clearly\n- When creating tasks for people, always confirm the person's identity if the name is ambiguous\n";
9
9
  export declare const NEXT_STEP_ADVISOR_PROMPT = "You are an AI advisor in Edsger, a software development platform. A workflow phase just completed for a feature you're helping develop.\n\n## Your Job\nAnalyze the completed phase output and the feature's current state, then give a concrete, data-backed suggestion for what to do next.\n\n## Critical Rules\n1. **Reference specific data** \u2014 \"8 user stories generated, 3 involve complex auth logic\" not \"several stories were created\"\n2. **Explain your reasoning** \u2014 \"Because Story #3 involves concurrent editing, I suggest writing test cases first to define edge cases before implementation\"\n3. **Present actionable options** \u2014 Always use the provide_options tool to give 2-4 choices the user can click\n4. **Do NOT follow a fixed phase order** \u2014 Adapt based on:\n - Feature size and complexity\n - What was just produced (quality, coverage, gaps)\n - Previous human feedback in the chat\n - Whether certain phases can be skipped for simple features\n5. **Flag issues proactively** \u2014 If the phase output has gaps, incomplete coverage, or potential problems, call them out\n\n## Phase Reference (know what each phase does before suggesting it)\n- **code-implementation**: Writes/updates production code (the phase that creates or modifies code)\n- **pr-splitting**: Plans how to split code changes into reviewable PRs\n- **pr-execution**: Syncs already-written code from the dev branch to split PR branches (does NOT write new code)\n- **code-testing**: Writes automated tests for implemented code\n- **functional-testing**: Runs end-to-end tests with Playwright\n- **bug-fixing**: Fixes code bugs and test failures\n- **code-review**: Reviews PR code for issues\n- **code-refine**: Updates code based on PR review feedback\n\n**Important distinctions:**\n- To fix bugs or update code \u2192 use **code-implementation** or **bug-fixing**, NOT pr-execution\n- pr-execution only moves existing code to PR branches \u2014 it never creates or modifies implementation code\n\n## Context You Receive\n- Feature description, size, and current state\n- The completed phase name and its full output\n- Remaining workflow phases (with descriptions)\n- User story and test case counts\n- Code change scope (if applicable)\n- Recent chat history (human feedback)\n\n## Communication Style\n- Respond in the same language as recent chat messages (default to the feature's language context)\n- Be specific and data-driven\n- Structure: brief summary \u2192 reasoning \u2192 options\n";
10
10
  /**
11
11
  * Phase descriptions so the AI advisor understands what each phase does.
@@ -38,12 +38,35 @@ You can see the feature's current state, user stories, test cases, workflow phas
38
38
  ## Task Creation
39
39
  When the user asks to notify someone, assign a review, or request action from a team member:
40
40
  1. Use list_product_members to find the person by name
41
- 2. Use create_task with executor="human" and the resolved user ID
41
+ 2. Use create_task with executor="human", the resolved user ID, and the correct action_url
42
42
  3. Confirm in chat what you created and who it's assigned to
43
43
 
44
44
  When the user describes work for AI to do (e.g., "implement X", "fix Y"):
45
45
  1. Use create_task with executor="ai" — the task worker will pick it up automatically
46
46
 
47
+ ### Action URL Patterns
48
+ Always set action_url to link to the most relevant page. Available patterns:
49
+ - Product page: \`/products/{product_id}\`
50
+ - Feature details: \`/products/{product_id}/features/{feature_id}\`
51
+ - Feature tab (append ?tab=): \`/products/{product_id}/features/{feature_id}?tab={tab}\`
52
+
53
+ Available feature tabs:
54
+ - \`stories\` — User Stories (use for: review user stories, update stories)
55
+ - \`test-cases\` — Test Cases (use for: review test cases, verify tests)
56
+ - \`technical-design\` — Technical Design (use for: review design, architecture review)
57
+ - \`checklists\` — Checklists (use for: review checklists, quality checks)
58
+ - \`branches\` — Branches (use for: code review, branch management)
59
+ - \`pull-requests\` — Pull Requests (use for: review PRs, merge requests)
60
+ - \`test-reports\` — Test Reports (use for: review test results)
61
+ - \`feedbacks\` — Feedbacks (use for: provide feedback)
62
+ - \`chat\` — Chat (use for: discussion)
63
+ - \`details\` — Feature Details (default)
64
+
65
+ Choose the tab that best matches the task content. For example:
66
+ - "review user stories" → \`?tab=stories\`
67
+ - "review technical design" → \`?tab=technical-design\`
68
+ - "check test results" → \`?tab=test-reports\`
69
+
47
70
  ## Important Rules
48
71
  - Never make destructive changes without confirmation (deleting stories, resetting phases)
49
72
  - For ambiguous feedback, present options rather than guessing
@@ -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.string().describe('New execution mode'),
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: args.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', 'ready', 'approved']).optional(),
105
+ status: z.enum(['draft', 'pending_approval', 'approved']).optional(),
76
106
  })),
77
107
  }, async (args) => {
78
108
  const results = [];
@@ -286,6 +316,7 @@ export function createChatMcpServer() {
286
316
  executor: z.enum(['ai', 'human']).describe('Who should do this: "ai" for automated work, "human" for manual review/approval'),
287
317
  assigned_to: z.string().optional().describe('User ID to assign to (use list_product_members to look up)'),
288
318
  feature_id: z.string().optional().describe('Related feature ID'),
319
+ action_url: z.string().optional().describe('URL where the assignee should take action. See prompt for URL patterns.'),
289
320
  priority: z.number().optional().describe('1=low, 2=medium, 3=high, 4=urgent'),
290
321
  }, async (args) => {
291
322
  // Get next sequence number
@@ -295,6 +326,9 @@ export function createChatMcpServer() {
295
326
  const text = listResult?.content?.[0]?.text || '[]';
296
327
  const existingTasks = JSON.parse(text);
297
328
  const nextSequence = existingTasks.length + 1;
329
+ // Use explicit action_url if provided, otherwise auto-generate from feature_id
330
+ const actionUrl = args.action_url
331
+ || (args.feature_id ? `/products/${args.product_id}/features/${args.feature_id}` : null);
298
332
  const result = await callMcpEndpoint('tasks/create', {
299
333
  product_id: args.product_id,
300
334
  sequence: nextSequence,
@@ -304,6 +338,7 @@ export function createChatMcpServer() {
304
338
  source: 'system',
305
339
  assigned_to: args.assigned_to || null,
306
340
  feature_id: args.feature_id || null,
341
+ action_url: actionUrl,
307
342
  priority: args.priority || (args.executor === 'human' ? 3 : 2),
308
343
  });
309
344
  return {
@@ -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 ready artifacts to draft so AI can manage them on re-run
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 ready and exit
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 ready status...');
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 ready now
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 ready status...');
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 'ready' status (if verification passed)
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 ready status after successful verification
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 === 'ready')
13
+ .filter((s) => s.status === 'pending_approval')
14
14
  .map((s) => s.id);
15
15
  const readyTestCaseIds = testCases
16
- .filter((tc) => tc.status === 'ready')
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 ready status after successful verification
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 ready...`);
152
+ logInfo(`Updating ${userStoryIds.length} user stories from draft to pending_approval...`);
153
153
  }
154
- await batchUpdateUserStoryStatus(userStoryIds, 'ready', verbose);
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 ready...`);
158
+ logInfo(`Updating ${testCaseIds.length} test cases from draft to pending_approval...`);
159
159
  }
160
- await batchUpdateTestCaseStatus(testCaseIds, 'ready', verbose);
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 === 'ready' ? 'ready' : story.status === 'approved' ? 'approved' : 'draft'),
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 ready test cases to draft so AI can manage them on re-run
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} ready test cases to draft for re-analysis`);
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 ready status...');
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 ready
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 ready status...');
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 === 'ready')
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 ready...`);
73
+ logInfo(`Updating ${testCaseIds.length} test cases from draft to pending_approval...`);
74
74
  }
75
- await batchUpdateTestCaseStatus(testCaseIds, 'ready', verbose);
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 ready user stories to draft so AI can manage them on re-run
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} ready user stories to draft for re-analysis`);
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 ready status...');
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 ready
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 ready status...');
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 === 'ready')
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 ready...`);
73
+ logInfo(`Updating ${userStoryIds.length} user stories from draft to pending_approval...`);
74
74
  }
75
- await batchUpdateUserStoryStatus(userStoryIds, 'ready', verbose);
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 === 'ready' ? 'ready' : 'draft'),
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,4 @@
1
+ /**
2
+ * Unit tests for phase quality criteria definitions
3
+ */
4
+ export {};
@@ -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
+ });