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.
- package/LICENSE +21 -0
- package/README.md +847 -0
- package/dist/agents/implementation.d.ts +11 -0
- package/dist/agents/implementation.d.ts.map +1 -0
- package/dist/agents/implementation.js +123 -0
- package/dist/agents/implementation.js.map +1 -0
- package/dist/agents/index.d.ts +7 -0
- package/dist/agents/index.d.ts.map +1 -0
- package/dist/agents/index.js +8 -0
- package/dist/agents/index.js.map +1 -0
- package/dist/agents/planning.d.ts +9 -0
- package/dist/agents/planning.d.ts.map +1 -0
- package/dist/agents/planning.js +84 -0
- package/dist/agents/planning.js.map +1 -0
- package/dist/agents/refinement.d.ts +10 -0
- package/dist/agents/refinement.d.ts.map +1 -0
- package/dist/agents/refinement.js +98 -0
- package/dist/agents/refinement.js.map +1 -0
- package/dist/agents/research.d.ts +16 -0
- package/dist/agents/research.d.ts.map +1 -0
- package/dist/agents/research.js +141 -0
- package/dist/agents/research.js.map +1 -0
- package/dist/agents/review.d.ts +24 -0
- package/dist/agents/review.d.ts.map +1 -0
- package/dist/agents/review.js +740 -0
- package/dist/agents/review.js.map +1 -0
- package/dist/agents/rework.d.ts +17 -0
- package/dist/agents/rework.d.ts.map +1 -0
- package/dist/agents/rework.js +139 -0
- package/dist/agents/rework.js.map +1 -0
- package/dist/agents/state-assessor.d.ts +21 -0
- package/dist/agents/state-assessor.d.ts.map +1 -0
- package/dist/agents/state-assessor.js +29 -0
- package/dist/agents/state-assessor.js.map +1 -0
- package/dist/cli/commands.d.ts +87 -0
- package/dist/cli/commands.d.ts.map +1 -0
- package/dist/cli/commands.js +1183 -0
- package/dist/cli/commands.js.map +1 -0
- package/dist/cli/formatting.d.ts +68 -0
- package/dist/cli/formatting.d.ts.map +1 -0
- package/dist/cli/formatting.js +194 -0
- package/dist/cli/formatting.js.map +1 -0
- package/dist/cli/runner.d.ts +57 -0
- package/dist/cli/runner.d.ts.map +1 -0
- package/dist/cli/runner.js +272 -0
- package/dist/cli/runner.js.map +1 -0
- package/dist/cli/story-utils.d.ts +19 -0
- package/dist/cli/story-utils.d.ts.map +1 -0
- package/dist/cli/story-utils.js +44 -0
- package/dist/cli/story-utils.js.map +1 -0
- package/dist/cli/table-renderer.d.ts +22 -0
- package/dist/cli/table-renderer.d.ts.map +1 -0
- package/dist/cli/table-renderer.js +159 -0
- package/dist/cli/table-renderer.js.map +1 -0
- package/dist/core/auth.d.ts +39 -0
- package/dist/core/auth.d.ts.map +1 -0
- package/dist/core/auth.js +128 -0
- package/dist/core/auth.js.map +1 -0
- package/dist/core/client.d.ts +73 -0
- package/dist/core/client.d.ts.map +1 -0
- package/dist/core/client.js +140 -0
- package/dist/core/client.js.map +1 -0
- package/dist/core/config.d.ts +48 -0
- package/dist/core/config.d.ts.map +1 -0
- package/dist/core/config.js +330 -0
- package/dist/core/config.js.map +1 -0
- package/dist/core/kanban.d.ts +34 -0
- package/dist/core/kanban.d.ts.map +1 -0
- package/dist/core/kanban.js +253 -0
- package/dist/core/kanban.js.map +1 -0
- package/dist/core/story.d.ts +91 -0
- package/dist/core/story.d.ts.map +1 -0
- package/dist/core/story.js +349 -0
- package/dist/core/story.js.map +1 -0
- package/dist/core/theme.d.ts +17 -0
- package/dist/core/theme.d.ts.map +1 -0
- package/dist/core/theme.js +136 -0
- package/dist/core/theme.js.map +1 -0
- package/dist/core/workflow-state.d.ts +56 -0
- package/dist/core/workflow-state.d.ts.map +1 -0
- package/dist/core/workflow-state.js +162 -0
- package/dist/core/workflow-state.js.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +103 -0
- package/dist/index.js.map +1 -0
- package/dist/types/index.d.ts +228 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/index.js +38 -0
- package/dist/types/index.js.map +1 -0
- package/dist/types/workflow-state.d.ts +54 -0
- package/dist/types/workflow-state.d.ts.map +1 -0
- package/dist/types/workflow-state.js +5 -0
- package/dist/types/workflow-state.js.map +1 -0
- package/package.json +71 -0
- 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
|