edsger 0.13.3 → 0.14.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.
@@ -1,3 +1,4 @@
1
+ import { FeatureWorkflow } from '../../types/pipeline.js';
1
2
  /**
2
3
  * Update feature with new data
3
4
  */
@@ -6,8 +7,15 @@ export declare function updateFeature(featureId: string, updates: {
6
7
  status?: string;
7
8
  pull_request_url?: string;
8
9
  execution_mode?: string;
10
+ workflow?: FeatureWorkflow;
9
11
  }, verbose?: boolean): Promise<boolean>;
10
12
  /**
11
13
  * Update technical design for a feature
12
14
  */
13
15
  export declare function updateTechnicalDesign(featureId: string, technicalDesign: string, verbose?: boolean): Promise<boolean>;
16
+ /**
17
+ * Mark a workflow phase as completed
18
+ * Fetches current workflow, updates the phase status, and saves back
19
+ * Accepts phase names in either format (hyphens or underscores)
20
+ */
21
+ export declare function markWorkflowPhaseCompleted(featureId: string, phaseName: string, verbose?: boolean): Promise<boolean>;
@@ -29,3 +29,55 @@ export async function updateFeature(featureId, updates, verbose) {
29
29
  export async function updateTechnicalDesign(featureId, technicalDesign, verbose) {
30
30
  return updateFeature(featureId, { technical_design: technicalDesign }, verbose);
31
31
  }
32
+ /**
33
+ * Normalize phase name from pipeline format (hyphens) to workflow format (underscores)
34
+ * e.g., 'feature-analysis' -> 'feature_analysis'
35
+ */
36
+ function normalizePhaseNameForWorkflow(phaseName) {
37
+ return phaseName.replace(/-/g, '_');
38
+ }
39
+ /**
40
+ * Mark a workflow phase as completed
41
+ * Fetches current workflow, updates the phase status, and saves back
42
+ * Accepts phase names in either format (hyphens or underscores)
43
+ */
44
+ export async function markWorkflowPhaseCompleted(featureId, phaseName, verbose) {
45
+ try {
46
+ // Normalize phase name to workflow format (underscores)
47
+ const normalizedPhaseName = normalizePhaseNameForWorkflow(phaseName);
48
+ if (verbose) {
49
+ logInfo(`Marking workflow phase '${normalizedPhaseName}' as completed for feature: ${featureId}`);
50
+ }
51
+ // Fetch current feature to get workflow
52
+ const response = (await callMcpEndpoint('features/get', {
53
+ feature_id: featureId,
54
+ }));
55
+ const feature = response?.features?.[0];
56
+ if (!feature) {
57
+ logError(`Feature not found: ${featureId}`);
58
+ return false;
59
+ }
60
+ const workflow = feature.workflow || [];
61
+ if (workflow.length === 0) {
62
+ if (verbose) {
63
+ logInfo(`No workflow defined for feature, skipping phase completion`);
64
+ }
65
+ return true;
66
+ }
67
+ // Update the phase status
68
+ const updatedWorkflow = workflow.map((p) => p.phase === normalizedPhaseName
69
+ ? {
70
+ ...p,
71
+ status: 'completed',
72
+ executed_at: new Date().toISOString(),
73
+ }
74
+ : p);
75
+ // Save updated workflow
76
+ return await updateFeature(featureId, { workflow: updatedWorkflow }, verbose);
77
+ }
78
+ catch (error) {
79
+ const errorMessage = error instanceof Error ? error.message : String(error);
80
+ logError(`Failed to mark workflow phase completed: ${errorMessage}`);
81
+ return false;
82
+ }
83
+ }
@@ -125,6 +125,7 @@ export function getExecutionModeDescription(mode) {
125
125
  from_functional_testing: 'Execute from functional testing to end: testing',
126
126
  from_pull_request: 'Execute from pull request creation to end: pull-request → code-review → code-refine → code-refine-verification',
127
127
  from_code_review: 'Execute from code review to end: code-review → code-refine → code-refine-verification',
128
+ custom: 'Execute user-selected phases in order',
128
129
  };
129
130
  return descriptions[mode] || 'Unknown execution mode';
130
131
  }
@@ -13,13 +13,24 @@
13
13
  *
14
14
  * Uses functional programming principles for composability
15
15
  */
16
- import { updateFeatureStatusForPhase } from '../../api/features/index.js';
16
+ import { updateFeatureStatusForPhase, markWorkflowPhaseCompleted, } from '../../api/features/index.js';
17
17
  import { runFeatureAnalysisPhase, runTechnicalDesignPhase, runCodeImplementationPhase, runFunctionalTestingPhase, runCodeTestingPhase, runCodeReviewPhase, } from './executors/phase-executor.js';
18
18
  import { logPipelineStart, logPhaseResult, logPipelineComplete, shouldContinuePipeline, } from '../../utils/pipeline-logger.js';
19
19
  import { handleTestFailuresWithRetry } from '../../phases/functional-testing/test-retry-handler.js';
20
20
  import { handleCodeRefineWithRetry } from '../../phases/code-refine/retry-handler.js';
21
21
  import { handlePullRequestCreation } from '../../phases/pull-request/handler.js';
22
22
  import { logInfo } from '../../utils/logger.js';
23
+ /**
24
+ * Helper to log phase result and mark workflow phase as completed if successful
25
+ */
26
+ const logAndMarkPhaseCompleted = async (result, verbose) => {
27
+ const logged = logPhaseResult(result, verbose);
28
+ // Mark workflow phase as completed if phase succeeded
29
+ if (result.status === 'success') {
30
+ await markWorkflowPhaseCompleted(result.featureId, result.phase, verbose);
31
+ }
32
+ return logged;
33
+ };
23
34
  /**
24
35
  * Orchestrate phase execution based on execution mode
25
36
  * Routes to appropriate phase sequence based on mode (only_*, from_*, full_pipeline)
@@ -111,7 +122,7 @@ const runOnlyFeatureAnalysis = async (options, config) => {
111
122
  logPipelineStart(featureId, verbose);
112
123
  const results = [];
113
124
  const analysisResult = await runFeatureAnalysisPhase(options, config);
114
- results.push(logPhaseResult(analysisResult, verbose));
125
+ results.push(await logAndMarkPhaseCompleted(analysisResult, verbose));
115
126
  return finalizePipelineExecution(options, results, verbose);
116
127
  };
117
128
  /**
@@ -122,7 +133,7 @@ const runOnlyTechnicalDesign = async (options, config) => {
122
133
  logPipelineStart(featureId, verbose);
123
134
  const results = [];
124
135
  const designResult = await runTechnicalDesignPhase(options, config);
125
- results.push(logPhaseResult(designResult, verbose));
136
+ results.push(await logAndMarkPhaseCompleted(designResult, verbose));
126
137
  return finalizePipelineExecution(options, results, verbose);
127
138
  };
128
139
  /**
@@ -134,7 +145,7 @@ const runOnlyCodeImplementation = async (options, config) => {
134
145
  logPipelineStart(featureId, verbose);
135
146
  const results = [];
136
147
  const implementationResult = await runCodeImplementationPhase(options, config);
137
- results.push(logPhaseResult(implementationResult, verbose));
148
+ results.push(await logAndMarkPhaseCompleted(implementationResult, verbose));
138
149
  // If implementation succeeded, create a pull request so the code can be reviewed
139
150
  if (implementationResult.status === 'success') {
140
151
  if (verbose) {
@@ -156,7 +167,7 @@ const runOnlyCodeImplementation = async (options, config) => {
156
167
  : 'Pull request creation failed',
157
168
  data: {},
158
169
  };
159
- results.push(logPhaseResult(prResult, verbose));
170
+ results.push(await logAndMarkPhaseCompleted(prResult, verbose));
160
171
  }
161
172
  return finalizePipelineExecution(options, results, verbose);
162
173
  };
@@ -168,7 +179,7 @@ const runOnlyFunctionalTesting = async (options, config) => {
168
179
  logPipelineStart(featureId, verbose);
169
180
  const results = [];
170
181
  const testingResult = await runFunctionalTestingPhase(options, config);
171
- results.push(logPhaseResult(testingResult, verbose));
182
+ results.push(await logAndMarkPhaseCompleted(testingResult, verbose));
172
183
  return finalizePipelineExecution(options, results, verbose);
173
184
  };
174
185
  /**
@@ -180,19 +191,19 @@ const runFromFeatureAnalysis = async (options, config) => {
180
191
  const results = [];
181
192
  // 1. Feature Analysis
182
193
  const analysisResult = await runFeatureAnalysisPhase(options, config);
183
- results.push(logPhaseResult(analysisResult, verbose));
194
+ results.push(await logAndMarkPhaseCompleted(analysisResult, verbose));
184
195
  if (!shouldContinuePipeline(results)) {
185
196
  return finalizePipelineExecution(options, results, verbose);
186
197
  }
187
198
  // 2. Technical Design
188
199
  const designResult = await runTechnicalDesignPhase(options, config);
189
- results.push(logPhaseResult(designResult, verbose));
200
+ results.push(await logAndMarkPhaseCompleted(designResult, verbose));
190
201
  if (!shouldContinuePipeline(results)) {
191
202
  return finalizePipelineExecution(options, results, verbose);
192
203
  }
193
204
  // 3. Code Implementation
194
205
  const implementationResult = await runCodeImplementationPhase(options, config);
195
- results.push(logPhaseResult(implementationResult, verbose));
206
+ results.push(await logAndMarkPhaseCompleted(implementationResult, verbose));
196
207
  if (!shouldContinuePipeline(results)) {
197
208
  return finalizePipelineExecution(options, results, verbose);
198
209
  }
@@ -235,13 +246,13 @@ const runFromTechnicalDesign = async (options, config) => {
235
246
  const results = [];
236
247
  // 1. Technical Design
237
248
  const designResult = await runTechnicalDesignPhase(options, config);
238
- results.push(logPhaseResult(designResult, verbose));
249
+ results.push(await logAndMarkPhaseCompleted(designResult, verbose));
239
250
  if (!shouldContinuePipeline(results)) {
240
251
  return finalizePipelineExecution(options, results, verbose);
241
252
  }
242
253
  // 2. Code Implementation
243
254
  const implementationResult = await runCodeImplementationPhase(options, config);
244
- results.push(logPhaseResult(implementationResult, verbose));
255
+ results.push(await logAndMarkPhaseCompleted(implementationResult, verbose));
245
256
  if (!shouldContinuePipeline(results)) {
246
257
  return finalizePipelineExecution(options, results, verbose);
247
258
  }
@@ -284,7 +295,7 @@ const runFromCodeImplementation = async (options, config) => {
284
295
  const results = [];
285
296
  // 1. Code Implementation
286
297
  const implementationResult = await runCodeImplementationPhase(options, config);
287
- results.push(logPhaseResult(implementationResult, verbose));
298
+ results.push(await logAndMarkPhaseCompleted(implementationResult, verbose));
288
299
  if (!shouldContinuePipeline(results)) {
289
300
  return finalizePipelineExecution(options, results, verbose);
290
301
  }
@@ -387,7 +398,7 @@ const runOnlyPullRequest = async (options, config) => {
387
398
  : 'Pull request creation failed',
388
399
  data: {},
389
400
  };
390
- results.push(logPhaseResult(prResult, verbose));
401
+ results.push(await logAndMarkPhaseCompleted(prResult, verbose));
391
402
  return finalizePipelineExecution(options, results, verbose);
392
403
  };
393
404
  /**
@@ -416,7 +427,7 @@ const runOnlyCodeReview = async (options, config) => {
416
427
  const results = [];
417
428
  // Code Review - analyze PR and create review comments
418
429
  const reviewResult = await runCodeReviewPhase(options, config);
419
- results.push(logPhaseResult(reviewResult, verbose));
430
+ results.push(await logAndMarkPhaseCompleted(reviewResult, verbose));
420
431
  return finalizePipelineExecution(options, results, verbose);
421
432
  };
422
433
  /**
@@ -428,7 +439,7 @@ const runFromCodeReview = async (options, config) => {
428
439
  const results = [];
429
440
  // 1. Code Review - analyze PR and create review comments
430
441
  const reviewResult = await runCodeReviewPhase(options, config);
431
- results.push(logPhaseResult(reviewResult, verbose));
442
+ results.push(await logAndMarkPhaseCompleted(reviewResult, verbose));
432
443
  if (!shouldContinuePipeline(results)) {
433
444
  return finalizePipelineExecution(options, results, verbose);
434
445
  }
@@ -484,7 +495,7 @@ const continueWithCodeReviewAndRefine = async (options, config, results, verbose
484
495
  }
485
496
  // 1. Code Review - analyze PR and create review comments
486
497
  const reviewResult = await runCodeReviewPhase(options, config);
487
- results.push(logPhaseResult(reviewResult, verbose));
498
+ results.push(await logAndMarkPhaseCompleted(reviewResult, verbose));
488
499
  if (!shouldContinuePipeline(results)) {
489
500
  return;
490
501
  }
@@ -503,5 +514,5 @@ const continueWithCodeReviewAndRefine = async (options, config, results, verbose
503
514
  logInfo('\n📝 Starting code testing phase...');
504
515
  }
505
516
  const testingResult = await runCodeTestingPhase(options, config);
506
- results.push(logPhaseResult(testingResult, verbose));
517
+ results.push(await logAndMarkPhaseCompleted(testingResult, verbose));
507
518
  };
@@ -36,3 +36,21 @@ export declare const STATUS_PROGRESSION_ORDER: readonly FeatureStatus[];
36
36
  * and skip the status update to prevent unintended regression to 'backlog'.
37
37
  */
38
38
  export declare const PHASE_STATUS_MAP: Record<string, FeatureStatus>;
39
+ /**
40
+ * Human-selectable statuses
41
+ * These are the statuses that a human user can manually set.
42
+ *
43
+ * Excluded statuses (system-managed):
44
+ * - *_verification statuses: Automatically entered after completing the corresponding phase
45
+ * - testing_in_progress: Automatically set when functional testing begins
46
+ * - testing_passed: Automatically set when all tests pass
47
+ * - testing_failed: Automatically set when tests fail
48
+ *
49
+ * These excluded statuses should not be shown in UI dropdowns for manual status changes
50
+ * because they represent intermediate states that are managed by the workflow system.
51
+ */
52
+ export declare const HUMAN_SELECTABLE_STATUSES: readonly FeatureStatus[];
53
+ /**
54
+ * Check if a status can be manually selected by a human user
55
+ */
56
+ export declare function isHumanSelectableStatus(status: FeatureStatus): boolean;
@@ -72,3 +72,36 @@ export const PHASE_STATUS_MAP = {
72
72
  'testing-failed': 'testing_failed',
73
73
  'ready-for-review': 'ready_for_review',
74
74
  };
75
+ /**
76
+ * Human-selectable statuses
77
+ * These are the statuses that a human user can manually set.
78
+ *
79
+ * Excluded statuses (system-managed):
80
+ * - *_verification statuses: Automatically entered after completing the corresponding phase
81
+ * - testing_in_progress: Automatically set when functional testing begins
82
+ * - testing_passed: Automatically set when all tests pass
83
+ * - testing_failed: Automatically set when tests fail
84
+ *
85
+ * These excluded statuses should not be shown in UI dropdowns for manual status changes
86
+ * because they represent intermediate states that are managed by the workflow system.
87
+ */
88
+ export const HUMAN_SELECTABLE_STATUSES = [
89
+ 'backlog',
90
+ 'ready_for_ai',
91
+ 'feature_analysis',
92
+ 'technical_design',
93
+ 'code_implementation',
94
+ 'code_refine',
95
+ 'bug_fixing',
96
+ 'code_review',
97
+ 'pull_request',
98
+ 'functional_testing',
99
+ 'ready_for_review',
100
+ 'shipped',
101
+ ];
102
+ /**
103
+ * Check if a status can be manually selected by a human user
104
+ */
105
+ export function isHumanSelectableStatus(status) {
106
+ return HUMAN_SELECTABLE_STATUSES.includes(status);
107
+ }
@@ -1,3 +1,4 @@
1
+ import { FeatureWorkflow } from './pipeline.js';
1
2
  export interface FeatureInfo {
2
3
  id: string;
3
4
  name: string;
@@ -6,6 +7,7 @@ export interface FeatureInfo {
6
7
  status: string;
7
8
  product_id: string;
8
9
  execution_mode?: string;
10
+ workflow?: FeatureWorkflow | null;
9
11
  pull_request_url?: string;
10
12
  created_at?: string;
11
13
  updated_at?: string;
@@ -3,7 +3,14 @@
3
3
  */
4
4
  import { EdsgerConfig } from './index.js';
5
5
  import { ChecklistPhaseContext } from '../services/checklist.js';
6
- export type ExecutionMode = 'full_pipeline' | 'only_feature_analysis' | 'only_technical_design' | 'only_code_implementation' | 'only_functional_testing' | 'only_pull_request' | 'only_code_refine' | 'only_code_review' | 'from_feature_analysis' | 'from_technical_design' | 'from_code_implementation' | 'from_functional_testing' | 'from_code_review' | 'from_pull_request';
6
+ export type ExecutionMode = 'full_pipeline' | 'only_feature_analysis' | 'only_technical_design' | 'only_code_implementation' | 'only_functional_testing' | 'only_pull_request' | 'only_code_refine' | 'only_code_review' | 'from_feature_analysis' | 'from_technical_design' | 'from_code_implementation' | 'from_functional_testing' | 'from_code_review' | 'from_pull_request' | 'custom';
7
+ export type WorkflowPhaseStatus = 'pending' | 'completed';
8
+ export interface WorkflowPhase {
9
+ phase: string;
10
+ status: WorkflowPhaseStatus;
11
+ executed_at?: string;
12
+ }
13
+ export type FeatureWorkflow = WorkflowPhase[];
7
14
  export interface PipelinePhaseOptions {
8
15
  readonly featureId: string;
9
16
  readonly verbose?: boolean;
@@ -409,42 +409,42 @@ export async function syncFeatBranchWithMain(featureId, githubToken, owner, repo
409
409
  branch: featBranch,
410
410
  });
411
411
  const featSha = featBranchData.commit.sha;
412
- // Check if feat branch is already up to date (same as main or ahead)
413
- // We need to merge main into feat to keep it updated
412
+ // Check if feat branch is already up to date
414
413
  if (verbose) {
415
414
  logInfo(`📥 Syncing ${featBranch} with ${baseBranch}...`);
416
415
  logInfo(` ${baseBranch} SHA: ${mainSha.substring(0, 7)}`);
417
416
  logInfo(` ${featBranch} SHA: ${featSha.substring(0, 7)}`);
418
417
  }
419
- // Use GitHub merge API to merge main into feat branch
418
+ // If already at the same SHA, no need to update
419
+ if (mainSha === featSha) {
420
+ if (verbose) {
421
+ logInfo(`ℹ️ ${featBranch} is already up to date with ${baseBranch}`);
422
+ }
423
+ return true;
424
+ }
425
+ // Use git.updateRef to fast-forward feat branch to main's SHA
426
+ // This avoids creating a merge commit
420
427
  try {
421
- await octokit.repos.merge({
428
+ await octokit.git.updateRef({
422
429
  owner,
423
430
  repo,
424
- base: featBranch,
425
- head: baseBranch,
426
- commit_message: `chore: sync ${featBranch} with ${baseBranch}`,
431
+ ref: `heads/${featBranch}`,
432
+ sha: mainSha,
433
+ force: true, // Force update since feat branch may have diverged
427
434
  });
428
435
  if (verbose) {
429
- logInfo(`✅ Successfully synced ${featBranch} with ${baseBranch}`);
436
+ logInfo(`✅ Successfully synced ${featBranch} to ${baseBranch} (${mainSha.substring(0, 7)})`);
430
437
  }
431
438
  }
432
- catch (mergeError) {
433
- // 409 means nothing to merge (already up to date)
434
- if (mergeError.status === 409) {
435
- if (verbose) {
436
- logInfo(`ℹ️ ${featBranch} is already up to date with ${baseBranch}`);
437
- }
438
- return true;
439
- }
440
- // 404 means branch doesn't exist (shouldn't happen since we checked above)
441
- if (mergeError.status === 404) {
439
+ catch (updateError) {
440
+ // 422 means the ref doesn't exist or other validation error
441
+ if (updateError.status === 422 || updateError.status === 404) {
442
442
  if (verbose) {
443
- logInfo(`ℹ️ ${featBranch} branch not found, skipping sync`);
443
+ logInfo(`ℹ️ Could not update ${featBranch} ref, skipping sync`);
444
444
  }
445
445
  return true;
446
446
  }
447
- throw mergeError;
447
+ throw updateError;
448
448
  }
449
449
  return true;
450
450
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "edsger",
3
- "version": "0.13.3",
3
+ "version": "0.14.0",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "edsger": "dist/index.js"