ai-sdlc 0.1.0-alpha.1

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 (96) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +847 -0
  3. package/dist/agents/implementation.d.ts +11 -0
  4. package/dist/agents/implementation.d.ts.map +1 -0
  5. package/dist/agents/implementation.js +123 -0
  6. package/dist/agents/implementation.js.map +1 -0
  7. package/dist/agents/index.d.ts +7 -0
  8. package/dist/agents/index.d.ts.map +1 -0
  9. package/dist/agents/index.js +8 -0
  10. package/dist/agents/index.js.map +1 -0
  11. package/dist/agents/planning.d.ts +9 -0
  12. package/dist/agents/planning.d.ts.map +1 -0
  13. package/dist/agents/planning.js +84 -0
  14. package/dist/agents/planning.js.map +1 -0
  15. package/dist/agents/refinement.d.ts +10 -0
  16. package/dist/agents/refinement.d.ts.map +1 -0
  17. package/dist/agents/refinement.js +98 -0
  18. package/dist/agents/refinement.js.map +1 -0
  19. package/dist/agents/research.d.ts +16 -0
  20. package/dist/agents/research.d.ts.map +1 -0
  21. package/dist/agents/research.js +141 -0
  22. package/dist/agents/research.js.map +1 -0
  23. package/dist/agents/review.d.ts +24 -0
  24. package/dist/agents/review.d.ts.map +1 -0
  25. package/dist/agents/review.js +740 -0
  26. package/dist/agents/review.js.map +1 -0
  27. package/dist/agents/rework.d.ts +17 -0
  28. package/dist/agents/rework.d.ts.map +1 -0
  29. package/dist/agents/rework.js +139 -0
  30. package/dist/agents/rework.js.map +1 -0
  31. package/dist/agents/state-assessor.d.ts +21 -0
  32. package/dist/agents/state-assessor.d.ts.map +1 -0
  33. package/dist/agents/state-assessor.js +29 -0
  34. package/dist/agents/state-assessor.js.map +1 -0
  35. package/dist/cli/commands.d.ts +87 -0
  36. package/dist/cli/commands.d.ts.map +1 -0
  37. package/dist/cli/commands.js +1183 -0
  38. package/dist/cli/commands.js.map +1 -0
  39. package/dist/cli/formatting.d.ts +68 -0
  40. package/dist/cli/formatting.d.ts.map +1 -0
  41. package/dist/cli/formatting.js +194 -0
  42. package/dist/cli/formatting.js.map +1 -0
  43. package/dist/cli/runner.d.ts +57 -0
  44. package/dist/cli/runner.d.ts.map +1 -0
  45. package/dist/cli/runner.js +272 -0
  46. package/dist/cli/runner.js.map +1 -0
  47. package/dist/cli/story-utils.d.ts +19 -0
  48. package/dist/cli/story-utils.d.ts.map +1 -0
  49. package/dist/cli/story-utils.js +44 -0
  50. package/dist/cli/story-utils.js.map +1 -0
  51. package/dist/cli/table-renderer.d.ts +22 -0
  52. package/dist/cli/table-renderer.d.ts.map +1 -0
  53. package/dist/cli/table-renderer.js +159 -0
  54. package/dist/cli/table-renderer.js.map +1 -0
  55. package/dist/core/auth.d.ts +39 -0
  56. package/dist/core/auth.d.ts.map +1 -0
  57. package/dist/core/auth.js +128 -0
  58. package/dist/core/auth.js.map +1 -0
  59. package/dist/core/client.d.ts +73 -0
  60. package/dist/core/client.d.ts.map +1 -0
  61. package/dist/core/client.js +140 -0
  62. package/dist/core/client.js.map +1 -0
  63. package/dist/core/config.d.ts +48 -0
  64. package/dist/core/config.d.ts.map +1 -0
  65. package/dist/core/config.js +330 -0
  66. package/dist/core/config.js.map +1 -0
  67. package/dist/core/kanban.d.ts +34 -0
  68. package/dist/core/kanban.d.ts.map +1 -0
  69. package/dist/core/kanban.js +253 -0
  70. package/dist/core/kanban.js.map +1 -0
  71. package/dist/core/story.d.ts +91 -0
  72. package/dist/core/story.d.ts.map +1 -0
  73. package/dist/core/story.js +349 -0
  74. package/dist/core/story.js.map +1 -0
  75. package/dist/core/theme.d.ts +17 -0
  76. package/dist/core/theme.d.ts.map +1 -0
  77. package/dist/core/theme.js +136 -0
  78. package/dist/core/theme.js.map +1 -0
  79. package/dist/core/workflow-state.d.ts +56 -0
  80. package/dist/core/workflow-state.d.ts.map +1 -0
  81. package/dist/core/workflow-state.js +162 -0
  82. package/dist/core/workflow-state.js.map +1 -0
  83. package/dist/index.d.ts +3 -0
  84. package/dist/index.d.ts.map +1 -0
  85. package/dist/index.js +103 -0
  86. package/dist/index.js.map +1 -0
  87. package/dist/types/index.d.ts +228 -0
  88. package/dist/types/index.d.ts.map +1 -0
  89. package/dist/types/index.js +38 -0
  90. package/dist/types/index.js.map +1 -0
  91. package/dist/types/workflow-state.d.ts +54 -0
  92. package/dist/types/workflow-state.d.ts.map +1 -0
  93. package/dist/types/workflow-state.js +5 -0
  94. package/dist/types/workflow-state.js.map +1 -0
  95. package/package.json +71 -0
  96. package/templates/story.md +35 -0
@@ -0,0 +1,1183 @@
1
+ import ora from 'ora';
2
+ import fs from 'fs';
3
+ import { getSdlcRoot, loadConfig, initConfig } from '../core/config.js';
4
+ import { initializeKanban, kanbanExists, assessState, getBoardStats, findStoryBySlug, findStoryById } from '../core/kanban.js';
5
+ import { createStory, parseStory, resetRPIVCycle, isAtMaxRetries } from '../core/story.js';
6
+ import { ReviewDecision } from '../types/index.js';
7
+ import { getThemedChalk } from '../core/theme.js';
8
+ import { saveWorkflowState, loadWorkflowState, clearWorkflowState, generateWorkflowId, calculateStoryHash, hasWorkflowState, } from '../core/workflow-state.js';
9
+ import { renderStories } from './table-renderer.js';
10
+ import { getStoryFlags as getStoryFlagsUtil, formatStatus as formatStatusUtil } from './story-utils.js';
11
+ /**
12
+ * Initialize the .agentic-sdlc folder structure
13
+ */
14
+ export async function init() {
15
+ const spinner = ora('Initializing agentic-sdlc...').start();
16
+ try {
17
+ const config = initConfig();
18
+ const sdlcRoot = getSdlcRoot();
19
+ const c = getThemedChalk(config);
20
+ if (kanbanExists(sdlcRoot)) {
21
+ spinner.info('agentic-sdlc already initialized');
22
+ return;
23
+ }
24
+ initializeKanban(sdlcRoot);
25
+ spinner.succeed(c.success('Initialized .agentic-sdlc/'));
26
+ console.log(c.dim(' ├── backlog/'));
27
+ console.log(c.dim(' ├── ready/'));
28
+ console.log(c.dim(' ├── in-progress/'));
29
+ console.log(c.dim(' └── done/'));
30
+ console.log();
31
+ console.log(c.info('Get started:'));
32
+ console.log(c.dim(` agentic-sdlc add "Your first story"`));
33
+ }
34
+ catch (error) {
35
+ spinner.fail('Failed to initialize');
36
+ console.error(error);
37
+ process.exit(1);
38
+ }
39
+ }
40
+ /**
41
+ * Show current board state
42
+ */
43
+ export async function status(options) {
44
+ const config = loadConfig();
45
+ const sdlcRoot = getSdlcRoot();
46
+ const c = getThemedChalk(config);
47
+ if (!kanbanExists(sdlcRoot)) {
48
+ console.log(c.warning('agentic-sdlc not initialized. Run `agentic-sdlc init` first.'));
49
+ return;
50
+ }
51
+ const assessment = assessState(sdlcRoot);
52
+ const stats = getBoardStats(sdlcRoot);
53
+ console.log();
54
+ console.log(c.bold('═══ Agentic SDLC Board ═══'));
55
+ console.log();
56
+ // Show each column with new table format
57
+ const columns = [
58
+ { name: 'BACKLOG', folder: 'backlog', color: c.backlog },
59
+ { name: 'READY', folder: 'ready', color: c.ready },
60
+ { name: 'IN-PROGRESS', folder: 'in-progress', color: c.inProgress },
61
+ { name: 'DONE', folder: 'done', color: c.done },
62
+ ];
63
+ // Filter columns if --active flag is set
64
+ let displayColumns = columns;
65
+ let doneCount = 0;
66
+ if (options?.active) {
67
+ doneCount = stats['done'];
68
+ displayColumns = columns.filter(col => col.folder !== 'done');
69
+ }
70
+ for (const col of displayColumns) {
71
+ const count = stats[col.folder];
72
+ console.log(c.bold(col.color(`${col.name} (${count})`)));
73
+ const stories = col.folder === 'backlog' ? assessment.backlogItems
74
+ : col.folder === 'ready' ? assessment.readyItems
75
+ : col.folder === 'in-progress' ? assessment.inProgressItems
76
+ : assessment.doneItems;
77
+ // Use new table renderer
78
+ console.log(renderStories(stories, c));
79
+ console.log();
80
+ }
81
+ // Show summary line when done is filtered and there are done stories
82
+ if (options?.active && doneCount > 0) {
83
+ console.log(c.dim(`${doneCount} done stories (use 'status' without --active to show all)`));
84
+ console.log();
85
+ }
86
+ // Show recommended next action
87
+ if (assessment.recommendedActions.length > 0) {
88
+ const nextAction = assessment.recommendedActions[0];
89
+ console.log(c.info('Recommended:'), formatAction(nextAction));
90
+ }
91
+ else {
92
+ console.log(c.success('No pending actions. Board is up to date!'));
93
+ }
94
+ }
95
+ /**
96
+ * Add a new story to the backlog
97
+ */
98
+ export async function add(title) {
99
+ const spinner = ora('Creating story...').start();
100
+ try {
101
+ const config = loadConfig();
102
+ const sdlcRoot = getSdlcRoot();
103
+ const c = getThemedChalk(config);
104
+ if (!kanbanExists(sdlcRoot)) {
105
+ spinner.fail('agentic-sdlc not initialized. Run `agentic-sdlc init` first.');
106
+ return;
107
+ }
108
+ const story = createStory(title, sdlcRoot);
109
+ spinner.succeed(c.success(`Created: ${story.path}`));
110
+ console.log(c.dim(` ID: ${story.frontmatter.id}`));
111
+ console.log(c.dim(` Slug: ${story.slug}`));
112
+ console.log();
113
+ console.log(c.info('Next step:'), `agentic-sdlc run`);
114
+ }
115
+ catch (error) {
116
+ spinner.fail('Failed to create story');
117
+ console.error(error);
118
+ process.exit(1);
119
+ }
120
+ }
121
+ /**
122
+ * Validates flag combinations for --auto --story --step conflicts
123
+ * @throws Error if conflicting flags are detected
124
+ */
125
+ function validateAutoStoryOptions(options) {
126
+ if (options.auto && options.story && options.step) {
127
+ throw new Error('Cannot combine --auto --story with --step flag.\n' +
128
+ 'Use either:\n' +
129
+ ' - agentic-sdlc run --auto --story <id> (full SDLC)\n' +
130
+ ' - agentic-sdlc run --story <id> --step <phase> (single phase)');
131
+ }
132
+ }
133
+ /**
134
+ * Determines if a specific phase should be executed based on story state
135
+ * @param story The story to check
136
+ * @param phase The phase to evaluate
137
+ * @returns true if the phase should be executed, false if it should be skipped
138
+ */
139
+ function shouldExecutePhase(story, phase) {
140
+ switch (phase) {
141
+ case 'refine':
142
+ // Execute refine if story is in backlog
143
+ return story.frontmatter.status === 'backlog';
144
+ case 'research':
145
+ return !story.frontmatter.research_complete;
146
+ case 'plan':
147
+ return !story.frontmatter.plan_complete;
148
+ case 'implement':
149
+ return !story.frontmatter.implementation_complete;
150
+ case 'review':
151
+ return !story.frontmatter.reviews_complete;
152
+ default:
153
+ return false;
154
+ }
155
+ }
156
+ /**
157
+ * Generates the complete SDLC action sequence for a story
158
+ * @param story The target story
159
+ * @param c Themed chalk instance for logging (optional)
160
+ * @returns Array of actions to execute in sequence
161
+ */
162
+ function generateFullSDLCActions(story, c) {
163
+ const allPhases = ['refine', 'research', 'plan', 'implement', 'review'];
164
+ const actions = [];
165
+ const skippedPhases = [];
166
+ for (const phase of allPhases) {
167
+ if (shouldExecutePhase(story, phase)) {
168
+ actions.push({
169
+ type: phase,
170
+ storyId: story.frontmatter.id,
171
+ storyPath: story.path,
172
+ reason: `Full SDLC: ${phase} phase`,
173
+ priority: 0,
174
+ });
175
+ }
176
+ else {
177
+ skippedPhases.push(phase);
178
+ }
179
+ }
180
+ // Log skipped phases if chalk is provided
181
+ if (c && skippedPhases.length > 0) {
182
+ console.log(c.dim(` Skipping completed phases: ${skippedPhases.join(', ')}`));
183
+ }
184
+ return actions;
185
+ }
186
+ /**
187
+ * Run the workflow (process one action or all)
188
+ */
189
+ export async function run(options) {
190
+ const config = loadConfig();
191
+ // Parse maxIterations from CLI (undefined means use config default which is Infinity)
192
+ const maxIterationsOverride = options.maxIterations !== undefined
193
+ ? parseInt(options.maxIterations, 10)
194
+ : undefined;
195
+ const sdlcRoot = getSdlcRoot();
196
+ const c = getThemedChalk(config);
197
+ // Valid step names for --step option
198
+ const validSteps = ['refine', 'research', 'plan', 'implement', 'review'];
199
+ // Validate --step option early
200
+ if (options.step) {
201
+ const normalizedStep = options.step.toLowerCase();
202
+ if (!validSteps.includes(normalizedStep)) {
203
+ console.log(c.error(`Error: Invalid step "${options.step}"`));
204
+ console.log(c.dim(`Valid steps: ${validSteps.join(', ')}`));
205
+ return;
206
+ }
207
+ }
208
+ if (!kanbanExists(sdlcRoot)) {
209
+ console.log(c.warning('agentic-sdlc not initialized. Run `agentic-sdlc init` first.'));
210
+ return;
211
+ }
212
+ // Validate flag combinations
213
+ try {
214
+ validateAutoStoryOptions(options);
215
+ }
216
+ catch (error) {
217
+ console.log(c.error(`Error: ${error instanceof Error ? error.message : String(error)}`));
218
+ return;
219
+ }
220
+ // Detect full SDLC mode: --auto combined with --story
221
+ let isFullSDLC = !!(options.auto && options.story && !options.continue);
222
+ // Handle --continue flag
223
+ let workflowId;
224
+ let completedActions = [];
225
+ let storyContentHash;
226
+ if (options.continue) {
227
+ // Try to load existing state
228
+ const existingState = await loadWorkflowState(sdlcRoot);
229
+ if (!existingState) {
230
+ console.log(c.error('Error: No checkpoint found.'));
231
+ console.log(c.dim('Remove --continue flag to start a new workflow.'));
232
+ return;
233
+ }
234
+ workflowId = existingState.workflowId;
235
+ completedActions = existingState.completedActions;
236
+ storyContentHash = existingState.context.storyContentHash;
237
+ // Restore full SDLC mode from checkpoint if it was set
238
+ if (existingState.context.options.fullSDLC) {
239
+ isFullSDLC = true;
240
+ // Also restore the story option for proper filtering
241
+ if (existingState.context.options.story) {
242
+ options.story = existingState.context.options.story;
243
+ options.auto = true; // Ensure auto mode is set for continuation
244
+ }
245
+ }
246
+ // Display resume information
247
+ console.log();
248
+ console.log(c.info('⟳ Resuming workflow from checkpoint'));
249
+ console.log(c.dim(` Workflow ID: ${workflowId}`));
250
+ console.log(c.dim(` Checkpoint: ${new Date(existingState.timestamp).toLocaleString()}`));
251
+ console.log(c.dim(` Completed actions: ${completedActions.length}`));
252
+ if (isFullSDLC) {
253
+ console.log(c.dim(` Mode: Full SDLC (story: ${options.story})`));
254
+ }
255
+ // Warn if story content changed
256
+ if (storyContentHash && completedActions.length > 0) {
257
+ const lastAction = completedActions[completedActions.length - 1];
258
+ const currentHash = calculateStoryHash(lastAction.storyPath);
259
+ if (currentHash && currentHash !== storyContentHash) {
260
+ console.log(c.warning(' ⚠ Warning: Story content changed since interruption'));
261
+ console.log(c.dim(' Proceeding with current state...'));
262
+ }
263
+ }
264
+ // Check if workflow is stale (older than 48 hours)
265
+ const stateAge = Date.now() - new Date(existingState.timestamp).getTime();
266
+ const MAX_STATE_AGE_MS = 48 * 60 * 60 * 1000; // 48 hours
267
+ if (stateAge > MAX_STATE_AGE_MS) {
268
+ console.log(c.warning(' ⚠ Warning: Checkpoint is more than 48 hours old'));
269
+ console.log(c.dim(' Context may be stale. Consider starting fresh.'));
270
+ }
271
+ console.log();
272
+ }
273
+ else {
274
+ // Check if there's an existing state and suggest --continue
275
+ if (hasWorkflowState(sdlcRoot) && !options.dryRun) {
276
+ console.log(c.info('Note: Found previous checkpoint. Use --continue to resume.'));
277
+ console.log();
278
+ }
279
+ // Start new workflow
280
+ workflowId = generateWorkflowId();
281
+ }
282
+ let assessment = assessState(sdlcRoot);
283
+ // Filter actions by story if --story flag is provided
284
+ if (options.story) {
285
+ const normalizedInput = options.story.toLowerCase().trim();
286
+ // Try to find story by ID first, then by slug (case-insensitive)
287
+ let targetStory = findStoryById(sdlcRoot, normalizedInput);
288
+ if (!targetStory) {
289
+ targetStory = findStoryBySlug(sdlcRoot, normalizedInput);
290
+ }
291
+ // Also try original case for slug
292
+ if (!targetStory) {
293
+ targetStory = findStoryBySlug(sdlcRoot, options.story.trim());
294
+ }
295
+ if (!targetStory) {
296
+ console.log(c.error(`Error: Story not found: "${options.story}"`));
297
+ console.log();
298
+ console.log(c.dim('Searched for:'));
299
+ console.log(c.dim(` ID: ${normalizedInput}`));
300
+ console.log(c.dim(` Slug: ${normalizedInput}`));
301
+ console.log();
302
+ console.log(c.info('Tip: Use `agentic-sdlc status` to see all available stories.'));
303
+ return;
304
+ }
305
+ // Full SDLC mode: Generate complete phase sequence for the story
306
+ if (isFullSDLC) {
307
+ console.log();
308
+ console.log(c.bold(`🚀 Starting full SDLC for story: ${targetStory.frontmatter.title}`));
309
+ console.log(c.dim(` ID: ${targetStory.frontmatter.id}`));
310
+ console.log(c.dim(` Status: ${targetStory.frontmatter.status}`));
311
+ const fullSDLCActions = generateFullSDLCActions(targetStory, c);
312
+ const totalPhases = 5; // refine, research, plan, implement, review
313
+ const phasesToExecute = fullSDLCActions.length;
314
+ console.log(c.dim(` Phases to execute: ${phasesToExecute}/${totalPhases}`));
315
+ console.log();
316
+ if (fullSDLCActions.length === 0) {
317
+ console.log(c.success('✓ All SDLC phases already completed!'));
318
+ console.log(c.dim('Story has completed: refine, research, plan, implement, and review.'));
319
+ return;
320
+ }
321
+ // Replace assessment actions with full SDLC sequence
322
+ assessment.recommendedActions = fullSDLCActions;
323
+ }
324
+ else {
325
+ // Normal --story mode: Filter existing recommended actions
326
+ const originalCount = assessment.recommendedActions.length;
327
+ assessment.recommendedActions = assessment.recommendedActions.filter(action => action.storyPath === targetStory.path);
328
+ console.log(c.info(`Targeting story: ${targetStory.frontmatter.title}`));
329
+ console.log(c.dim(` ID: ${targetStory.frontmatter.id}`));
330
+ console.log(c.dim(` Status: ${targetStory.frontmatter.status}`));
331
+ console.log(c.dim(` Actions: ${assessment.recommendedActions.length} of ${originalCount} total`));
332
+ console.log();
333
+ }
334
+ }
335
+ // Filter actions by step type if --step flag is provided
336
+ if (options.step) {
337
+ const normalizedStep = options.step.toLowerCase();
338
+ const originalCount = assessment.recommendedActions.length;
339
+ assessment.recommendedActions = assessment.recommendedActions.filter(action => action.type === normalizedStep);
340
+ if (assessment.recommendedActions.length < originalCount) {
341
+ console.log(c.dim(`Filtered to "${options.step}" step: ${assessment.recommendedActions.length} actions`));
342
+ console.log();
343
+ }
344
+ }
345
+ if (assessment.recommendedActions.length === 0) {
346
+ if (options.story || options.step) {
347
+ const filterDesc = [
348
+ options.story ? `story "${options.story}"` : null,
349
+ options.step ? `step "${options.step}"` : null,
350
+ ].filter(Boolean).join(' and ');
351
+ console.log(c.info(`No pending actions for ${filterDesc}.`));
352
+ console.log(c.dim('The specified work may already be complete.'));
353
+ }
354
+ else {
355
+ console.log(c.success('No pending actions. Board is up to date!'));
356
+ }
357
+ // Clear state if workflow is complete
358
+ if (options.continue || hasWorkflowState(sdlcRoot)) {
359
+ await clearWorkflowState(sdlcRoot);
360
+ console.log(c.dim('Checkpoint cleared.'));
361
+ }
362
+ return;
363
+ }
364
+ if (options.dryRun) {
365
+ console.log(c.info('Dry run - would execute:'));
366
+ for (const action of assessment.recommendedActions) {
367
+ console.log(` ${formatAction(action)}`);
368
+ if (!options.auto)
369
+ break;
370
+ }
371
+ return;
372
+ }
373
+ // Filter out completed actions if resuming
374
+ let actionsToProcess = options.auto
375
+ ? assessment.recommendedActions
376
+ : [assessment.recommendedActions[0]];
377
+ if (options.continue && completedActions.length > 0) {
378
+ const completedActionKeys = new Set(completedActions.map(a => `${a.type}:${a.storyPath}`));
379
+ const skippedActions = [];
380
+ const remainingActions = [];
381
+ for (const action of actionsToProcess) {
382
+ const actionKey = `${action.type}:${action.storyPath}`;
383
+ if (completedActionKeys.has(actionKey)) {
384
+ skippedActions.push(action);
385
+ }
386
+ else {
387
+ remainingActions.push(action);
388
+ }
389
+ }
390
+ if (skippedActions.length > 0) {
391
+ console.log(c.dim('⊘ Skipping completed actions:'));
392
+ for (const action of skippedActions) {
393
+ console.log(c.dim(` ✓ ${formatAction(action)}`));
394
+ }
395
+ console.log();
396
+ }
397
+ actionsToProcess = remainingActions;
398
+ if (actionsToProcess.length === 0) {
399
+ console.log(c.success('All actions from checkpoint already completed!'));
400
+ await clearWorkflowState(sdlcRoot);
401
+ console.log(c.dim('Checkpoint cleared.'));
402
+ return;
403
+ }
404
+ }
405
+ // Process actions with retry support for Full SDLC mode
406
+ let currentActions = [...actionsToProcess];
407
+ let currentActionIndex = 0;
408
+ let retryAttempt = 0;
409
+ const MAX_DISPLAY_RETRIES = 3; // For display purposes
410
+ while (currentActionIndex < currentActions.length) {
411
+ const action = currentActions[currentActionIndex];
412
+ const totalActions = currentActions.length;
413
+ // Enhanced progress indicator for full SDLC mode
414
+ if (isFullSDLC && totalActions > 1) {
415
+ const retryIndicator = retryAttempt > 0 ? ` (retry ${retryAttempt})` : '';
416
+ console.log(c.info(`\n═══ Phase ${currentActionIndex + 1}/${totalActions}: ${action.type.toUpperCase()}${retryIndicator} ═══`));
417
+ }
418
+ const actionResult = await executeAction(action, sdlcRoot);
419
+ // Handle action failure in full SDLC mode
420
+ if (!actionResult.success && isFullSDLC) {
421
+ console.log();
422
+ console.log(c.error(`✗ Phase ${action.type} failed`));
423
+ console.log(c.dim(`Completed ${currentActionIndex} of ${totalActions} phases`));
424
+ console.log(c.info('Fix the error above and use --continue to resume.'));
425
+ return;
426
+ }
427
+ // Handle review rejection in Full SDLC mode - trigger retry loop
428
+ if (isFullSDLC && action.type === 'review' && actionResult.reviewResult) {
429
+ const reviewResult = actionResult.reviewResult;
430
+ if (reviewResult.decision === ReviewDecision.REJECTED) {
431
+ // Load fresh story state and config for retry check
432
+ const story = parseStory(action.storyPath);
433
+ const config = loadConfig();
434
+ // Check if we're at max retries (pass CLI override if provided)
435
+ if (isAtMaxRetries(story, config, maxIterationsOverride)) {
436
+ console.log();
437
+ console.log(c.error('═'.repeat(50)));
438
+ console.log(c.error(`✗ Review failed - maximum retries reached`));
439
+ console.log(c.error('═'.repeat(50)));
440
+ console.log(c.dim(`Story has reached the maximum retry limit.`));
441
+ console.log(c.dim(`Issues found: ${reviewResult.issues.length}`));
442
+ console.log(c.warning('Manual intervention required to address the review feedback.'));
443
+ console.log(c.info('You can:'));
444
+ console.log(c.dim(' 1. Fix issues manually and run again'));
445
+ console.log(c.dim(' 2. Reset retry count in the story frontmatter'));
446
+ await clearWorkflowState(sdlcRoot);
447
+ return;
448
+ }
449
+ // We can retry - reset RPIV cycle and loop back
450
+ const currentRetry = (story.frontmatter.retry_count || 0) + 1;
451
+ // Use CLI override, then story-specific, then config default
452
+ const effectiveMaxRetries = maxIterationsOverride !== undefined
453
+ ? maxIterationsOverride
454
+ : (story.frontmatter.max_retries ?? config.reviewConfig?.maxRetries ?? Infinity);
455
+ const maxRetriesDisplay = Number.isFinite(effectiveMaxRetries) ? effectiveMaxRetries : '∞';
456
+ console.log();
457
+ console.log(c.warning(`⟳ Review rejected with ${reviewResult.issues.length} issue(s) - initiating rework (attempt ${currentRetry}/${maxRetriesDisplay})`));
458
+ // Reset the RPIV cycle (this increments retry_count and resets flags)
459
+ resetRPIVCycle(story, reviewResult.feedback);
460
+ // Log what's being reset
461
+ console.log(c.dim(` → Reset plan_complete, implementation_complete, reviews_complete`));
462
+ console.log(c.dim(` → Retry count: ${currentRetry}/${maxRetriesDisplay}`));
463
+ // Regenerate actions starting from the phase that needs rework
464
+ // For now, we restart from 'plan' since that's the typical flow after research
465
+ const freshStory = parseStory(action.storyPath);
466
+ const newActions = generateFullSDLCActions(freshStory, c);
467
+ if (newActions.length > 0) {
468
+ // Replace remaining actions with the new sequence
469
+ currentActions = newActions;
470
+ currentActionIndex = 0;
471
+ retryAttempt++;
472
+ console.log(c.info(` → Restarting SDLC from ${newActions[0].type} phase`));
473
+ console.log();
474
+ continue; // Restart the loop with new actions
475
+ }
476
+ else {
477
+ // No actions to retry (shouldn't happen but handle gracefully)
478
+ console.log(c.error('Error: No actions generated for retry. Manual intervention required.'));
479
+ return;
480
+ }
481
+ }
482
+ }
483
+ // Save checkpoint after successful action
484
+ if (actionResult.success) {
485
+ completedActions.push({
486
+ type: action.type,
487
+ storyId: action.storyId,
488
+ storyPath: action.storyPath,
489
+ completedAt: new Date().toISOString(),
490
+ });
491
+ const state = {
492
+ version: '1.0',
493
+ workflowId,
494
+ timestamp: new Date().toISOString(),
495
+ currentAction: null,
496
+ completedActions,
497
+ context: {
498
+ sdlcRoot,
499
+ options: {
500
+ auto: options.auto,
501
+ dryRun: options.dryRun,
502
+ story: options.story,
503
+ fullSDLC: isFullSDLC,
504
+ },
505
+ storyContentHash: calculateStoryHash(action.storyPath),
506
+ },
507
+ };
508
+ await saveWorkflowState(state, sdlcRoot);
509
+ console.log(c.dim(` ✓ Progress saved (${completedActions.length} actions completed)`));
510
+ }
511
+ currentActionIndex++;
512
+ // Re-assess after each action in auto mode
513
+ if (options.auto) {
514
+ // For full SDLC mode, check if all phases are complete (and review passed)
515
+ if (isFullSDLC) {
516
+ // Check if we've completed all actions in our sequence
517
+ if (currentActionIndex >= currentActions.length) {
518
+ // Verify the review actually passed (reviews_complete should be true)
519
+ const finalStory = parseStory(action.storyPath);
520
+ if (finalStory.frontmatter.reviews_complete) {
521
+ console.log();
522
+ console.log(c.success('═'.repeat(50)));
523
+ console.log(c.success(`✓ Full SDLC completed successfully!`));
524
+ console.log(c.success('═'.repeat(50)));
525
+ console.log(c.dim(`Completed phases: ${currentActions.length}`));
526
+ if (retryAttempt > 0) {
527
+ console.log(c.dim(`Retry attempts: ${retryAttempt}`));
528
+ }
529
+ console.log(c.dim(`Story is now ready for PR creation.`));
530
+ await clearWorkflowState(sdlcRoot);
531
+ console.log(c.dim('Checkpoint cleared.'));
532
+ }
533
+ else {
534
+ // This shouldn't happen if our logic is correct, but handle it
535
+ console.log();
536
+ console.log(c.warning('All phases executed but reviews_complete is false.'));
537
+ console.log(c.dim('This may indicate an issue with the review process.'));
538
+ }
539
+ break;
540
+ }
541
+ }
542
+ else {
543
+ // Normal auto mode: re-assess state
544
+ const newAssessment = assessState(sdlcRoot);
545
+ if (newAssessment.recommendedActions.length === 0) {
546
+ console.log(c.success('\n✓ All actions completed!'));
547
+ await clearWorkflowState(sdlcRoot);
548
+ console.log(c.dim('Checkpoint cleared.'));
549
+ break;
550
+ }
551
+ }
552
+ }
553
+ }
554
+ }
555
+ /**
556
+ * Validate and resolve the story path for an action.
557
+ * If the path doesn't exist, attempts to find the story by ID.
558
+ *
559
+ * @returns The resolved story path, or null if story cannot be found
560
+ */
561
+ function resolveStoryPath(action, sdlcRoot) {
562
+ // Check if the current path exists
563
+ if (fs.existsSync(action.storyPath)) {
564
+ return action.storyPath;
565
+ }
566
+ // Path is stale - try to find by story ID
567
+ const story = findStoryById(sdlcRoot, action.storyId);
568
+ if (story) {
569
+ return story.path;
570
+ }
571
+ // Story not found by ID either
572
+ return null;
573
+ }
574
+ /**
575
+ * Execute a specific action
576
+ *
577
+ * @returns ActionExecutionResult with success status and optional review result
578
+ */
579
+ async function executeAction(action, sdlcRoot) {
580
+ const config = loadConfig();
581
+ const c = getThemedChalk(config);
582
+ // Validate and resolve the story path before executing
583
+ const resolvedPath = resolveStoryPath(action, sdlcRoot);
584
+ if (!resolvedPath) {
585
+ console.log(c.error(`Error: Story not found for action "${action.type}"`));
586
+ console.log(c.dim(` Story ID: ${action.storyId}`));
587
+ console.log(c.dim(` Original path: ${action.storyPath}`));
588
+ console.log(c.dim(' The story file may have been moved or deleted.'));
589
+ return { success: false };
590
+ }
591
+ // Update action path if it was stale
592
+ if (resolvedPath !== action.storyPath) {
593
+ console.log(c.warning(`Note: Story path updated (file was moved)`));
594
+ console.log(c.dim(` From: ${action.storyPath}`));
595
+ console.log(c.dim(` To: ${resolvedPath}`));
596
+ action.storyPath = resolvedPath;
597
+ }
598
+ // Store phase completion state BEFORE action execution (to detect transitions)
599
+ const storyBeforeAction = parseStory(action.storyPath);
600
+ const prevPhaseState = {
601
+ research_complete: storyBeforeAction.frontmatter.research_complete,
602
+ plan_complete: storyBeforeAction.frontmatter.plan_complete,
603
+ implementation_complete: storyBeforeAction.frontmatter.implementation_complete,
604
+ reviews_complete: storyBeforeAction.frontmatter.reviews_complete,
605
+ status: storyBeforeAction.frontmatter.status,
606
+ };
607
+ const spinner = ora(formatAction(action, true, c)).start();
608
+ const baseText = formatAction(action, true, c);
609
+ // Create agent progress callback for real-time updates
610
+ const onAgentProgress = (event) => {
611
+ switch (event.type) {
612
+ case 'session_start':
613
+ spinner.text = `${baseText} ${c.dim('(session started)')}`;
614
+ break;
615
+ case 'tool_start':
616
+ // Show which tool is being executed
617
+ const toolName = event.toolName || 'unknown';
618
+ const shortName = toolName.replace(/^(mcp__|Mcp)/, '').substring(0, 30);
619
+ spinner.text = `${baseText} ${c.dim(`→ ${shortName}`)}`;
620
+ break;
621
+ case 'tool_end':
622
+ // Keep showing the action, tool completed
623
+ spinner.text = baseText;
624
+ break;
625
+ case 'completion':
626
+ spinner.text = `${baseText} ${c.dim('(completing...)')}`;
627
+ break;
628
+ }
629
+ };
630
+ try {
631
+ // Import and run the appropriate agent
632
+ let result;
633
+ switch (action.type) {
634
+ case 'refine':
635
+ const { runRefinementAgent } = await import('../agents/refinement.js');
636
+ result = await runRefinementAgent(action.storyPath, sdlcRoot, { onProgress: onAgentProgress });
637
+ break;
638
+ case 'research':
639
+ const { runResearchAgent } = await import('../agents/research.js');
640
+ result = await runResearchAgent(action.storyPath, sdlcRoot, { onProgress: onAgentProgress });
641
+ break;
642
+ case 'plan':
643
+ const { runPlanningAgent } = await import('../agents/planning.js');
644
+ result = await runPlanningAgent(action.storyPath, sdlcRoot, { onProgress: onAgentProgress });
645
+ break;
646
+ case 'implement':
647
+ const { runImplementationAgent } = await import('../agents/implementation.js');
648
+ result = await runImplementationAgent(action.storyPath, sdlcRoot, { onProgress: onAgentProgress });
649
+ break;
650
+ case 'review':
651
+ const { runReviewAgent } = await import('../agents/review.js');
652
+ result = await runReviewAgent(action.storyPath, sdlcRoot, {
653
+ onVerificationProgress: (phase, status, message) => {
654
+ const phaseLabel = phase === 'build' ? 'Building' : 'Testing';
655
+ switch (status) {
656
+ case 'starting':
657
+ spinner.text = c.dim(`${phaseLabel}: ${message || ''}`);
658
+ break;
659
+ case 'running':
660
+ // Keep spinner spinning, optionally could show last line of output
661
+ break;
662
+ case 'passed':
663
+ spinner.text = c.success(`${phaseLabel}: passed`);
664
+ break;
665
+ case 'failed':
666
+ spinner.text = c.error(`${phaseLabel}: failed`);
667
+ break;
668
+ }
669
+ },
670
+ });
671
+ break;
672
+ case 'rework':
673
+ const { runReworkAgent } = await import('../agents/rework.js');
674
+ if (!action.context) {
675
+ throw new Error('Rework action requires context with review feedback');
676
+ }
677
+ result = await runReworkAgent(action.storyPath, sdlcRoot, action.context);
678
+ break;
679
+ case 'create_pr':
680
+ const { createPullRequest } = await import('../agents/review.js');
681
+ result = await createPullRequest(action.storyPath, sdlcRoot);
682
+ break;
683
+ case 'move_to_done':
684
+ // Move story to done folder
685
+ const { moveStory } = await import('../core/story.js');
686
+ const storyToMove = parseStory(action.storyPath);
687
+ const movedStory = moveStory(storyToMove, 'done', sdlcRoot);
688
+ result = {
689
+ success: true,
690
+ story: movedStory,
691
+ changesMade: ['Moved story to done/'],
692
+ };
693
+ break;
694
+ default:
695
+ throw new Error(`Unknown action type: ${action.type}`);
696
+ }
697
+ // Check if agent succeeded
698
+ if (result && !result.success) {
699
+ spinner.fail(c.error(`Failed: ${formatAction(action, true, c)}`));
700
+ if (result.error) {
701
+ console.error(c.error(` Error: ${result.error}`));
702
+ }
703
+ return { success: false };
704
+ }
705
+ spinner.succeed(c.success(formatAction(action, true, c)));
706
+ // Show changes made
707
+ if (result && result.changesMade.length > 0) {
708
+ for (const change of result.changesMade) {
709
+ console.log(c.dim(` → ${change}`));
710
+ }
711
+ }
712
+ // Display phase progress after successful action
713
+ if (result && result.success) {
714
+ // Use the story from result if available (handles moved files like refine)
715
+ const story = result.story || parseStory(action.storyPath);
716
+ const progress = calculatePhaseProgress(story);
717
+ // Show phase checklist
718
+ console.log(c.dim(` Progress: ${renderPhaseChecklist(story, c)}`));
719
+ // Check if a phase just completed (detect transition from false → true)
720
+ const phaseInfo = getPhaseInfo(action.type, c);
721
+ if (phaseInfo) {
722
+ let phaseJustCompleted = false;
723
+ switch (action.type) {
724
+ case 'refine':
725
+ // Refine completes when status changes from backlog to something else
726
+ phaseJustCompleted = prevPhaseState.status === 'backlog' && story.frontmatter.status !== 'backlog';
727
+ break;
728
+ case 'research':
729
+ // Research completes when research_complete transitions from false to true
730
+ phaseJustCompleted = !prevPhaseState.research_complete && story.frontmatter.research_complete;
731
+ break;
732
+ case 'plan':
733
+ // Plan completes when plan_complete transitions from false to true
734
+ phaseJustCompleted = !prevPhaseState.plan_complete && story.frontmatter.plan_complete;
735
+ break;
736
+ case 'implement':
737
+ // Implement completes when implementation_complete transitions from false to true
738
+ phaseJustCompleted = !prevPhaseState.implementation_complete && story.frontmatter.implementation_complete;
739
+ break;
740
+ case 'review':
741
+ // Review completes when reviews_complete transitions from false to true
742
+ phaseJustCompleted = !prevPhaseState.reviews_complete && story.frontmatter.reviews_complete;
743
+ break;
744
+ case 'rework':
745
+ // Rework doesn't have a specific completion flag
746
+ phaseJustCompleted = false;
747
+ break;
748
+ }
749
+ // Only show completion message if phase transitioned to complete
750
+ if (phaseJustCompleted) {
751
+ const useAscii = process.env.NO_COLOR !== undefined;
752
+ const completionSymbol = useAscii ? '[X]' : '✓';
753
+ console.log(c.phaseComplete(` ${completionSymbol} ${phaseInfo.name} phase complete`));
754
+ }
755
+ }
756
+ }
757
+ // Return review result for review actions
758
+ if (action.type === 'review' && result) {
759
+ return { success: true, reviewResult: result };
760
+ }
761
+ return { success: true };
762
+ }
763
+ catch (error) {
764
+ spinner.fail(c.error(`Failed: ${formatAction(action, true, c)}`));
765
+ console.error(error);
766
+ // Show phase checklist with error indication (if file still exists)
767
+ try {
768
+ const story = parseStory(action.storyPath);
769
+ console.log(c.dim(` Progress: ${renderPhaseChecklist(story, c)}`));
770
+ // Update story with error
771
+ story.frontmatter.last_error = error instanceof Error ? error.message : String(error);
772
+ }
773
+ catch {
774
+ // File may have been moved - skip progress display
775
+ }
776
+ // Don't throw - let the workflow continue if in auto mode
777
+ return { success: false };
778
+ }
779
+ }
780
+ /**
781
+ * Get phase information for an action type
782
+ * Returns null for non-RPIV actions (create_pr, move_to_done)
783
+ *
784
+ * @param actionType - The type of action to get phase info for
785
+ * @param colors - The theme colors object
786
+ * @returns Phase information object or null for non-RPIV actions
787
+ */
788
+ export function getPhaseInfo(actionType, colors) {
789
+ const useAscii = process.env.NO_COLOR !== undefined;
790
+ switch (actionType) {
791
+ case 'refine':
792
+ return {
793
+ name: 'Refine',
794
+ icon: '✨',
795
+ iconAscii: '[RF]', // Changed from [R] to avoid collision with Research
796
+ colorFn: colors.phaseRefine,
797
+ };
798
+ case 'research':
799
+ return {
800
+ name: 'Research',
801
+ icon: '🔍',
802
+ iconAscii: '[R]',
803
+ colorFn: colors.phaseResearch,
804
+ };
805
+ case 'plan':
806
+ return {
807
+ name: 'Plan',
808
+ icon: '📋',
809
+ iconAscii: '[P]',
810
+ colorFn: colors.phasePlan,
811
+ };
812
+ case 'implement':
813
+ return {
814
+ name: 'Implement',
815
+ icon: '🔨',
816
+ iconAscii: '[I]',
817
+ colorFn: colors.phaseImplement,
818
+ };
819
+ case 'review':
820
+ return {
821
+ name: 'Verify',
822
+ icon: '✓',
823
+ iconAscii: '[V]',
824
+ colorFn: colors.phaseVerify,
825
+ };
826
+ case 'rework':
827
+ return {
828
+ name: 'Rework',
829
+ icon: '🔄',
830
+ iconAscii: '[RW]',
831
+ colorFn: colors.warning,
832
+ };
833
+ default:
834
+ return null; // create_pr, move_to_done are not RPIV phases
835
+ }
836
+ }
837
+ /**
838
+ * Calculate phase progress for a story
839
+ *
840
+ * @param story - The story to calculate progress for
841
+ * @returns Object containing current phase, completed phases, and all phases
842
+ */
843
+ export function calculatePhaseProgress(story) {
844
+ const allPhases = ['Refine', 'Research', 'Plan', 'Implement', 'Verify'];
845
+ const completedPhases = [];
846
+ let currentPhase = 'Refine';
847
+ // Check each phase completion status
848
+ if (story.frontmatter.status !== 'backlog') {
849
+ completedPhases.push('Refine');
850
+ currentPhase = 'Research';
851
+ }
852
+ if (story.frontmatter.research_complete) {
853
+ completedPhases.push('Research');
854
+ currentPhase = 'Plan';
855
+ }
856
+ if (story.frontmatter.plan_complete) {
857
+ completedPhases.push('Plan');
858
+ currentPhase = 'Implement';
859
+ }
860
+ if (story.frontmatter.implementation_complete) {
861
+ completedPhases.push('Implement');
862
+ currentPhase = 'Verify';
863
+ }
864
+ if (story.frontmatter.reviews_complete) {
865
+ completedPhases.push('Verify');
866
+ currentPhase = 'Complete';
867
+ }
868
+ return { currentPhase, completedPhases, allPhases };
869
+ }
870
+ /**
871
+ * Render phase checklist for progress display
872
+ *
873
+ * @param story - The story to render progress for
874
+ * @param colors - The theme colors object
875
+ * @returns Formatted checklist string with symbols and colors
876
+ */
877
+ export function renderPhaseChecklist(story, colors) {
878
+ const { currentPhase, completedPhases, allPhases } = calculatePhaseProgress(story);
879
+ const useAscii = process.env.NO_COLOR !== undefined;
880
+ const symbols = {
881
+ complete: useAscii ? '[X]' : '✓',
882
+ current: useAscii ? '[>]' : '●',
883
+ pending: useAscii ? '[ ]' : '○',
884
+ arrow: useAscii ? '->' : '→',
885
+ };
886
+ const parts = allPhases.map(phase => {
887
+ if (completedPhases.includes(phase)) {
888
+ return colors.success(symbols.complete) + ' ' + colors.dim(phase);
889
+ }
890
+ else if (phase === currentPhase) {
891
+ return colors.info(symbols.current) + ' ' + colors.bold(phase);
892
+ }
893
+ else {
894
+ return colors.dim(symbols.pending + ' ' + phase);
895
+ }
896
+ });
897
+ return parts.join(colors.dim(' ' + symbols.arrow + ' '));
898
+ }
899
+ /**
900
+ * Truncate story slug if it exceeds terminal width
901
+ *
902
+ * @param text - The text to truncate
903
+ * @param maxWidth - Maximum width (defaults to terminal columns or 80)
904
+ * @returns Truncated text with ellipsis if needed
905
+ */
906
+ export function truncateForTerminal(text, maxWidth) {
907
+ // Enforce minimum 40 and maximum 1000 width to prevent memory/performance issues
908
+ const terminalWidth = Math.min(1000, Math.max(40, maxWidth || process.stdout.columns || 80));
909
+ const minWidth = 40; // Reserve space for phase indicators and verbs
910
+ if (text.length + minWidth <= terminalWidth) {
911
+ return text;
912
+ }
913
+ const availableWidth = terminalWidth - minWidth - 3; // -3 for "..."
914
+ if (availableWidth <= 0) {
915
+ // When there's no room for truncation indicator, just return what fits
916
+ return text.slice(0, 10);
917
+ }
918
+ return text.slice(0, availableWidth) + '...';
919
+ }
920
+ /**
921
+ * Sanitize story slug by removing ANSI escape codes
922
+ *
923
+ * This function prevents ANSI injection attacks by stripping escape sequences
924
+ * that could manipulate terminal output (colors, cursor movement, screen clearing, etc.)
925
+ *
926
+ * @security Prevents ANSI injection attacks through malicious story titles
927
+ * @param text - The text to sanitize
928
+ * @returns Sanitized text without ANSI codes
929
+ */
930
+ export function sanitizeStorySlug(text) {
931
+ // Remove ANSI escape codes (security: prevent ANSI injection attacks)
932
+ // Comprehensive regex that covers:
933
+ // - SGR (Select Graphic Rendition): \x1b\[[0-9;]*m
934
+ // - Cursor positioning and other CSI sequences: \x1b\[[0-9;]*[A-Za-z]
935
+ // - OSC (Operating System Command): \x1b\][^\x07]*\x07
936
+ // - Incomplete sequences: \x1b\[[^\x1b]*
937
+ return text
938
+ .replace(/\x1b\[[0-9;]*m/g, '') // SGR color codes (complete)
939
+ .replace(/\x1b\[[0-9;]*[A-Za-z]/g, '') // Other CSI sequences (cursor movement, etc.)
940
+ .replace(/\x1b\][^\x07]*\x07/g, '') // OSC sequences (complete)
941
+ .replace(/\x1b\[[^\x1b]*/g, ''); // Incomplete CSI sequences
942
+ }
943
+ /**
944
+ * Format an action for display with phase indicator
945
+ *
946
+ * @param action - The action to format
947
+ * @param includePhaseIndicator - Whether to include phase indicator brackets
948
+ * @param colors - The theme colors object (parameter renamed for clarity)
949
+ * @returns Formatted action string
950
+ */
951
+ function formatAction(action, includePhaseIndicator = false, colors) {
952
+ const actionVerbs = {
953
+ refine: 'Refine',
954
+ research: 'Research',
955
+ plan: 'Plan',
956
+ implement: 'Implement',
957
+ review: 'Review',
958
+ rework: 'Rework',
959
+ create_pr: 'Create PR for',
960
+ move_to_done: 'Move to done',
961
+ };
962
+ const storySlug = action.storyPath.split('/').pop()?.replace('.md', '') || action.storyId;
963
+ const sanitizedSlug = sanitizeStorySlug(storySlug); // Security: sanitize ANSI codes
964
+ const truncatedSlug = truncateForTerminal(sanitizedSlug);
965
+ const verb = actionVerbs[action.type];
966
+ // If no color context or phase indicator not requested, return simple format
967
+ if (!includePhaseIndicator || !colors) {
968
+ return `${verb} "${truncatedSlug}"`;
969
+ }
970
+ // Get phase info for RPIV actions
971
+ const phaseInfo = getPhaseInfo(action.type, colors);
972
+ if (!phaseInfo) {
973
+ // Non-RPIV actions (create_pr, move_to_done) don't get phase indicators
974
+ return `${verb} "${truncatedSlug}"`;
975
+ }
976
+ // Format with phase indicator
977
+ const useAscii = process.env.NO_COLOR !== undefined;
978
+ const icon = useAscii ? phaseInfo.iconAscii : phaseInfo.icon;
979
+ const phaseLabel = phaseInfo.colorFn(`[${phaseInfo.name}]`);
980
+ // Special formatting for review actions
981
+ if (action.type === 'review') {
982
+ return `${phaseLabel} ${icon} ${colors.reviewAction(verb)} "${truncatedSlug}"`;
983
+ }
984
+ return `${phaseLabel} ${icon} ${verb} "${truncatedSlug}"`;
985
+ }
986
+ /**
987
+ * Get status flags for a story (wrapper for shared utility)
988
+ * Adds dim styling and error color for backward compatibility
989
+ */
990
+ function getStoryFlags(story, c) {
991
+ const flags = getStoryFlagsUtil(story, c);
992
+ return flags ? c.dim(` ${flags}`) : '';
993
+ }
994
+ /**
995
+ * Show detailed information about a story by ID or slug
996
+ */
997
+ export async function details(idOrSlug) {
998
+ const config = loadConfig();
999
+ const sdlcRoot = getSdlcRoot();
1000
+ const c = getThemedChalk(config);
1001
+ // Check if SDLC is initialized
1002
+ if (!kanbanExists(sdlcRoot)) {
1003
+ console.log(c.warning('agentic-sdlc not initialized. Run `agentic-sdlc init` first.'));
1004
+ return;
1005
+ }
1006
+ // Validate input
1007
+ if (!idOrSlug || idOrSlug.trim() === '') {
1008
+ console.log(c.error('Error: Please provide a story ID or slug.'));
1009
+ console.log(c.dim('Usage: agentic-sdlc details <id|slug>'));
1010
+ return;
1011
+ }
1012
+ // Normalize input (case-insensitive)
1013
+ const normalizedInput = idOrSlug.toLowerCase().trim();
1014
+ // Try to find story by ID first, then by slug
1015
+ let story = findStoryById(sdlcRoot, normalizedInput);
1016
+ if (!story) {
1017
+ story = findStoryBySlug(sdlcRoot, normalizedInput);
1018
+ }
1019
+ // Handle not found
1020
+ if (!story) {
1021
+ console.log(c.error(`Error: Story not found: "${idOrSlug}"`));
1022
+ console.log();
1023
+ console.log(c.dim('Searched for:'));
1024
+ console.log(c.dim(` ID: ${normalizedInput}`));
1025
+ console.log(c.dim(` Slug: ${normalizedInput}`));
1026
+ console.log();
1027
+ console.log(c.info('Tip: Use `agentic-sdlc status` to see all available stories.'));
1028
+ return;
1029
+ }
1030
+ // Display story details
1031
+ console.log();
1032
+ console.log(c.bold('═'.repeat(60)));
1033
+ console.log(c.bold(story.frontmatter.title));
1034
+ console.log(c.bold('═'.repeat(60)));
1035
+ console.log();
1036
+ // Metadata section
1037
+ console.log(c.info('METADATA'));
1038
+ console.log(c.dim('─'.repeat(60)));
1039
+ console.log(`${c.dim('ID:')} ${story.frontmatter.id}`);
1040
+ console.log(`${c.dim('Slug:')} ${story.slug}`);
1041
+ console.log(`${c.dim('Status:')} ${formatStatus(story.frontmatter.status, c)}`);
1042
+ console.log(`${c.dim('Priority:')} ${story.frontmatter.priority}`);
1043
+ console.log(`${c.dim('Type:')} ${story.frontmatter.type}`);
1044
+ if (story.frontmatter.estimated_effort) {
1045
+ console.log(`${c.dim('Effort:')} ${story.frontmatter.estimated_effort}`);
1046
+ }
1047
+ if (story.frontmatter.assignee) {
1048
+ console.log(`${c.dim('Assignee:')} ${story.frontmatter.assignee}`);
1049
+ }
1050
+ if (story.frontmatter.labels && story.frontmatter.labels.length > 0) {
1051
+ console.log(`${c.dim('Labels:')} ${story.frontmatter.labels.join(', ')}`);
1052
+ }
1053
+ console.log(`${c.dim('Created:')} ${formatDate(story.frontmatter.created)}`);
1054
+ if (story.frontmatter.updated) {
1055
+ console.log(`${c.dim('Updated:')} ${formatDate(story.frontmatter.updated)}`);
1056
+ }
1057
+ console.log();
1058
+ // Workflow status section
1059
+ console.log(c.info('WORKFLOW STATUS'));
1060
+ console.log(c.dim('─'.repeat(60)));
1061
+ console.log(`${c.dim('Research:')} ${formatCheckbox(story.frontmatter.research_complete, c)}`);
1062
+ console.log(`${c.dim('Planning:')} ${formatCheckbox(story.frontmatter.plan_complete, c)}`);
1063
+ console.log(`${c.dim('Implementation:')} ${formatCheckbox(story.frontmatter.implementation_complete, c)}`);
1064
+ console.log(`${c.dim('Reviews:')} ${formatCheckbox(story.frontmatter.reviews_complete, c)}`);
1065
+ console.log();
1066
+ // PR information (if present)
1067
+ if (story.frontmatter.pr_url || story.frontmatter.branch) {
1068
+ console.log(c.info('PULL REQUEST'));
1069
+ console.log(c.dim('─'.repeat(60)));
1070
+ if (story.frontmatter.branch) {
1071
+ console.log(`${c.dim('Branch:')} ${story.frontmatter.branch}`);
1072
+ }
1073
+ if (story.frontmatter.pr_url) {
1074
+ console.log(`${c.dim('PR URL:')} ${story.frontmatter.pr_url}`);
1075
+ }
1076
+ console.log();
1077
+ }
1078
+ // Error information (if present)
1079
+ if (story.frontmatter.last_error) {
1080
+ console.log(c.error('LAST ERROR'));
1081
+ console.log(c.dim('─'.repeat(60)));
1082
+ console.log(c.error(story.frontmatter.last_error));
1083
+ console.log();
1084
+ }
1085
+ // Content sections
1086
+ displayContentSections(story, c);
1087
+ console.log(c.bold('═'.repeat(60)));
1088
+ console.log();
1089
+ }
1090
+ /**
1091
+ * Format status with appropriate color (wrapper for shared utility)
1092
+ */
1093
+ function formatStatus(status, c) {
1094
+ return formatStatusUtil(status, c);
1095
+ }
1096
+ /**
1097
+ * Format date for display
1098
+ */
1099
+ function formatDate(dateStr) {
1100
+ try {
1101
+ const date = new Date(dateStr);
1102
+ return date.toLocaleDateString('en-US', {
1103
+ year: 'numeric',
1104
+ month: 'short',
1105
+ day: 'numeric'
1106
+ });
1107
+ }
1108
+ catch {
1109
+ return dateStr;
1110
+ }
1111
+ }
1112
+ /**
1113
+ * Format checkbox (completed/not completed)
1114
+ */
1115
+ function formatCheckbox(completed, c) {
1116
+ return completed ? c.success('✓ Complete') : c.dim('○ Pending');
1117
+ }
1118
+ /**
1119
+ * Display content sections from the story
1120
+ */
1121
+ function displayContentSections(story, c) {
1122
+ const content = story.content;
1123
+ // Parse sections from markdown
1124
+ const sections = parseContentSections(content);
1125
+ // Display each section if it has content
1126
+ for (const section of sections) {
1127
+ if (section.content && !isEmptySection(section.content)) {
1128
+ console.log(c.info(section.title.toUpperCase()));
1129
+ console.log(c.dim('─'.repeat(60)));
1130
+ console.log(section.content);
1131
+ console.log();
1132
+ }
1133
+ }
1134
+ }
1135
+ /**
1136
+ * Parse markdown content into sections
1137
+ */
1138
+ function parseContentSections(content) {
1139
+ const sections = [];
1140
+ const lines = content.split('\n');
1141
+ let currentSection = null;
1142
+ for (const line of lines) {
1143
+ // Check if this is a section header (## Header)
1144
+ const headerMatch = line.match(/^##\s+(.+)$/);
1145
+ if (headerMatch) {
1146
+ // Save previous section if exists
1147
+ if (currentSection) {
1148
+ sections.push(currentSection);
1149
+ }
1150
+ // Start new section
1151
+ currentSection = {
1152
+ title: headerMatch[1],
1153
+ content: '',
1154
+ };
1155
+ }
1156
+ else if (currentSection) {
1157
+ // Add line to current section
1158
+ currentSection.content += line + '\n';
1159
+ }
1160
+ }
1161
+ // Don't forget the last section
1162
+ if (currentSection) {
1163
+ sections.push(currentSection);
1164
+ }
1165
+ return sections;
1166
+ }
1167
+ /**
1168
+ * Check if a section is empty (contains only placeholder comments or whitespace)
1169
+ */
1170
+ function isEmptySection(content) {
1171
+ const trimmed = content.trim();
1172
+ // Empty or only whitespace
1173
+ if (!trimmed) {
1174
+ return true;
1175
+ }
1176
+ // Only contains placeholder HTML comments
1177
+ const withoutComments = trimmed.replace(/<!--[\s\S]*?-->/g, '').trim();
1178
+ if (!withoutComments) {
1179
+ return true;
1180
+ }
1181
+ return false;
1182
+ }
1183
+ //# sourceMappingURL=commands.js.map