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.
- package/.claude/settings.local.json +3 -23
- package/dist/commands/pr-resolve/index.d.ts +1 -0
- package/dist/commands/pr-resolve/index.js +1 -0
- package/dist/commands/workflow/executors/phase-executor.js +1 -3
- package/dist/commands/workflow/phase-orchestrator.js +1 -3
- package/dist/index.js +3 -2
- package/dist/phases/app-store-generation/index.js +1 -3
- package/dist/phases/branch-planning/index.js +1 -3
- package/dist/phases/bug-fixing/analyzer.js +1 -3
- package/dist/phases/code-implementation/index.js +1 -3
- package/dist/phases/code-refine/index.js +1 -3
- package/dist/phases/code-review/index.js +1 -3
- package/dist/phases/code-testing/analyzer.js +1 -3
- package/dist/phases/feature-analysis/index.js +1 -3
- package/dist/phases/functional-testing/analyzer.js +1 -3
- package/dist/phases/growth-analysis/index.js +1 -3
- package/dist/phases/pr-execution/index.js +59 -1
- package/dist/phases/pr-resolve/__tests__/checklist-learner.test.d.ts +1 -0
- package/dist/phases/pr-resolve/__tests__/checklist-learner.test.js +157 -0
- package/dist/phases/pr-resolve/__tests__/types.test.d.ts +1 -0
- package/dist/phases/pr-resolve/__tests__/types.test.js +43 -0
- package/dist/phases/pr-resolve/checklist-learner.d.ts +28 -0
- package/dist/phases/pr-resolve/checklist-learner.js +128 -0
- package/dist/phases/pr-resolve/index.d.ts +4 -0
- package/dist/phases/pr-resolve/index.js +23 -5
- package/dist/phases/pr-resolve/prompts.js +2 -1
- package/dist/phases/pr-resolve/types.d.ts +18 -0
- package/dist/phases/pr-resolve/types.js +14 -0
- package/dist/phases/pr-resolve/workspace.d.ts +1 -1
- package/dist/phases/pr-resolve/workspace.js +1 -1
- package/dist/phases/pr-review/__tests__/review-comments.test.js +4 -2
- package/dist/phases/pr-review/index.js +1 -3
- package/dist/phases/pr-shared/agent-utils.js +0 -1
- package/dist/phases/pr-shared/context.d.ts +1 -1
- package/dist/phases/pr-splitting/context.js +20 -15
- package/dist/phases/pr-splitting/index.js +1 -3
- package/dist/phases/technical-design/index.js +1 -3
- package/dist/phases/test-cases-analysis/index.js +1 -3
- package/dist/phases/user-stories-analysis/index.js +1 -3
- 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/package.json +1 -1
- 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
|
@@ -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
|
-
|
|
88
|
-
if (
|
|
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
|
-
|
|
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
|
|
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,
|
|
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,
|
|
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,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
|
+
});
|