edsger 0.9.0 → 0.9.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.
@@ -0,0 +1,20 @@
1
+ /**
2
+ * Approval workflow integration for feature phases
3
+ * Checks if current feature status has been approved before executing next phase
4
+ */
5
+ interface ApprovalCheckResult {
6
+ canProceed: boolean;
7
+ requiresApproval: boolean;
8
+ approvalId?: string;
9
+ message?: string;
10
+ }
11
+ /**
12
+ * Check if current feature status has been approved before executing next phase
13
+ * This should be called BEFORE executing a phase, not after
14
+ *
15
+ * @param featureId - Feature ID
16
+ * @param verbose - Verbose logging
17
+ * @returns Promise<ApprovalCheckResult> - Whether the phase can proceed
18
+ */
19
+ export declare function checkApprovalBeforePhaseExecution(featureId: string, verbose?: boolean): Promise<ApprovalCheckResult>;
20
+ export {};
@@ -0,0 +1,163 @@
1
+ /**
2
+ * Approval workflow integration for feature phases
3
+ * Checks if current feature status has been approved before executing next phase
4
+ */
5
+ import { logInfo, logError, logWarning } from '../../utils/logger.js';
6
+ import { callMcpEndpoint } from '../mcp-client.js';
7
+ import { getFeature } from './get-feature.js';
8
+ /**
9
+ * Check if current feature status has been approved before executing next phase
10
+ * This should be called BEFORE executing a phase, not after
11
+ *
12
+ * @param featureId - Feature ID
13
+ * @param verbose - Verbose logging
14
+ * @returns Promise<ApprovalCheckResult> - Whether the phase can proceed
15
+ */
16
+ export async function checkApprovalBeforePhaseExecution(featureId, verbose = false) {
17
+ try {
18
+ // 1. Get current feature to check its status and product_id
19
+ const feature = await getFeature(featureId, verbose);
20
+ const currentStatus = feature.status;
21
+ const productId = feature.product_id;
22
+ if (verbose) {
23
+ logInfo(`🔍 Checking approval for current status: ${currentStatus}`);
24
+ }
25
+ // 2. Check if current status requires approval
26
+ const requiresApprovalResult = (await callMcpEndpoint('approvals/requires_approval', {
27
+ product_id: productId,
28
+ feature_status: currentStatus,
29
+ }));
30
+ const requiresApproval = requiresApprovalResult?.requires_approval === true;
31
+ if (!requiresApproval) {
32
+ if (verbose) {
33
+ logInfo(`✅ Current status ${currentStatus} does not require approval. Proceeding.`);
34
+ }
35
+ return {
36
+ canProceed: true,
37
+ requiresApproval: false,
38
+ };
39
+ }
40
+ // 3. Status requires approval - check if already approved
41
+ if (verbose) {
42
+ logInfo(`🔒 Current status ${currentStatus} requires approval`);
43
+ }
44
+ const existingApprovals = (await callMcpEndpoint('approvals/feature_approvals', {
45
+ feature_id: featureId,
46
+ }));
47
+ // Check if there's an approved approval for the current status
48
+ const approvedApproval = existingApprovals?.approvals?.find((approval) => approval.requested_status === currentStatus &&
49
+ approval.approval_status === 'approved');
50
+ if (approvedApproval) {
51
+ if (verbose) {
52
+ logInfo(`✅ Found approved approval for ${currentStatus}. Proceeding.`);
53
+ }
54
+ return {
55
+ canProceed: true,
56
+ requiresApproval: true,
57
+ approvalId: approvedApproval.id,
58
+ };
59
+ }
60
+ // Check if there's already a pending approval for current status
61
+ const pendingApproval = existingApprovals?.approvals?.find((approval) => approval.requested_status === currentStatus &&
62
+ approval.approval_status === 'pending');
63
+ if (pendingApproval) {
64
+ if (verbose) {
65
+ logWarning(`⏳ Approval already pending for ${currentStatus}. Waiting for review.`);
66
+ }
67
+ return {
68
+ canProceed: false,
69
+ requiresApproval: true,
70
+ approvalId: pendingApproval.id,
71
+ message: `Waiting for approval of current status: ${currentStatus}`,
72
+ };
73
+ }
74
+ // No pending or approved approval - need to create one
75
+ if (verbose) {
76
+ logInfo(`🔔 No approval exists for ${currentStatus}. Creating approval request...`);
77
+ }
78
+ // Create approval request
79
+ const approvalId = await createApprovalRequestWithEmail(featureId, productId, currentStatus, verbose);
80
+ if (approvalId) {
81
+ return {
82
+ canProceed: false,
83
+ requiresApproval: true,
84
+ approvalId,
85
+ message: `Approval request created for status: ${currentStatus}`,
86
+ };
87
+ }
88
+ else {
89
+ logError('Failed to create approval request');
90
+ return {
91
+ canProceed: false,
92
+ requiresApproval: true,
93
+ message: `Failed to create approval request for status: ${currentStatus}`,
94
+ };
95
+ }
96
+ }
97
+ catch (error) {
98
+ const errorMessage = error instanceof Error ? error.message : String(error);
99
+ logError(`Error checking approval before phase execution: ${errorMessage}`);
100
+ // On error, block execution to be safe
101
+ return {
102
+ canProceed: false,
103
+ requiresApproval: true,
104
+ message: `Error checking approval: ${errorMessage}`,
105
+ };
106
+ }
107
+ }
108
+ /**
109
+ * Create an approval request and send notification emails
110
+ * This is for the CURRENT status, not the next status
111
+ */
112
+ async function createApprovalRequestWithEmail(featureId, productId, currentStatus, verbose = false) {
113
+ try {
114
+ if (verbose) {
115
+ logInfo(`Creating approval request for current status: ${currentStatus}`);
116
+ }
117
+ // Create approval request via MCP endpoint
118
+ // Note: We're requesting approval for the current status
119
+ // The approval is asking: "Can we proceed from this status?"
120
+ const result = (await callMcpEndpoint('approvals/create', {
121
+ feature_id: featureId,
122
+ requested_status: currentStatus,
123
+ previous_status: null, // We're approving current status, not transitioning
124
+ }));
125
+ const approvalId = result?.approval_id;
126
+ if (!approvalId) {
127
+ logError('Failed to create approval request: No approval_id returned');
128
+ return null;
129
+ }
130
+ if (verbose) {
131
+ logInfo(`✅ Approval request created: ${approvalId}`);
132
+ }
133
+ // Send approval emails via edge function
134
+ try {
135
+ if (verbose) {
136
+ logInfo('Sending approval notification emails...');
137
+ }
138
+ const emailResult = (await callMcpEndpoint('approvals/send_email', {
139
+ approval_id: approvalId,
140
+ }));
141
+ if (emailResult?.emails_sent) {
142
+ const sentCount = emailResult.emails_sent.filter((e) => e.status === 'sent').length;
143
+ if (verbose) {
144
+ logInfo(`✅ Sent approval emails to ${sentCount} assignee(s)`);
145
+ }
146
+ }
147
+ }
148
+ catch (emailError) {
149
+ // Don't fail the whole process if email fails
150
+ const errorMessage = emailError instanceof Error ? emailError.message : String(emailError);
151
+ logWarning(`Failed to send approval emails: ${errorMessage}`);
152
+ if (verbose) {
153
+ logInfo('⚠️ Approval created but email notification failed');
154
+ }
155
+ }
156
+ return approvalId;
157
+ }
158
+ catch (error) {
159
+ const errorMessage = error instanceof Error ? error.message : String(error);
160
+ logError(`Failed to create approval request: ${errorMessage}`);
161
+ return null;
162
+ }
163
+ }
@@ -55,6 +55,7 @@ export async function updateFeatureStatus({ featureId, status, verbose = false,
55
55
  }
56
56
  return true;
57
57
  }
58
+ // Update status
58
59
  await callMcpEndpoint('features/update', {
59
60
  feature_id: featureId,
60
61
  status,
@@ -5,6 +5,7 @@ import { updateFeatureStatusForPhase } from '../../../api/features/index.js';
5
5
  import { phaseConfigs } from '../config/phase-configs.js';
6
6
  import { getChecklistsForPhase, validateChecklistsForPhase, validateRequiredChecklistResults, processChecklistResultsFromResponse, processChecklistItemResultsFromResponse, } from '../../../services/checklist.js';
7
7
  import { logFeaturePhaseEvent } from '../../../services/audit-logs.js';
8
+ import { checkApprovalBeforePhaseExecution } from '../../../api/features/approval-checker.js';
8
9
  // Higher-order function for phase execution
9
10
  export const createPhaseRunner = (phaseConfig) => async (options, config) => {
10
11
  const { featureId, verbose } = options;
@@ -12,6 +13,34 @@ export const createPhaseRunner = (phaseConfig) => async (options, config) => {
12
13
  // Track phase duration for logging
13
14
  const phaseStartTime = Date.now();
14
15
  try {
16
+ // CHECK APPROVAL BEFORE EXECUTING PHASE
17
+ // This checks if the current feature status has been approved
18
+ if (verbose) {
19
+ console.log(`🔍 Checking approval before executing ${name} phase...`);
20
+ }
21
+ const approvalCheck = await checkApprovalBeforePhaseExecution(featureId, verbose);
22
+ if (!approvalCheck.canProceed) {
23
+ // Current status requires approval but hasn't been approved yet
24
+ // Block phase execution and return blocked result
25
+ if (verbose) {
26
+ console.log(`⛔ Phase ${name} blocked: ${approvalCheck.message || 'Waiting for approval'}`);
27
+ }
28
+ return {
29
+ featureId,
30
+ phase: name,
31
+ status: 'blocked',
32
+ message: approvalCheck.message ||
33
+ 'Phase execution blocked - waiting for approval of current feature status',
34
+ data: {
35
+ approval_required: true,
36
+ approval_id: approvalCheck.approvalId,
37
+ },
38
+ };
39
+ }
40
+ // Approval check passed, proceed with phase execution
41
+ if (verbose && approvalCheck.requiresApproval) {
42
+ console.log(`✅ Approval verified for current status. Proceeding with ${name} phase.`);
43
+ }
15
44
  // Update feature status to reflect current phase
16
45
  await updateFeatureStatusForPhase(featureId, name, verbose);
17
46
  if (verbose) {
@@ -88,6 +88,21 @@ export const runCompletePipeline = (options, config) => {
88
88
  };
89
89
  };
90
90
  // Helper functions for different execution patterns
91
+ /**
92
+ * Finalize pipeline execution by updating feature status to ready_for_review if all phases succeeded
93
+ */
94
+ const finalizePipelineExecution = async (options, results, verbose) => {
95
+ // Check if all phases succeeded
96
+ const allSucceeded = results.every((result) => result.status === 'success');
97
+ if (allSucceeded && results.length > 0) {
98
+ // Update status to ready_for_review
99
+ await updateFeatureStatusForPhase(options.featureId, 'ready-for-review', verbose);
100
+ if (verbose) {
101
+ logInfo('✅ Pipeline execution completed - feature is ready for review');
102
+ }
103
+ }
104
+ return logPipelineComplete(results, verbose);
105
+ };
91
106
  /**
92
107
  * Run only feature analysis phase
93
108
  */
@@ -97,7 +112,7 @@ const runOnlyFeatureAnalysis = async (options, config) => {
97
112
  const results = [];
98
113
  const analysisResult = await runFeatureAnalysisPhase(options, config);
99
114
  results.push(logPhaseResult(analysisResult, verbose));
100
- return logPipelineComplete(results, verbose);
115
+ return finalizePipelineExecution(options, results, verbose);
101
116
  };
102
117
  /**
103
118
  * Run only technical design phase
@@ -108,7 +123,7 @@ const runOnlyTechnicalDesign = async (options, config) => {
108
123
  const results = [];
109
124
  const designResult = await runTechnicalDesignPhase(options, config);
110
125
  results.push(logPhaseResult(designResult, verbose));
111
- return logPipelineComplete(results, verbose);
126
+ return finalizePipelineExecution(options, results, verbose);
112
127
  };
113
128
  /**
114
129
  * Run only code implementation phase
@@ -119,7 +134,7 @@ const runOnlyCodeImplementation = async (options, config) => {
119
134
  const results = [];
120
135
  const implementationResult = await runCodeImplementationPhase(options, config);
121
136
  results.push(logPhaseResult(implementationResult, verbose));
122
- return logPipelineComplete(results, verbose);
137
+ return finalizePipelineExecution(options, results, verbose);
123
138
  };
124
139
  /**
125
140
  * Run only functional testing phase
@@ -130,7 +145,7 @@ const runOnlyFunctionalTesting = async (options, config) => {
130
145
  const results = [];
131
146
  const testingResult = await runFunctionalTestingPhase(options, config);
132
147
  results.push(logPhaseResult(testingResult, verbose));
133
- return logPipelineComplete(results, verbose);
148
+ return finalizePipelineExecution(options, results, verbose);
134
149
  };
135
150
  /**
136
151
  * Run from feature analysis to end
@@ -143,19 +158,19 @@ const runFromFeatureAnalysis = async (options, config) => {
143
158
  const analysisResult = await runFeatureAnalysisPhase(options, config);
144
159
  results.push(logPhaseResult(analysisResult, verbose));
145
160
  if (!shouldContinuePipeline(results)) {
146
- return logPipelineComplete(results, verbose);
161
+ return finalizePipelineExecution(options, results, verbose);
147
162
  }
148
163
  // 2. Technical Design
149
164
  const designResult = await runTechnicalDesignPhase(options, config);
150
165
  results.push(logPhaseResult(designResult, verbose));
151
166
  if (!shouldContinuePipeline(results)) {
152
- return logPipelineComplete(results, verbose);
167
+ return finalizePipelineExecution(options, results, verbose);
153
168
  }
154
169
  // 3. Code Implementation
155
170
  const implementationResult = await runCodeImplementationPhase(options, config);
156
171
  results.push(logPhaseResult(implementationResult, verbose));
157
172
  if (!shouldContinuePipeline(results)) {
158
- return logPipelineComplete(results, verbose);
173
+ return finalizePipelineExecution(options, results, verbose);
159
174
  }
160
175
  // 4. Functional Testing with retry loop for bug fixes
161
176
  const testingResult = await handleTestFailuresWithRetry({
@@ -185,7 +200,7 @@ const runFromFeatureAnalysis = async (options, config) => {
185
200
  }
186
201
  }
187
202
  }
188
- return logPipelineComplete(results, verbose);
203
+ return finalizePipelineExecution(options, results, verbose);
189
204
  };
190
205
  /**
191
206
  * Run from technical design to end
@@ -198,13 +213,13 @@ const runFromTechnicalDesign = async (options, config) => {
198
213
  const designResult = await runTechnicalDesignPhase(options, config);
199
214
  results.push(logPhaseResult(designResult, verbose));
200
215
  if (!shouldContinuePipeline(results)) {
201
- return logPipelineComplete(results, verbose);
216
+ return finalizePipelineExecution(options, results, verbose);
202
217
  }
203
218
  // 2. Code Implementation
204
219
  const implementationResult = await runCodeImplementationPhase(options, config);
205
220
  results.push(logPhaseResult(implementationResult, verbose));
206
221
  if (!shouldContinuePipeline(results)) {
207
- return logPipelineComplete(results, verbose);
222
+ return finalizePipelineExecution(options, results, verbose);
208
223
  }
209
224
  // 3. Functional Testing with retry loop for bug fixes
210
225
  const testingResult = await handleTestFailuresWithRetry({
@@ -234,7 +249,7 @@ const runFromTechnicalDesign = async (options, config) => {
234
249
  }
235
250
  }
236
251
  }
237
- return logPipelineComplete(results, verbose);
252
+ return finalizePipelineExecution(options, results, verbose);
238
253
  };
239
254
  /**
240
255
  * Run from code implementation to end
@@ -247,7 +262,7 @@ const runFromCodeImplementation = async (options, config) => {
247
262
  const implementationResult = await runCodeImplementationPhase(options, config);
248
263
  results.push(logPhaseResult(implementationResult, verbose));
249
264
  if (!shouldContinuePipeline(results)) {
250
- return logPipelineComplete(results, verbose);
265
+ return finalizePipelineExecution(options, results, verbose);
251
266
  }
252
267
  // 2. Functional Testing with retry loop for bug fixes
253
268
  const testingResult = await handleTestFailuresWithRetry({
@@ -277,7 +292,7 @@ const runFromCodeImplementation = async (options, config) => {
277
292
  }
278
293
  }
279
294
  }
280
- return logPipelineComplete(results, verbose);
295
+ return finalizePipelineExecution(options, results, verbose);
281
296
  };
282
297
  /**
283
298
  * Run from functional testing to end
@@ -314,7 +329,7 @@ const runFromFunctionalTesting = async (options, config) => {
314
329
  }
315
330
  }
316
331
  }
317
- return logPipelineComplete(results, verbose);
332
+ return finalizePipelineExecution(options, results, verbose);
318
333
  };
319
334
  /**
320
335
  * Run only pull request creation phase
@@ -349,7 +364,7 @@ const runOnlyPullRequest = async (options, config) => {
349
364
  data: {},
350
365
  };
351
366
  results.push(logPhaseResult(prResult, verbose));
352
- return logPipelineComplete(results, verbose);
367
+ return finalizePipelineExecution(options, results, verbose);
353
368
  };
354
369
  /**
355
370
  * Run only code refine phase (refine code based on PR feedback)
@@ -366,7 +381,7 @@ const runOnlyCodeRefine = async (options, config) => {
366
381
  results,
367
382
  verbose,
368
383
  });
369
- return logPipelineComplete(results, verbose);
384
+ return finalizePipelineExecution(options, results, verbose);
370
385
  };
371
386
  /**
372
387
  * Run only code review phase (review PR and create review comments)
@@ -378,7 +393,7 @@ const runOnlyCodeReview = async (options, config) => {
378
393
  // Code Review - analyze PR and create review comments
379
394
  const reviewResult = await runCodeReviewPhase(options, config);
380
395
  results.push(logPhaseResult(reviewResult, verbose));
381
- return logPipelineComplete(results, verbose);
396
+ return finalizePipelineExecution(options, results, verbose);
382
397
  };
383
398
  /**
384
399
  * Run from code review to end (code-review → code-refine → code-refine-verification)
@@ -391,7 +406,7 @@ const runFromCodeReview = async (options, config) => {
391
406
  const reviewResult = await runCodeReviewPhase(options, config);
392
407
  results.push(logPhaseResult(reviewResult, verbose));
393
408
  if (!shouldContinuePipeline(results)) {
394
- return logPipelineComplete(results, verbose);
409
+ return finalizePipelineExecution(options, results, verbose);
395
410
  }
396
411
  // 2. Code Refine with automatic retry for verification failures
397
412
  await handleCodeRefineWithRetry({
@@ -400,7 +415,7 @@ const runFromCodeReview = async (options, config) => {
400
415
  results,
401
416
  verbose,
402
417
  });
403
- return logPipelineComplete(results, verbose);
418
+ return finalizePipelineExecution(options, results, verbose);
404
419
  };
405
420
  /**
406
421
  * Run from pull request creation to end (pull-request → code-review → code-refine → code-refine-verification)
@@ -429,11 +444,11 @@ const runFromPullRequest = async (options, config) => {
429
444
  if (verbose) {
430
445
  logInfo('⚠️ Pull request creation failed, stopping workflow');
431
446
  }
432
- return logPipelineComplete(results, verbose);
447
+ return finalizePipelineExecution(options, results, verbose);
433
448
  }
434
449
  // 2. Continue with code review and refine workflow
435
450
  await continueWithCodeReviewAndRefine(options, config, results, verbose);
436
- return logPipelineComplete(results, verbose);
451
+ return finalizePipelineExecution(options, results, verbose);
437
452
  };
438
453
  /**
439
454
  * Continue with code review and refine after PR creation
@@ -70,4 +70,5 @@ export const PHASE_STATUS_MAP = {
70
70
  'testing-in-progress': 'testing_in_progress',
71
71
  'testing-passed': 'testing_passed',
72
72
  'testing-failed': 'testing_failed',
73
+ 'ready-for-review': 'ready_for_review',
73
74
  };
@@ -36,7 +36,22 @@ const pushChanges = (verbose) => {
36
36
  }
37
37
  }
38
38
  catch (error) {
39
- throw new Error(`Failed to push changes: ${error instanceof Error ? error.message : String(error)}`);
39
+ // If push fails due to non-fast-forward, use force-with-lease for safer force push
40
+ // force-with-lease ensures we don't overwrite others' work by checking remote state
41
+ if (verbose) {
42
+ logInfo(`⚠️ Push rejected, attempting force push with lease...`);
43
+ }
44
+ try {
45
+ execSync('git push --force-with-lease origin $(git branch --show-current)', {
46
+ encoding: 'utf-8',
47
+ });
48
+ if (verbose) {
49
+ logInfo(`✅ Successfully force pushed changes`);
50
+ }
51
+ }
52
+ catch (forceError) {
53
+ throw new Error(`Failed to push changes: ${forceError instanceof Error ? forceError.message : String(forceError)}`);
54
+ }
40
55
  }
41
56
  };
42
57
  /**
@@ -11,6 +11,8 @@ export interface Feedback {
11
11
  id: string;
12
12
  feature_id: string | null;
13
13
  product_id: string | null;
14
+ user_story_id?: string | null;
15
+ test_case_id?: string | null;
14
16
  phase: string;
15
17
  feedback_type: FeedbackType;
16
18
  title: string;
@@ -141,6 +141,15 @@ function formatFeedbacksList(feedbacks) {
141
141
  const priorityBadge = getPriorityBadge(feedback.priority);
142
142
  // Build context metadata section
143
143
  let contextInfo = '';
144
+ // Item references (User Story or Test Case)
145
+ if (feedback.user_story_id) {
146
+ contextInfo += `\n**User Story ID**: ${feedback.user_story_id}`;
147
+ contextInfo += `\n*Note: This feedback applies to the user story referenced above in the context. Look for the ID in the "Existing User Stories" section.*`;
148
+ }
149
+ if (feedback.test_case_id) {
150
+ contextInfo += `\n**Test Case ID**: ${feedback.test_case_id}`;
151
+ contextInfo += `\n*Note: This feedback applies to the test case referenced above in the context. Look for the ID in the "Existing Test Cases" section.*`;
152
+ }
144
153
  // Document type
145
154
  if (feedback.document_type) {
146
155
  const docTypeDisplay = feedback.document_type
@@ -11,7 +11,7 @@ export interface PipelinePhaseOptions {
11
11
  export interface PipelineResult {
12
12
  readonly featureId: string;
13
13
  readonly phase: string;
14
- readonly status: 'success' | 'error';
14
+ readonly status: 'success' | 'error' | 'blocked';
15
15
  readonly message: string;
16
16
  readonly data?: unknown;
17
17
  }
@@ -238,8 +238,12 @@ export function returnToMainBranch(baseBranch = 'main', verbose) {
238
238
  if (verbose) {
239
239
  logError(`⚠️ Warning: Uncommitted changes detected before returning to ${baseBranch}:`);
240
240
  uncommittedFiles.forEach((file) => logError(` ${file}`));
241
- logError(` These changes will remain on the ${currentBranch} branch.`);
241
+ logError(` Resetting uncommitted changes to allow branch switch.`);
242
242
  }
243
+ // Reset uncommitted changes to allow clean branch switch
244
+ // The code should already be committed on the feature branch,
245
+ // so these changes are likely from post-commit operations
246
+ resetUncommittedChanges(verbose);
243
247
  }
244
248
  switchToBranch(baseBranch, verbose);
245
249
  if (verbose) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "edsger",
3
- "version": "0.9.0",
3
+ "version": "0.9.2",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "edsger": "dist/index.js"