edsger 0.41.2 → 0.42.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (139) hide show
  1. package/.claude/settings.local.json +3 -23
  2. package/dist/commands/pr-resolve/index.d.ts +1 -0
  3. package/dist/commands/pr-resolve/index.js +1 -0
  4. package/dist/commands/workflow/executors/phase-executor.js +1 -3
  5. package/dist/commands/workflow/phase-orchestrator.js +1 -3
  6. package/dist/index.js +3 -2
  7. package/dist/phases/app-store-generation/index.js +1 -3
  8. package/dist/phases/branch-planning/index.js +1 -3
  9. package/dist/phases/bug-fixing/analyzer.js +1 -3
  10. package/dist/phases/code-implementation/index.js +1 -3
  11. package/dist/phases/code-refine/index.js +1 -3
  12. package/dist/phases/code-review/index.js +1 -3
  13. package/dist/phases/code-testing/analyzer.js +1 -3
  14. package/dist/phases/feature-analysis/index.js +1 -3
  15. package/dist/phases/functional-testing/analyzer.js +1 -3
  16. package/dist/phases/growth-analysis/index.js +1 -3
  17. package/dist/phases/pr-execution/index.js +59 -1
  18. package/dist/phases/pr-resolve/__tests__/checklist-learner.test.d.ts +1 -0
  19. package/dist/phases/pr-resolve/__tests__/checklist-learner.test.js +157 -0
  20. package/dist/phases/pr-resolve/__tests__/types.test.d.ts +1 -0
  21. package/dist/phases/pr-resolve/__tests__/types.test.js +43 -0
  22. package/dist/phases/pr-resolve/checklist-learner.d.ts +28 -0
  23. package/dist/phases/pr-resolve/checklist-learner.js +128 -0
  24. package/dist/phases/pr-resolve/index.d.ts +4 -0
  25. package/dist/phases/pr-resolve/index.js +23 -5
  26. package/dist/phases/pr-resolve/prompts.js +2 -1
  27. package/dist/phases/pr-resolve/types.d.ts +18 -0
  28. package/dist/phases/pr-resolve/types.js +14 -0
  29. package/dist/phases/pr-resolve/workspace.d.ts +1 -1
  30. package/dist/phases/pr-resolve/workspace.js +1 -1
  31. package/dist/phases/pr-review/__tests__/review-comments.test.js +4 -2
  32. package/dist/phases/pr-review/index.js +1 -3
  33. package/dist/phases/pr-shared/agent-utils.js +0 -1
  34. package/dist/phases/pr-shared/context.d.ts +1 -1
  35. package/dist/phases/pr-splitting/context.js +20 -15
  36. package/dist/phases/pr-splitting/index.js +1 -3
  37. package/dist/phases/technical-design/index.js +1 -3
  38. package/dist/phases/test-cases-analysis/index.js +1 -3
  39. package/dist/phases/user-stories-analysis/index.js +1 -3
  40. package/dist/services/lifecycle-agent/__tests__/phase-criteria.test.d.ts +4 -0
  41. package/dist/services/lifecycle-agent/__tests__/phase-criteria.test.js +133 -0
  42. package/dist/services/lifecycle-agent/__tests__/transition-rules.test.d.ts +4 -0
  43. package/dist/services/lifecycle-agent/__tests__/transition-rules.test.js +336 -0
  44. package/dist/services/lifecycle-agent/index.d.ts +24 -0
  45. package/dist/services/lifecycle-agent/index.js +25 -0
  46. package/dist/services/lifecycle-agent/phase-criteria.d.ts +57 -0
  47. package/dist/services/lifecycle-agent/phase-criteria.js +335 -0
  48. package/dist/services/lifecycle-agent/transition-rules.d.ts +60 -0
  49. package/dist/services/lifecycle-agent/transition-rules.js +184 -0
  50. package/dist/services/lifecycle-agent/types.d.ts +190 -0
  51. package/dist/services/lifecycle-agent/types.js +12 -0
  52. package/package.json +1 -1
  53. package/.env.local +0 -12
  54. package/dist/api/features/__tests__/regression-prevention.test.d.ts +0 -5
  55. package/dist/api/features/__tests__/regression-prevention.test.js +0 -338
  56. package/dist/api/features/__tests__/status-updater.integration.test.d.ts +0 -5
  57. package/dist/api/features/__tests__/status-updater.integration.test.js +0 -497
  58. package/dist/commands/workflow/pipeline-runner.d.ts +0 -17
  59. package/dist/commands/workflow/pipeline-runner.js +0 -393
  60. package/dist/commands/workflow/runner.d.ts +0 -26
  61. package/dist/commands/workflow/runner.js +0 -119
  62. package/dist/commands/workflow/workflow-runner.d.ts +0 -26
  63. package/dist/commands/workflow/workflow-runner.js +0 -119
  64. package/dist/phases/code-implementation/analyzer-helpers.d.ts +0 -28
  65. package/dist/phases/code-implementation/analyzer-helpers.js +0 -177
  66. package/dist/phases/code-implementation/analyzer.d.ts +0 -32
  67. package/dist/phases/code-implementation/analyzer.js +0 -629
  68. package/dist/phases/code-implementation/context-fetcher.d.ts +0 -17
  69. package/dist/phases/code-implementation/context-fetcher.js +0 -86
  70. package/dist/phases/code-implementation/mcp-server.d.ts +0 -1
  71. package/dist/phases/code-implementation/mcp-server.js +0 -93
  72. package/dist/phases/code-implementation/prompts-improvement.d.ts +0 -5
  73. package/dist/phases/code-implementation/prompts-improvement.js +0 -108
  74. package/dist/phases/code-implementation-verification/verifier.d.ts +0 -31
  75. package/dist/phases/code-implementation-verification/verifier.js +0 -196
  76. package/dist/phases/code-refine/analyzer.d.ts +0 -41
  77. package/dist/phases/code-refine/analyzer.js +0 -561
  78. package/dist/phases/code-refine/context-fetcher.d.ts +0 -94
  79. package/dist/phases/code-refine/context-fetcher.js +0 -423
  80. package/dist/phases/code-refine-verification/analysis/llm-analyzer.d.ts +0 -22
  81. package/dist/phases/code-refine-verification/analysis/llm-analyzer.js +0 -134
  82. package/dist/phases/code-refine-verification/verifier.d.ts +0 -47
  83. package/dist/phases/code-refine-verification/verifier.js +0 -597
  84. package/dist/phases/code-review/analyzer.d.ts +0 -29
  85. package/dist/phases/code-review/analyzer.js +0 -363
  86. package/dist/phases/code-review/context-fetcher.d.ts +0 -92
  87. package/dist/phases/code-review/context-fetcher.js +0 -296
  88. package/dist/phases/feature-analysis/analyzer-helpers.d.ts +0 -10
  89. package/dist/phases/feature-analysis/analyzer-helpers.js +0 -47
  90. package/dist/phases/feature-analysis/analyzer.d.ts +0 -11
  91. package/dist/phases/feature-analysis/analyzer.js +0 -208
  92. package/dist/phases/feature-analysis/context-fetcher.d.ts +0 -26
  93. package/dist/phases/feature-analysis/context-fetcher.js +0 -134
  94. package/dist/phases/feature-analysis/http-fallback.d.ts +0 -20
  95. package/dist/phases/feature-analysis/http-fallback.js +0 -95
  96. package/dist/phases/feature-analysis/mcp-server.d.ts +0 -1
  97. package/dist/phases/feature-analysis/mcp-server.js +0 -144
  98. package/dist/phases/feature-analysis/prompts-improvement.d.ts +0 -8
  99. package/dist/phases/feature-analysis/prompts-improvement.js +0 -109
  100. package/dist/phases/feature-analysis-verification/verifier.d.ts +0 -37
  101. package/dist/phases/feature-analysis-verification/verifier.js +0 -147
  102. package/dist/phases/technical-design/analyzer-helpers.d.ts +0 -25
  103. package/dist/phases/technical-design/analyzer-helpers.js +0 -39
  104. package/dist/phases/technical-design/analyzer.d.ts +0 -21
  105. package/dist/phases/technical-design/analyzer.js +0 -461
  106. package/dist/phases/technical-design/context-fetcher.d.ts +0 -12
  107. package/dist/phases/technical-design/context-fetcher.js +0 -39
  108. package/dist/phases/technical-design/http-fallback.d.ts +0 -17
  109. package/dist/phases/technical-design/http-fallback.js +0 -151
  110. package/dist/phases/technical-design/mcp-server.d.ts +0 -1
  111. package/dist/phases/technical-design/mcp-server.js +0 -157
  112. package/dist/phases/technical-design/prompts-improvement.d.ts +0 -5
  113. package/dist/phases/technical-design/prompts-improvement.js +0 -93
  114. package/dist/phases/technical-design-verification/verifier.d.ts +0 -53
  115. package/dist/phases/technical-design-verification/verifier.js +0 -170
  116. package/dist/services/feature-branches.d.ts +0 -77
  117. package/dist/services/feature-branches.js +0 -205
  118. package/dist/workflow-runner/config/phase-configs.d.ts +0 -5
  119. package/dist/workflow-runner/config/phase-configs.js +0 -120
  120. package/dist/workflow-runner/core/feature-filter.d.ts +0 -16
  121. package/dist/workflow-runner/core/feature-filter.js +0 -46
  122. package/dist/workflow-runner/core/index.d.ts +0 -8
  123. package/dist/workflow-runner/core/index.js +0 -12
  124. package/dist/workflow-runner/core/pipeline-evaluator.d.ts +0 -24
  125. package/dist/workflow-runner/core/pipeline-evaluator.js +0 -32
  126. package/dist/workflow-runner/core/state-manager.d.ts +0 -24
  127. package/dist/workflow-runner/core/state-manager.js +0 -42
  128. package/dist/workflow-runner/core/workflow-logger.d.ts +0 -20
  129. package/dist/workflow-runner/core/workflow-logger.js +0 -65
  130. package/dist/workflow-runner/executors/phase-executor.d.ts +0 -8
  131. package/dist/workflow-runner/executors/phase-executor.js +0 -248
  132. package/dist/workflow-runner/feature-workflow-runner.d.ts +0 -26
  133. package/dist/workflow-runner/feature-workflow-runner.js +0 -119
  134. package/dist/workflow-runner/index.d.ts +0 -2
  135. package/dist/workflow-runner/index.js +0 -2
  136. package/dist/workflow-runner/pipeline-runner.d.ts +0 -17
  137. package/dist/workflow-runner/pipeline-runner.js +0 -393
  138. package/dist/workflow-runner/workflow-processor.d.ts +0 -54
  139. package/dist/workflow-runner/workflow-processor.js +0 -170
@@ -0,0 +1,128 @@
1
+ /**
2
+ * Checklist Learner — runs after PR resolve to analyse addressed review
3
+ * comments and create / update code-review checklists so the same issues
4
+ * don't recur.
5
+ *
6
+ * Uses the Claude Agent SDK with the existing checklist MCP tools.
7
+ * Strictly non-blocking: failures only log a warning.
8
+ */
9
+ import { query } from '@anthropic-ai/claude-agent-sdk';
10
+ import { createChecklistsMcpServer } from '../../commands/checklists/tools.js';
11
+ import { logInfo, logWarning } from '../../utils/logger.js';
12
+ const LEARNER_SYSTEM_PROMPT = `You are a software quality engineer. Your task is to analyse review comments that were addressed during a PR resolve and distil them into actionable checklist items for the code_review phase.
13
+
14
+ ## Workflow
15
+
16
+ 1. **Read** the addressed review comments provided below.
17
+ 2. **Query existing checklists** using \`list_checklists\` with phase "code_review" to see what already exists.
18
+ 3. **Identify patterns** — group related comments into categories (e.g., error handling, naming, security, performance, testing).
19
+ 4. **Update or create checklist items**:
20
+ - If an existing checklist covers the category, add new items to it (skip if a similar item already exists).
21
+ - If no suitable checklist exists, create a new one.
22
+ 5. **Summarise** what you added or updated.
23
+
24
+ ## Rules
25
+
26
+ - Only create items for **genuine quality patterns** — things that should be checked in every code review.
27
+ - Skip one-off nits, purely stylistic preferences, or context-specific fixes that won't generalise.
28
+ - Role: \`developer\`
29
+ - Phases: \`["code_review"]\`
30
+ - Item type: \`boolean\` (yes/no checkable)
31
+ - Keep item titles concise (< 80 chars). Use the description for details.
32
+ - Do NOT duplicate items that already exist in the checklists.
33
+ - If all comments are too specific to generalise, it's fine to add nothing — just say so.
34
+ `;
35
+ /**
36
+ * Build a user prompt from the addressed review comments.
37
+ * @param addressedComments - pre-filtered list of comments with action === 'changed'
38
+ */
39
+ export function buildLearnerPrompt(addressedComments, unresolvedThreads, commentIdToThreadId, summary) {
40
+ // Build a reverse lookup: threadId → thread for O(1) access
41
+ const threadById = new Map();
42
+ for (const thread of unresolvedThreads) {
43
+ threadById.set(thread.id, thread);
44
+ }
45
+ const sections = [
46
+ '# Addressed PR Review Comments',
47
+ '',
48
+ `${addressedComments.length} review comment(s) were accepted and fixed during PR resolution.`,
49
+ `Analyse these to identify patterns that should become checklist items for future code reviews.`,
50
+ '',
51
+ ];
52
+ for (const comment of addressedComments) {
53
+ const threadId = commentIdToThreadId.get(comment.comment_id);
54
+ const thread = threadId ? threadById.get(threadId) : undefined;
55
+ const firstNode = thread?.comments.nodes[0];
56
+ sections.push(`## ${comment.comment_id}`);
57
+ if (firstNode) {
58
+ sections.push(`**File**: ${firstNode.path}`);
59
+ if (firstNode.line) {
60
+ sections.push(`**Line**: ${firstNode.line}`);
61
+ }
62
+ sections.push(`**Reviewer**: @${firstNode.author.login}`);
63
+ sections.push(`**Review comment**:`);
64
+ sections.push(firstNode.body);
65
+ }
66
+ sections.push(`**Resolution**: ${comment.reply}`);
67
+ sections.push('');
68
+ }
69
+ if (summary) {
70
+ sections.push('## Overall Summary');
71
+ sections.push(summary);
72
+ sections.push('');
73
+ }
74
+ sections.push('## Instructions');
75
+ sections.push('Based on the patterns above, query existing code_review checklists and create or update items to prevent these issues from recurring.');
76
+ return sections.join('\n');
77
+ }
78
+ /**
79
+ * Analyse addressed review comments and update code-review checklists.
80
+ * Non-blocking — catches all errors and logs a warning.
81
+ */
82
+ export async function learnFromReviewFeedback(input) {
83
+ try {
84
+ const { resolveResult, unresolvedThreads, commentIdToThreadId, verbose, productId, } = input;
85
+ // Filter once — only learn from accepted changes
86
+ const addressedComments = resolveResult.comments.filter((c) => c.action === 'changed');
87
+ if (addressedComments.length === 0) {
88
+ if (verbose) {
89
+ logInfo('No addressed comments to learn from, skipping checklist sync.');
90
+ }
91
+ return;
92
+ }
93
+ logInfo(`Learning from ${addressedComments.length} addressed review comment(s) to update checklists...`);
94
+ const userPrompt = buildLearnerPrompt(addressedComments, unresolvedThreads, commentIdToThreadId, resolveResult.summary);
95
+ const mcpServer = createChecklistsMcpServer(productId);
96
+ for await (const message of query({
97
+ prompt: userPrompt,
98
+ options: {
99
+ systemPrompt: {
100
+ type: 'preset',
101
+ preset: 'claude_code',
102
+ append: LEARNER_SYSTEM_PROMPT,
103
+ },
104
+ model: 'sonnet',
105
+ maxTurns: 15,
106
+ permissionMode: 'bypassPermissions',
107
+ mcpServers: {
108
+ 'edsger-checklists': mcpServer,
109
+ },
110
+ },
111
+ })) {
112
+ if (message.type === 'result') {
113
+ if (message.subtype === 'success') {
114
+ logInfo('Checklist learning completed.');
115
+ if (verbose && message.result) {
116
+ logInfo(message.result);
117
+ }
118
+ }
119
+ else if (verbose) {
120
+ logWarning(`Checklist learning incomplete: ${message.subtype}`);
121
+ }
122
+ }
123
+ }
124
+ }
125
+ catch (error) {
126
+ logWarning(`Checklist learning failed (non-blocking): ${error instanceof Error ? error.message : String(error)}`);
127
+ }
128
+ }
@@ -11,6 +11,8 @@ export interface StandalonePRResolveOptions {
11
11
  repo: string;
12
12
  prId?: string;
13
13
  verbose?: boolean;
14
+ /** Set to false to skip checklist learning after resolve (default: true) */
15
+ learn?: boolean;
14
16
  }
15
17
  export interface PRResolveResult {
16
18
  status: 'success' | 'error';
@@ -21,6 +23,8 @@ export interface PRResolveResult {
21
23
  filesModified?: string[];
22
24
  summary?: string;
23
25
  }
26
+ export type { ResolveComment, ResolveResult } from './types.js';
27
+ export { isResolveResult } from './types.js';
24
28
  /**
25
29
  * Resolve PR change requests: evaluate each comment, fix or explain.
26
30
  */
@@ -13,9 +13,12 @@ import { logError, logInfo, logSuccess } from '../../utils/logger.js';
13
13
  import { fetchUnresolvedReviewThreads } from '../code-refine-verification/github.js';
14
14
  import { createPromptGenerator, extractTextFromContent, tryExtractResult, } from '../pr-shared/agent-utils.js';
15
15
  import { parsePullRequestUrl } from '../pr-shared/context.js';
16
+ import { learnFromReviewFeedback } from './checklist-learner.js';
16
17
  import { replyToReviewThread, resolveReviewThread } from './github-reply.js';
17
18
  import { createResolveSystemPrompt, createResolveUserPrompt, } from './prompts.js';
19
+ import { isResolveResult } from './types.js';
18
20
  import { hasNewCommits, hasUncommittedChanges, prepareWorkspace, pushChanges, } from './workspace.js';
21
+ export { isResolveResult } from './types.js';
19
22
  /**
20
23
  * Resolve PR change requests: evaluate each comment, fix or explain.
21
24
  */
@@ -57,7 +60,6 @@ export async function resolveStandalonePR(options) {
57
60
  const systemPrompt = createResolveSystemPrompt();
58
61
  const { prompt: resolvePrompt, commentIdToThreadId } = createResolveUserPrompt(unresolvedThreads);
59
62
  let lastAssistantResponse = '';
60
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
61
63
  let resolveResult = null;
62
64
  logInfo('Starting Claude agent to evaluate and resolve comments...');
63
65
  for await (const message of query({
@@ -84,8 +86,11 @@ export async function resolveStandalonePR(options) {
84
86
  if (message.subtype === 'success') {
85
87
  logInfo('Agent completed, parsing results...');
86
88
  const responseText = message.result || lastAssistantResponse;
87
- resolveResult = tryExtractResult(responseText, 'resolve_result');
88
- if (!resolveResult) {
89
+ const parsed = tryExtractResult(responseText, 'resolve_result');
90
+ if (isResolveResult(parsed)) {
91
+ resolveResult = parsed;
92
+ }
93
+ else {
89
94
  logError('Failed to parse resolve result JSON');
90
95
  }
91
96
  }
@@ -93,7 +98,10 @@ export async function resolveStandalonePR(options) {
93
98
  logError(`Agent incomplete: ${message.subtype}`);
94
99
  // Try to salvage partial results from last response
95
100
  if (lastAssistantResponse) {
96
- resolveResult = tryExtractResult(lastAssistantResponse, 'resolve_result');
101
+ const salvaged = tryExtractResult(lastAssistantResponse, 'resolve_result');
102
+ if (isResolveResult(salvaged)) {
103
+ resolveResult = salvaged;
104
+ }
97
105
  }
98
106
  }
99
107
  }
@@ -113,7 +121,7 @@ export async function resolveStandalonePR(options) {
113
121
  let threadsSkipped = 0;
114
122
  let threadsErrored = 0;
115
123
  if (resolveResult?.comments) {
116
- const comments = resolveResult.comments;
124
+ const { comments } = resolveResult;
117
125
  for (const comment of comments) {
118
126
  // Map comment_id back to real GraphQL thread ID
119
127
  const threadId = commentIdToThreadId.get(comment.comment_id);
@@ -166,6 +174,16 @@ export async function resolveStandalonePR(options) {
166
174
  }
167
175
  }
168
176
  logSuccess(`PR resolve completed: ${threadsAddressed} addressed, ${threadsSkipped} skipped, ${threadsErrored} errors`);
177
+ // Learn from addressed comments to update code-review checklists
178
+ if (options.learn !== false && threadsAddressed > 0 && resolveResult) {
179
+ await learnFromReviewFeedback({
180
+ productId: options.productId,
181
+ unresolvedThreads,
182
+ resolveResult,
183
+ commentIdToThreadId,
184
+ verbose,
185
+ });
186
+ }
169
187
  if (prId) {
170
188
  try {
171
189
  await callMcpEndpoint('pull_requests/update', {
@@ -73,8 +73,9 @@ export function createResolveUserPrompt(unresolvedThreads) {
73
73
  let commentIndex = 0;
74
74
  for (const thread of unresolvedThreads) {
75
75
  const firstComment = thread.comments.nodes[0];
76
- if (!firstComment)
76
+ if (!firstComment) {
77
77
  continue;
78
+ }
78
79
  commentIndex++;
79
80
  const commentId = `comment_${commentIndex}`;
80
81
  commentIdToThreadId.set(commentId, thread.id);
@@ -0,0 +1,18 @@
1
+ /**
2
+ * Shared type definitions for PR resolve phase.
3
+ */
4
+ export interface ResolveComment {
5
+ comment_id: string;
6
+ action: 'changed' | 'skipped';
7
+ reply: string;
8
+ }
9
+ export interface ResolveResult {
10
+ comments: ResolveComment[];
11
+ files_modified?: string[];
12
+ summary?: string;
13
+ }
14
+ /**
15
+ * Runtime type guard — validates that an unknown value from tryExtractResult
16
+ * has the shape of a ResolveResult.
17
+ */
18
+ export declare function isResolveResult(value: unknown): value is ResolveResult;
@@ -0,0 +1,14 @@
1
+ /**
2
+ * Shared type definitions for PR resolve phase.
3
+ */
4
+ /**
5
+ * Runtime type guard — validates that an unknown value from tryExtractResult
6
+ * has the shape of a ResolveResult.
7
+ */
8
+ export function isResolveResult(value) {
9
+ if (!value || typeof value !== 'object') {
10
+ return false;
11
+ }
12
+ const obj = value;
13
+ return Array.isArray(obj.comments);
14
+ }
@@ -18,7 +18,7 @@ export declare function prepareWorkspace(owner: string, repo: string, headRef: s
18
18
  /**
19
19
  * Push changes from workspace back to remote.
20
20
  */
21
- export declare function pushChanges(repoPath: string, headRef: string, token: string, verbose?: boolean): boolean;
21
+ export declare function pushChanges(repoPath: string, headRef: string, token: string, _verbose?: boolean): boolean;
22
22
  /**
23
23
  * Check if there are uncommitted changes in the workspace.
24
24
  */
@@ -98,7 +98,7 @@ export function prepareWorkspace(owner, repo, headRef, prNumber, token, verbose)
98
98
  /**
99
99
  * Push changes from workspace back to remote.
100
100
  */
101
- export function pushChanges(repoPath, headRef, token, verbose) {
101
+ export function pushChanges(repoPath, headRef, token, _verbose) {
102
102
  const gitCredentialArgs = buildCredentialArgs(token);
103
103
  try {
104
104
  execFileSync('git', [...gitCredentialArgs, 'push', 'origin', headRef], {
@@ -17,11 +17,13 @@ function mapCommentsToReviewPayload(agentComments, files) {
17
17
  const result = [];
18
18
  for (const comment of agentComments) {
19
19
  const lineToPosition = fileLineToPosition.get(comment.file);
20
- if (!lineToPosition)
20
+ if (!lineToPosition) {
21
21
  continue;
22
+ }
22
23
  const positionResult = findClosestPosition(comment.line, lineToPosition);
23
- if (!positionResult)
24
+ if (!positionResult) {
24
25
  continue;
26
+ }
25
27
  let body = comment.comment;
26
28
  if (positionResult.actualLine !== comment.line) {
27
29
  body = `**Note**: Comment originally for line ${comment.line}, adjusted to line ${positionResult.actualLine} (nearest line in diff).\n\n${body}`;
@@ -14,9 +14,7 @@ import { createStandaloneReviewSystemPrompt, createStandaloneReviewUserPrompt, }
14
14
  /**
15
15
  * Review a standalone PR and post comments to GitHub.
16
16
  */
17
- export async function reviewStandalonePR(options
18
- // eslint-disable-next-line complexity
19
- ) {
17
+ export async function reviewStandalonePR(options) {
20
18
  const { pullRequestUrl, githubToken, verbose, prId } = options;
21
19
  logInfo(`Starting standalone PR review: ${pullRequestUrl}`);
22
20
  try {
@@ -24,7 +24,6 @@ export async function* createPromptGenerator(prompt) {
24
24
  /**
25
25
  * Extract text content from assistant message content array.
26
26
  */
27
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
28
27
  export function extractTextFromContent(
29
28
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
30
29
  content, verbose) {
@@ -3,8 +3,8 @@
3
3
  * Reuses GitHub API utilities from code-review/context.ts without feature dependencies.
4
4
  */
5
5
  import { type PRCommit, type PRData, type PRFile } from '../code-review/context.js';
6
- export { parsePullRequestUrl } from '../code-review/context.js';
7
6
  export type { PRCommit, PRData, PRFile } from '../code-review/context.js';
7
+ export { parsePullRequestUrl } from '../code-review/context.js';
8
8
  export interface StandalonePRContext {
9
9
  pullRequestUrl: string;
10
10
  pullRequestNumber: number;
@@ -53,18 +53,18 @@ function getChangedFiles(baseRef, headRef) {
53
53
  /**
54
54
  * Determine the diff base ref for incremental re-runs
55
55
  * If existing PRs have last_synced_commit, use the earliest one
56
- * Otherwise use main
56
+ * Otherwise use origin/main (remote-tracking ref, always up-to-date after fetch)
57
57
  */
58
58
  function determineDiffBaseRef(existingPRs, replaceExisting) {
59
59
  if (replaceExisting || existingPRs.length === 0) {
60
- return 'main';
60
+ return 'origin/main';
61
61
  }
62
62
  // Find the minimum last_synced_commit (earliest sync point)
63
63
  const syncedCommits = existingPRs
64
64
  .map((pr) => pr.last_synced_commit)
65
65
  .filter((c) => c !== null);
66
66
  if (syncedCommits.length === 0) {
67
- return 'main';
67
+ return 'origin/main';
68
68
  }
69
69
  // All PRs should have been synced to the same commit
70
70
  // Use the first one (they should all be equal after a successful sync)
@@ -87,6 +87,22 @@ export async function fetchPRSplittingContext(featureId, verbose, replaceExistin
87
87
  getPullRequests({ featureId, verbose }).catch(() => []),
88
88
  getGitHubConfig(featureId, verbose),
89
89
  ]);
90
+ // Fetch latest remote refs (updates origin/main and all remote-tracking branches)
91
+ try {
92
+ const credArgs = buildCredentialArgs(githubConfig.token);
93
+ execFileSync('git', [...credArgs, 'fetch', 'origin'], {
94
+ encoding: 'utf-8',
95
+ stdio: 'pipe',
96
+ });
97
+ if (verbose) {
98
+ logInfo('✅ Fetched latest remote refs');
99
+ }
100
+ }
101
+ catch (error) {
102
+ if (verbose) {
103
+ logInfo(`⚠️ Could not fetch from origin: ${error instanceof Error ? error.message : String(error)}`);
104
+ }
105
+ }
90
106
  // Verify dev branch exists
91
107
  const localExists = branchExists(devBranchName);
92
108
  const remoteExists = !localExists && remoteBranchExists(devBranchName, githubConfig.token);
@@ -94,17 +110,6 @@ export async function fetchPRSplittingContext(featureId, verbose, replaceExistin
94
110
  throw new Error(`Development branch '${devBranchName}' does not exist. ` +
95
111
  `The feature must have code on the dev branch before PR splitting.`);
96
112
  }
97
- // If branch only exists on remote, fetch it (using credential helper)
98
- if (!localExists && remoteExists) {
99
- if (verbose) {
100
- logInfo(`Fetching remote branch ${devBranchName}...`);
101
- }
102
- const credArgs = buildCredentialArgs(githubConfig.token);
103
- execFileSync('git', [...credArgs, 'fetch', 'origin', devBranchName], {
104
- encoding: 'utf-8',
105
- stdio: 'pipe',
106
- });
107
- }
108
113
  const product = await getProduct(feature.product_id, verbose);
109
114
  // Detect fork status
110
115
  let forkInfo = { isFork: false };
@@ -131,7 +136,7 @@ export async function fetchPRSplittingContext(featureId, verbose, replaceExistin
131
136
  const baseRef = determineDiffBaseRef(existingPullRequests, replaceExisting);
132
137
  const devBranchHeadSha = getBranchHeadSha(devRef);
133
138
  // Check if there are new changes since last sync
134
- if (baseRef !== 'main' && baseRef === devBranchHeadSha) {
139
+ if (baseRef !== 'origin/main' && baseRef === devBranchHeadSha) {
135
140
  if (verbose) {
136
141
  logInfo(`No new changes since last sync (HEAD: ${devBranchHeadSha})`);
137
142
  }
@@ -27,9 +27,7 @@ async function* prompt(analysisPrompt) {
27
27
  * then uses AI to produce a PR split plan saved to the database.
28
28
  * Human review is expected before running the pr-execution phase.
29
29
  */
30
- export const splitFeatureIntoPRs = async (options, config
31
- // eslint-disable-next-line complexity -- orchestration function with context fetching, agent execution, and result processing
32
- ) => {
30
+ export const splitFeatureIntoPRs = async (options, config) => {
33
31
  const { featureId, verbose, replaceExisting } = options;
34
32
  if (verbose) {
35
33
  logInfo(`Starting PR splitting for feature ID: ${featureId}`);
@@ -24,9 +24,7 @@ async function* prompt(analysisPrompt) {
24
24
  setTimeout(res, 10000);
25
25
  });
26
26
  }
27
- export const generateTechnicalDesign = async (options, config, checklistContext
28
- // eslint-disable-next-line complexity -- orchestration function with context assembly, design generation, and verification
29
- ) => {
27
+ export const generateTechnicalDesign = async (options, config, checklistContext) => {
30
28
  const { featureId, verbose } = options;
31
29
  if (verbose) {
32
30
  logInfo(`Starting technical design generation for feature ID: ${featureId}`);
@@ -6,9 +6,7 @@ import { executeTestCasesAnalysisQuery, parseAnalysisResult } from './agent.js';
6
6
  import { prepareTestCasesAnalysisContext } from './context.js';
7
7
  import { buildTestCasesAnalysisResult, deleteSpecificTestCases, deleteTestCaseArtifacts, getAllDraftTestCaseIds, resetReadyTestCasesToDraft, saveTestCasesAsDraft, updateTestCasesToReady, } from './outcome.js';
8
8
  import { createTestCasesAnalysisSystemPrompt } from './prompts.js';
9
- export const analyseTestCases = async (options, config, checklistContext
10
- // eslint-disable-next-line complexity -- orchestration function with context assembly, agent execution, and result processing
11
- ) => {
9
+ export const analyseTestCases = async (options, config, checklistContext) => {
12
10
  const { featureId, verbose } = options;
13
11
  if (verbose) {
14
12
  logInfo(`Starting test cases analysis for feature ID: ${featureId}`);
@@ -6,9 +6,7 @@ import { executeUserStoriesAnalysisQuery, parseAnalysisResult, } from './agent.j
6
6
  import { prepareUserStoriesAnalysisContext } from './context.js';
7
7
  import { buildUserStoriesAnalysisResult, deleteSpecificUserStories, deleteUserStoryArtifacts, getAllDraftUserStoryIds, resetReadyUserStoriesToDraft, saveUserStoriesAsDraft, updateUserStoriesToReady, } from './outcome.js';
8
8
  import { createUserStoriesAnalysisSystemPrompt } from './prompts.js';
9
- export const analyseUserStories = async (options, config, checklistContext
10
- // eslint-disable-next-line complexity -- orchestration function with context assembly, agent execution, and result processing
11
- ) => {
9
+ export const analyseUserStories = async (options, config, checklistContext) => {
12
10
  const { featureId, verbose } = options;
13
11
  if (verbose) {
14
12
  logInfo(`Starting user stories analysis for feature ID: ${featureId}`);
@@ -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
+ });
@@ -0,0 +1,4 @@
1
+ /**
2
+ * Unit tests for lifecycle agent transition rules and decision logic
3
+ */
4
+ export {};