ai-sdlc 0.2.0-alpha.5 → 0.2.0-alpha.51
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/README.md +53 -1058
- package/dist/agents/implementation.d.ts +6 -0
- package/dist/agents/implementation.d.ts.map +1 -1
- package/dist/agents/implementation.js +151 -13
- package/dist/agents/implementation.js.map +1 -1
- package/dist/agents/index.d.ts +2 -0
- package/dist/agents/index.d.ts.map +1 -1
- package/dist/agents/index.js +2 -0
- package/dist/agents/index.js.map +1 -1
- package/dist/agents/orchestrator.d.ts +61 -0
- package/dist/agents/orchestrator.d.ts.map +1 -0
- package/dist/agents/orchestrator.js +443 -0
- package/dist/agents/orchestrator.js.map +1 -0
- package/dist/agents/planning.d.ts +1 -1
- package/dist/agents/planning.d.ts.map +1 -1
- package/dist/agents/planning.js +55 -4
- package/dist/agents/planning.js.map +1 -1
- package/dist/agents/refinement.d.ts.map +1 -1
- package/dist/agents/refinement.js +22 -3
- package/dist/agents/refinement.js.map +1 -1
- package/dist/agents/research.d.ts +85 -1
- package/dist/agents/research.d.ts.map +1 -1
- package/dist/agents/research.js +506 -16
- package/dist/agents/research.js.map +1 -1
- package/dist/agents/review.d.ts +103 -2
- package/dist/agents/review.d.ts.map +1 -1
- package/dist/agents/review.js +775 -93
- package/dist/agents/review.js.map +1 -1
- package/dist/agents/rework.d.ts.map +1 -1
- package/dist/agents/rework.js +22 -3
- package/dist/agents/rework.js.map +1 -1
- package/dist/agents/single-task.d.ts +41 -0
- package/dist/agents/single-task.d.ts.map +1 -0
- package/dist/agents/single-task.js +357 -0
- package/dist/agents/single-task.js.map +1 -0
- package/dist/agents/state-assessor.d.ts +3 -3
- package/dist/agents/state-assessor.d.ts.map +1 -1
- package/dist/agents/state-assessor.js +6 -6
- package/dist/agents/state-assessor.js.map +1 -1
- package/dist/agents/test-pattern-detector.d.ts +49 -0
- package/dist/agents/test-pattern-detector.d.ts.map +1 -0
- package/dist/agents/test-pattern-detector.js +273 -0
- package/dist/agents/test-pattern-detector.js.map +1 -0
- package/dist/agents/verification.d.ts +11 -0
- package/dist/agents/verification.d.ts.map +1 -1
- package/dist/agents/verification.js +97 -12
- package/dist/agents/verification.js.map +1 -1
- package/dist/cli/commands/migrate.js +1 -1
- package/dist/cli/commands/migrate.js.map +1 -1
- package/dist/cli/commands.d.ts +65 -3
- package/dist/cli/commands.d.ts.map +1 -1
- package/dist/cli/commands.js +1108 -204
- package/dist/cli/commands.js.map +1 -1
- package/dist/cli/daemon.d.ts.map +1 -1
- package/dist/cli/daemon.js +20 -3
- package/dist/cli/daemon.js.map +1 -1
- package/dist/cli/runner.d.ts.map +1 -1
- package/dist/cli/runner.js +19 -11
- package/dist/cli/runner.js.map +1 -1
- package/dist/core/auth.d.ts +43 -0
- package/dist/core/auth.d.ts.map +1 -1
- package/dist/core/auth.js +105 -1
- package/dist/core/auth.js.map +1 -1
- package/dist/core/client.d.ts +6 -0
- package/dist/core/client.d.ts.map +1 -1
- package/dist/core/client.js +57 -3
- package/dist/core/client.js.map +1 -1
- package/dist/core/config.d.ts +24 -1
- package/dist/core/config.d.ts.map +1 -1
- package/dist/core/config.js +100 -3
- package/dist/core/config.js.map +1 -1
- package/dist/core/conflict-detector.d.ts +108 -0
- package/dist/core/conflict-detector.d.ts.map +1 -0
- package/dist/core/conflict-detector.js +413 -0
- package/dist/core/conflict-detector.js.map +1 -0
- package/dist/core/git-utils.d.ts +28 -0
- package/dist/core/git-utils.d.ts.map +1 -0
- package/dist/core/git-utils.js +146 -0
- package/dist/core/git-utils.js.map +1 -0
- package/dist/core/index.d.ts +19 -0
- package/dist/core/index.d.ts.map +1 -0
- package/dist/core/index.js +19 -0
- package/dist/core/index.js.map +1 -0
- package/dist/core/kanban.d.ts +1 -1
- package/dist/core/kanban.d.ts.map +1 -1
- package/dist/core/kanban.js +7 -6
- package/dist/core/kanban.js.map +1 -1
- package/dist/core/llm-utils.d.ts +103 -0
- package/dist/core/llm-utils.d.ts.map +1 -0
- package/dist/core/llm-utils.js +368 -0
- package/dist/core/llm-utils.js.map +1 -0
- package/dist/core/logger.d.ts +92 -0
- package/dist/core/logger.d.ts.map +1 -0
- package/dist/core/logger.js +221 -0
- package/dist/core/logger.js.map +1 -0
- package/dist/core/story-logger.d.ts +102 -0
- package/dist/core/story-logger.d.ts.map +1 -0
- package/dist/core/story-logger.js +265 -0
- package/dist/core/story-logger.js.map +1 -0
- package/dist/core/story.d.ts +89 -20
- package/dist/core/story.d.ts.map +1 -1
- package/dist/core/story.js +300 -52
- package/dist/core/story.js.map +1 -1
- package/dist/core/task-parser.d.ts +59 -0
- package/dist/core/task-parser.d.ts.map +1 -0
- package/dist/core/task-parser.js +235 -0
- package/dist/core/task-parser.js.map +1 -0
- package/dist/core/task-progress.d.ts +92 -0
- package/dist/core/task-progress.d.ts.map +1 -0
- package/dist/core/task-progress.js +280 -0
- package/dist/core/task-progress.js.map +1 -0
- package/dist/core/workflow-state.d.ts +45 -6
- package/dist/core/workflow-state.d.ts.map +1 -1
- package/dist/core/workflow-state.js +201 -12
- package/dist/core/workflow-state.js.map +1 -1
- package/dist/core/worktree.d.ts +77 -0
- package/dist/core/worktree.d.ts.map +1 -0
- package/dist/core/worktree.js +246 -0
- package/dist/core/worktree.js.map +1 -0
- package/dist/index.js +135 -5
- package/dist/index.js.map +1 -1
- package/dist/services/error-classifier.d.ts +119 -0
- package/dist/services/error-classifier.d.ts.map +1 -0
- package/dist/services/error-classifier.js +182 -0
- package/dist/services/error-classifier.js.map +1 -0
- package/dist/types/index.d.ts +362 -1
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/index.js +1 -0
- package/dist/types/index.js.map +1 -1
- package/package.json +4 -1
- package/templates/story.md +5 -0
|
@@ -0,0 +1,280 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Task progress tracking for resumable implementations
|
|
3
|
+
*
|
|
4
|
+
* Persists task-level progress in story files as markdown tables,
|
|
5
|
+
* enabling orchestrator to resume from last completed task after interruptions.
|
|
6
|
+
*/
|
|
7
|
+
import fs from 'fs';
|
|
8
|
+
import path from 'path';
|
|
9
|
+
import writeFileAtomic from 'write-file-atomic';
|
|
10
|
+
const TASK_PROGRESS_SECTION = '## Task Progress';
|
|
11
|
+
/**
|
|
12
|
+
* Parse progress table from markdown content
|
|
13
|
+
*
|
|
14
|
+
* Expected format:
|
|
15
|
+
* ## Task Progress
|
|
16
|
+
*
|
|
17
|
+
* | Task | Status | Started | Completed |
|
|
18
|
+
* |------|--------|---------|-----------|
|
|
19
|
+
* | T1 | completed | 2026-01-16T10:00:00Z | 2026-01-16T10:05:00Z |
|
|
20
|
+
* | T2 | in_progress | 2026-01-16T10:05:30Z | - |
|
|
21
|
+
*
|
|
22
|
+
* @param content - Story file markdown content
|
|
23
|
+
* @returns Array of TaskProgress objects (empty if section missing or corrupted)
|
|
24
|
+
*/
|
|
25
|
+
export function parseProgressTable(content) {
|
|
26
|
+
const sectionIndex = content.indexOf(TASK_PROGRESS_SECTION);
|
|
27
|
+
if (sectionIndex === -1) {
|
|
28
|
+
return [];
|
|
29
|
+
}
|
|
30
|
+
// Find the table rows (skip header and separator)
|
|
31
|
+
const afterSection = content.slice(sectionIndex);
|
|
32
|
+
const lines = afterSection.split('\n');
|
|
33
|
+
const taskProgress = [];
|
|
34
|
+
let foundTable = false;
|
|
35
|
+
for (const line of lines) {
|
|
36
|
+
const trimmed = line.trim();
|
|
37
|
+
// Skip section header, empty lines, and markdown table separator
|
|
38
|
+
if (!trimmed || trimmed.startsWith('#') || trimmed.match(/^\|[\s-:|]+\|$/)) {
|
|
39
|
+
continue;
|
|
40
|
+
}
|
|
41
|
+
// Stop at next section
|
|
42
|
+
if (trimmed.startsWith('##') && !trimmed.includes(TASK_PROGRESS_SECTION)) {
|
|
43
|
+
break;
|
|
44
|
+
}
|
|
45
|
+
// Parse table row
|
|
46
|
+
if (trimmed.startsWith('|')) {
|
|
47
|
+
foundTable = true;
|
|
48
|
+
const cells = trimmed
|
|
49
|
+
.split('|')
|
|
50
|
+
.map(cell => cell.trim())
|
|
51
|
+
.filter(cell => cell.length > 0);
|
|
52
|
+
// Skip header row
|
|
53
|
+
if (cells[0] === 'Task' || cells[1] === 'Status') {
|
|
54
|
+
continue;
|
|
55
|
+
}
|
|
56
|
+
// Validate we have at least 4 columns
|
|
57
|
+
if (cells.length < 4) {
|
|
58
|
+
console.warn(`[task-progress] Skipping malformed table row: ${trimmed}`);
|
|
59
|
+
continue;
|
|
60
|
+
}
|
|
61
|
+
const [taskId, statusStr, startedStr, completedStr] = cells;
|
|
62
|
+
// Validate status
|
|
63
|
+
const validStatuses = ['pending', 'in_progress', 'completed', 'failed'];
|
|
64
|
+
if (!validStatuses.includes(statusStr)) {
|
|
65
|
+
console.warn(`[task-progress] Invalid status '${statusStr}' for task ${taskId}, skipping row`);
|
|
66
|
+
continue;
|
|
67
|
+
}
|
|
68
|
+
const progress = {
|
|
69
|
+
taskId,
|
|
70
|
+
status: statusStr,
|
|
71
|
+
};
|
|
72
|
+
// Parse timestamps (handle "-" as missing)
|
|
73
|
+
if (startedStr && startedStr !== '-') {
|
|
74
|
+
progress.startedAt = startedStr;
|
|
75
|
+
}
|
|
76
|
+
if (completedStr && completedStr !== '-') {
|
|
77
|
+
progress.completedAt = completedStr;
|
|
78
|
+
}
|
|
79
|
+
// Handle error column if present (5th column)
|
|
80
|
+
if (cells.length >= 5 && cells[4] && cells[4] !== '-') {
|
|
81
|
+
progress.error = cells[4];
|
|
82
|
+
}
|
|
83
|
+
taskProgress.push(progress);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
if (foundTable && taskProgress.length === 0) {
|
|
87
|
+
console.warn('[task-progress] Found progress table but no valid task rows, table may be corrupted');
|
|
88
|
+
}
|
|
89
|
+
return taskProgress;
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Generate markdown table from TaskProgress array
|
|
93
|
+
*
|
|
94
|
+
* @param progress - Array of task progress entries
|
|
95
|
+
* @returns Markdown table string
|
|
96
|
+
*/
|
|
97
|
+
export function generateProgressTable(progress) {
|
|
98
|
+
const rows = [];
|
|
99
|
+
// Header
|
|
100
|
+
rows.push('| Task | Status | Started | Completed |');
|
|
101
|
+
rows.push('|------|--------|---------|-----------|');
|
|
102
|
+
// Task rows
|
|
103
|
+
for (const task of progress) {
|
|
104
|
+
const started = task.startedAt || '-';
|
|
105
|
+
const completed = task.completedAt || '-';
|
|
106
|
+
rows.push(`| ${task.taskId} | ${task.status} | ${started} | ${completed} |`);
|
|
107
|
+
}
|
|
108
|
+
return rows.join('\n');
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* Read story file content
|
|
112
|
+
*
|
|
113
|
+
* @param storyPath - Absolute path to story.md file
|
|
114
|
+
* @returns Story file content
|
|
115
|
+
* @throws Error if file doesn't exist or can't be read
|
|
116
|
+
*/
|
|
117
|
+
export async function readStoryFile(storyPath) {
|
|
118
|
+
try {
|
|
119
|
+
return await fs.promises.readFile(storyPath, 'utf-8');
|
|
120
|
+
}
|
|
121
|
+
catch (error) {
|
|
122
|
+
if (error.code === 'ENOENT') {
|
|
123
|
+
throw new Error(`Story file not found: ${path.basename(storyPath)}`);
|
|
124
|
+
}
|
|
125
|
+
if (error.code === 'EACCES' || error.code === 'EPERM') {
|
|
126
|
+
throw new Error(`Permission denied reading story file: ${path.basename(storyPath)}`);
|
|
127
|
+
}
|
|
128
|
+
throw new Error(`Failed to read story file: ${error.message}`);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
/**
|
|
132
|
+
* Write story file content atomically with retry logic
|
|
133
|
+
*
|
|
134
|
+
* @param storyPath - Absolute path to story.md file
|
|
135
|
+
* @param content - New story content
|
|
136
|
+
* @throws Error after all retries exhausted
|
|
137
|
+
*/
|
|
138
|
+
export async function writeStoryFile(storyPath, content) {
|
|
139
|
+
const maxRetries = 3;
|
|
140
|
+
const retryDelays = [100, 200, 400]; // Exponential backoff
|
|
141
|
+
// Ensure parent directory exists first (outside retry loop)
|
|
142
|
+
const storyDir = path.dirname(storyPath);
|
|
143
|
+
await fs.promises.mkdir(storyDir, { recursive: true });
|
|
144
|
+
for (let attempt = 0; attempt < maxRetries; attempt++) {
|
|
145
|
+
try {
|
|
146
|
+
// Atomic write
|
|
147
|
+
await writeFileAtomic(storyPath, content, { encoding: 'utf-8' });
|
|
148
|
+
return; // Success
|
|
149
|
+
}
|
|
150
|
+
catch (error) {
|
|
151
|
+
const isLastAttempt = attempt === maxRetries - 1;
|
|
152
|
+
// Don't retry on permission errors - throw immediately
|
|
153
|
+
if (error.code === 'EACCES' || error.code === 'EPERM') {
|
|
154
|
+
throw new Error(`Permission denied writing story file: ${path.basename(storyPath)}`);
|
|
155
|
+
}
|
|
156
|
+
if (isLastAttempt) {
|
|
157
|
+
throw new Error(`Failed to write story file after ${maxRetries} attempts`);
|
|
158
|
+
}
|
|
159
|
+
// Wait before retry
|
|
160
|
+
await new Promise(resolve => setTimeout(resolve, retryDelays[attempt]));
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
/**
|
|
165
|
+
* Get all task progress entries from story file
|
|
166
|
+
*
|
|
167
|
+
* @param storyPath - Absolute path to story.md file
|
|
168
|
+
* @returns Array of TaskProgress objects (empty if section missing)
|
|
169
|
+
*/
|
|
170
|
+
export async function getTaskProgress(storyPath) {
|
|
171
|
+
const content = await readStoryFile(storyPath);
|
|
172
|
+
return parseProgressTable(content);
|
|
173
|
+
}
|
|
174
|
+
/**
|
|
175
|
+
* Initialize progress tracking for a list of tasks
|
|
176
|
+
*
|
|
177
|
+
* Creates "## Task Progress" section with all tasks in 'pending' status.
|
|
178
|
+
* If section already exists, logs warning and skips initialization.
|
|
179
|
+
*
|
|
180
|
+
* @param storyPath - Absolute path to story.md file
|
|
181
|
+
* @param taskIds - Array of task IDs (e.g., ['T1', 'T2', 'T3'])
|
|
182
|
+
*/
|
|
183
|
+
export async function initializeTaskProgress(storyPath, taskIds) {
|
|
184
|
+
const content = await readStoryFile(storyPath);
|
|
185
|
+
// Check if progress section already exists
|
|
186
|
+
if (content.includes(TASK_PROGRESS_SECTION)) {
|
|
187
|
+
console.warn('[task-progress] Progress section already exists, skipping initialization');
|
|
188
|
+
return;
|
|
189
|
+
}
|
|
190
|
+
// Create initial progress entries (all pending)
|
|
191
|
+
const progress = taskIds.map(taskId => ({
|
|
192
|
+
taskId,
|
|
193
|
+
status: 'pending',
|
|
194
|
+
}));
|
|
195
|
+
const progressTable = generateProgressTable(progress);
|
|
196
|
+
const progressSection = `\n${TASK_PROGRESS_SECTION}\n\n${progressTable}\n`;
|
|
197
|
+
// Append to end of file
|
|
198
|
+
const newContent = content.trimEnd() + '\n' + progressSection;
|
|
199
|
+
await writeStoryFile(storyPath, newContent);
|
|
200
|
+
}
|
|
201
|
+
/**
|
|
202
|
+
* Update a specific task's status and timestamps
|
|
203
|
+
*
|
|
204
|
+
* Modifies the task row in the progress table and writes back to disk atomically.
|
|
205
|
+
* Sets timestamps based on status transitions:
|
|
206
|
+
* - 'in_progress': Sets startedAt (if not already set)
|
|
207
|
+
* - 'completed' or 'failed': Sets completedAt
|
|
208
|
+
*
|
|
209
|
+
* @param storyPath - Absolute path to story.md file
|
|
210
|
+
* @param taskId - Task ID to update (e.g., 'T1')
|
|
211
|
+
* @param status - New status
|
|
212
|
+
* @param error - Optional error message (only used with 'failed' status)
|
|
213
|
+
* @throws Error if task not found in progress table
|
|
214
|
+
*/
|
|
215
|
+
export async function updateTaskProgress(storyPath, taskId, status, error) {
|
|
216
|
+
const content = await readStoryFile(storyPath);
|
|
217
|
+
const progress = parseProgressTable(content);
|
|
218
|
+
// Find task to update
|
|
219
|
+
const taskIndex = progress.findIndex(t => t.taskId === taskId);
|
|
220
|
+
if (taskIndex === -1) {
|
|
221
|
+
throw new Error(`Task ${taskId} not found in progress table`);
|
|
222
|
+
}
|
|
223
|
+
const task = progress[taskIndex];
|
|
224
|
+
const now = new Date().toISOString();
|
|
225
|
+
// Update status
|
|
226
|
+
task.status = status;
|
|
227
|
+
// Set timestamps based on status transition
|
|
228
|
+
if (status === 'in_progress' && !task.startedAt) {
|
|
229
|
+
task.startedAt = now;
|
|
230
|
+
}
|
|
231
|
+
if (status === 'completed' || status === 'failed') {
|
|
232
|
+
task.completedAt = now;
|
|
233
|
+
}
|
|
234
|
+
// Store error message if provided
|
|
235
|
+
if (error) {
|
|
236
|
+
task.error = error;
|
|
237
|
+
}
|
|
238
|
+
// Regenerate table
|
|
239
|
+
const newTable = generateProgressTable(progress);
|
|
240
|
+
// Replace progress section in content
|
|
241
|
+
const sectionIndex = content.indexOf(TASK_PROGRESS_SECTION);
|
|
242
|
+
if (sectionIndex === -1) {
|
|
243
|
+
throw new Error('Progress section disappeared during update, this should not happen');
|
|
244
|
+
}
|
|
245
|
+
// Find next section or end of file
|
|
246
|
+
const afterSection = content.slice(sectionIndex + TASK_PROGRESS_SECTION.length);
|
|
247
|
+
const nextSectionMatch = afterSection.match(/\n## [^#]/);
|
|
248
|
+
const nextSectionOffset = nextSectionMatch
|
|
249
|
+
? sectionIndex + TASK_PROGRESS_SECTION.length + nextSectionMatch.index
|
|
250
|
+
: content.length;
|
|
251
|
+
// Rebuild content
|
|
252
|
+
const beforeSection = content.slice(0, sectionIndex);
|
|
253
|
+
const afterNextSection = content.slice(nextSectionOffset);
|
|
254
|
+
const newContent = `${beforeSection}${TASK_PROGRESS_SECTION}\n\n${newTable}\n${afterNextSection}`;
|
|
255
|
+
await writeStoryFile(storyPath, newContent);
|
|
256
|
+
}
|
|
257
|
+
/**
|
|
258
|
+
* Get list of task IDs with status 'pending'
|
|
259
|
+
*
|
|
260
|
+
* @param storyPath - Absolute path to story.md file
|
|
261
|
+
* @returns Array of pending task IDs
|
|
262
|
+
*/
|
|
263
|
+
export async function getPendingTasks(storyPath) {
|
|
264
|
+
const progress = await getTaskProgress(storyPath);
|
|
265
|
+
return progress
|
|
266
|
+
.filter(task => task.status === 'pending')
|
|
267
|
+
.map(task => task.taskId);
|
|
268
|
+
}
|
|
269
|
+
/**
|
|
270
|
+
* Get the task ID currently in 'in_progress' status
|
|
271
|
+
*
|
|
272
|
+
* @param storyPath - Absolute path to story.md file
|
|
273
|
+
* @returns Task ID or null if no task is in progress
|
|
274
|
+
*/
|
|
275
|
+
export async function getCurrentTask(storyPath) {
|
|
276
|
+
const progress = await getTaskProgress(storyPath);
|
|
277
|
+
const currentTask = progress.find(task => task.status === 'in_progress');
|
|
278
|
+
return currentTask ? currentTask.taskId : null;
|
|
279
|
+
}
|
|
280
|
+
//# sourceMappingURL=task-progress.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"task-progress.js","sourceRoot":"","sources":["../../src/core/task-progress.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,EAAE,MAAM,IAAI,CAAC;AACpB,OAAO,IAAI,MAAM,MAAM,CAAC;AACxB,OAAO,eAAe,MAAM,mBAAmB,CAAC;AAGhD,MAAM,qBAAqB,GAAG,kBAAkB,CAAC;AAEjD;;;;;;;;;;;;;GAaG;AACH,MAAM,UAAU,kBAAkB,CAAC,OAAe;IAChD,MAAM,YAAY,GAAG,OAAO,CAAC,OAAO,CAAC,qBAAqB,CAAC,CAAC;IAC5D,IAAI,YAAY,KAAK,CAAC,CAAC,EAAE,CAAC;QACxB,OAAO,EAAE,CAAC;IACZ,CAAC;IAED,kDAAkD;IAClD,MAAM,YAAY,GAAG,OAAO,CAAC,KAAK,CAAC,YAAY,CAAC,CAAC;IACjD,MAAM,KAAK,GAAG,YAAY,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;IAEvC,MAAM,YAAY,GAAmB,EAAE,CAAC;IACxC,IAAI,UAAU,GAAG,KAAK,CAAC;IAEvB,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE,CAAC;QACzB,MAAM,OAAO,GAAG,IAAI,CAAC,IAAI,EAAE,CAAC;QAE5B,iEAAiE;QACjE,IAAI,CAAC,OAAO,IAAI,OAAO,CAAC,UAAU,CAAC,GAAG,CAAC,IAAI,OAAO,CAAC,KAAK,CAAC,gBAAgB,CAAC,EAAE,CAAC;YAC3E,SAAS;QACX,CAAC;QAED,uBAAuB;QACvB,IAAI,OAAO,CAAC,UAAU,CAAC,IAAI,CAAC,IAAI,CAAC,OAAO,CAAC,QAAQ,CAAC,qBAAqB,CAAC,EAAE,CAAC;YACzE,MAAM;QACR,CAAC;QAED,kBAAkB;QAClB,IAAI,OAAO,CAAC,UAAU,CAAC,GAAG,CAAC,EAAE,CAAC;YAC5B,UAAU,GAAG,IAAI,CAAC;YAClB,MAAM,KAAK,GAAG,OAAO;iBAClB,KAAK,CAAC,GAAG,CAAC;iBACV,GAAG,CAAC,IAAI,CAAC,EAAE,CAAC,IAAI,CAAC,IAAI,EAAE,CAAC;iBACxB,MAAM,CAAC,IAAI,CAAC,EAAE,CAAC,IAAI,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC;YAEnC,kBAAkB;YAClB,IAAI,KAAK,CAAC,CAAC,CAAC,KAAK,MAAM,IAAI,KAAK,CAAC,CAAC,CAAC,KAAK,QAAQ,EAAE,CAAC;gBACjD,SAAS;YACX,CAAC;YAED,sCAAsC;YACtC,IAAI,KAAK,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;gBACrB,OAAO,CAAC,IAAI,CAAC,iDAAiD,OAAO,EAAE,CAAC,CAAC;gBACzE,SAAS;YACX,CAAC;YAED,MAAM,CAAC,MAAM,EAAE,SAAS,EAAE,UAAU,EAAE,YAAY,CAAC,GAAG,KAAK,CAAC;YAE5D,kBAAkB;YAClB,MAAM,aAAa,GAAiB,CAAC,SAAS,EAAE,aAAa,EAAE,WAAW,EAAE,QAAQ,CAAC,CAAC;YACtF,IAAI,CAAC,aAAa,CAAC,QAAQ,CAAC,SAAuB,CAAC,EAAE,CAAC;gBACrD,OAAO,CAAC,IAAI,CAAC,mCAAmC,SAAS,cAAc,MAAM,gBAAgB,CAAC,CAAC;gBAC/F,SAAS;YACX,CAAC;YAED,MAAM,QAAQ,GAAiB;gBAC7B,MAAM;gBACN,MAAM,EAAE,SAAuB;aAChC,CAAC;YAEF,2CAA2C;YAC3C,IAAI,UAAU,IAAI,UAAU,KAAK,GAAG,EAAE,CAAC;gBACrC,QAAQ,CAAC,SAAS,GAAG,UAAU,CAAC;YAClC,CAAC;YACD,IAAI,YAAY,IAAI,YAAY,KAAK,GAAG,EAAE,CAAC;gBACzC,QAAQ,CAAC,WAAW,GAAG,YAAY,CAAC;YACtC,CAAC;YAED,8CAA8C;YAC9C,IAAI,KAAK,CAAC,MAAM,IAAI,CAAC,IAAI,KAAK,CAAC,CAAC,CAAC,IAAI,KAAK,CAAC,CAAC,CAAC,KAAK,GAAG,EAAE,CAAC;gBACtD,QAAQ,CAAC,KAAK,GAAG,KAAK,CAAC,CAAC,CAAC,CAAC;YAC5B,CAAC;YAED,YAAY,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;QAC9B,CAAC;IACH,CAAC;IAED,IAAI,UAAU,IAAI,YAAY,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QAC5C,OAAO,CAAC,IAAI,CAAC,qFAAqF,CAAC,CAAC;IACtG,CAAC;IAED,OAAO,YAAY,CAAC;AACtB,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,qBAAqB,CAAC,QAAwB;IAC5D,MAAM,IAAI,GAAa,EAAE,CAAC;IAE1B,SAAS;IACT,IAAI,CAAC,IAAI,CAAC,yCAAyC,CAAC,CAAC;IACrD,IAAI,CAAC,IAAI,CAAC,yCAAyC,CAAC,CAAC;IAErD,YAAY;IACZ,KAAK,MAAM,IAAI,IAAI,QAAQ,EAAE,CAAC;QAC5B,MAAM,OAAO,GAAG,IAAI,CAAC,SAAS,IAAI,GAAG,CAAC;QACtC,MAAM,SAAS,GAAG,IAAI,CAAC,WAAW,IAAI,GAAG,CAAC;QAC1C,IAAI,CAAC,IAAI,CAAC,KAAK,IAAI,CAAC,MAAM,MAAM,IAAI,CAAC,MAAM,MAAM,OAAO,MAAM,SAAS,IAAI,CAAC,CAAC;IAC/E,CAAC;IAED,OAAO,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;AACzB,CAAC;AAED;;;;;;GAMG;AACH,MAAM,CAAC,KAAK,UAAU,aAAa,CAAC,SAAiB;IACnD,IAAI,CAAC;QACH,OAAO,MAAM,EAAE,CAAC,QAAQ,CAAC,QAAQ,CAAC,SAAS,EAAE,OAAO,CAAC,CAAC;IACxD,CAAC;IAAC,OAAO,KAAU,EAAE,CAAC;QACpB,IAAI,KAAK,CAAC,IAAI,KAAK,QAAQ,EAAE,CAAC;YAC5B,MAAM,IAAI,KAAK,CAAC,yBAAyB,IAAI,CAAC,QAAQ,CAAC,SAAS,CAAC,EAAE,CAAC,CAAC;QACvE,CAAC;QACD,IAAI,KAAK,CAAC,IAAI,KAAK,QAAQ,IAAI,KAAK,CAAC,IAAI,KAAK,OAAO,EAAE,CAAC;YACtD,MAAM,IAAI,KAAK,CAAC,yCAAyC,IAAI,CAAC,QAAQ,CAAC,SAAS,CAAC,EAAE,CAAC,CAAC;QACvF,CAAC;QACD,MAAM,IAAI,KAAK,CAAC,8BAA8B,KAAK,CAAC,OAAO,EAAE,CAAC,CAAC;IACjE,CAAC;AACH,CAAC;AAED;;;;;;GAMG;AACH,MAAM,CAAC,KAAK,UAAU,cAAc,CAAC,SAAiB,EAAE,OAAe;IACrE,MAAM,UAAU,GAAG,CAAC,CAAC;IACrB,MAAM,WAAW,GAAG,CAAC,GAAG,EAAE,GAAG,EAAE,GAAG,CAAC,CAAC,CAAC,sBAAsB;IAE3D,4DAA4D;IAC5D,MAAM,QAAQ,GAAG,IAAI,CAAC,OAAO,CAAC,SAAS,CAAC,CAAC;IACzC,MAAM,EAAE,CAAC,QAAQ,CAAC,KAAK,CAAC,QAAQ,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;IAEvD,KAAK,IAAI,OAAO,GAAG,CAAC,EAAE,OAAO,GAAG,UAAU,EAAE,OAAO,EAAE,EAAE,CAAC;QACtD,IAAI,CAAC;YACH,eAAe;YACf,MAAM,eAAe,CAAC,SAAS,EAAE,OAAO,EAAE,EAAE,QAAQ,EAAE,OAAO,EAAE,CAAC,CAAC;YACjE,OAAO,CAAC,UAAU;QACpB,CAAC;QAAC,OAAO,KAAU,EAAE,CAAC;YACpB,MAAM,aAAa,GAAG,OAAO,KAAK,UAAU,GAAG,CAAC,CAAC;YAEjD,uDAAuD;YACvD,IAAI,KAAK,CAAC,IAAI,KAAK,QAAQ,IAAI,KAAK,CAAC,IAAI,KAAK,OAAO,EAAE,CAAC;gBACtD,MAAM,IAAI,KAAK,CAAC,yCAAyC,IAAI,CAAC,QAAQ,CAAC,SAAS,CAAC,EAAE,CAAC,CAAC;YACvF,CAAC;YAED,IAAI,aAAa,EAAE,CAAC;gBAClB,MAAM,IAAI,KAAK,CAAC,oCAAoC,UAAU,WAAW,CAAC,CAAC;YAC7E,CAAC;YAED,oBAAoB;YACpB,MAAM,IAAI,OAAO,CAAC,OAAO,CAAC,EAAE,CAAC,UAAU,CAAC,OAAO,EAAE,WAAW,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC;QAC1E,CAAC;IACH,CAAC;AACH,CAAC;AAED;;;;;GAKG;AACH,MAAM,CAAC,KAAK,UAAU,eAAe,CAAC,SAAiB;IACrD,MAAM,OAAO,GAAG,MAAM,aAAa,CAAC,SAAS,CAAC,CAAC;IAC/C,OAAO,kBAAkB,CAAC,OAAO,CAAC,CAAC;AACrC,CAAC;AAED;;;;;;;;GAQG;AACH,MAAM,CAAC,KAAK,UAAU,sBAAsB,CAC1C,SAAiB,EACjB,OAAiB;IAEjB,MAAM,OAAO,GAAG,MAAM,aAAa,CAAC,SAAS,CAAC,CAAC;IAE/C,2CAA2C;IAC3C,IAAI,OAAO,CAAC,QAAQ,CAAC,qBAAqB,CAAC,EAAE,CAAC;QAC5C,OAAO,CAAC,IAAI,CAAC,0EAA0E,CAAC,CAAC;QACzF,OAAO;IACT,CAAC;IAED,gDAAgD;IAChD,MAAM,QAAQ,GAAmB,OAAO,CAAC,GAAG,CAAC,MAAM,CAAC,EAAE,CAAC,CAAC;QACtD,MAAM;QACN,MAAM,EAAE,SAAuB;KAChC,CAAC,CAAC,CAAC;IAEJ,MAAM,aAAa,GAAG,qBAAqB,CAAC,QAAQ,CAAC,CAAC;IACtD,MAAM,eAAe,GAAG,KAAK,qBAAqB,OAAO,aAAa,IAAI,CAAC;IAE3E,wBAAwB;IACxB,MAAM,UAAU,GAAG,OAAO,CAAC,OAAO,EAAE,GAAG,IAAI,GAAG,eAAe,CAAC;IAE9D,MAAM,cAAc,CAAC,SAAS,EAAE,UAAU,CAAC,CAAC;AAC9C,CAAC;AAED;;;;;;;;;;;;;GAaG;AACH,MAAM,CAAC,KAAK,UAAU,kBAAkB,CACtC,SAAiB,EACjB,MAAc,EACd,MAAkB,EAClB,KAAc;IAEd,MAAM,OAAO,GAAG,MAAM,aAAa,CAAC,SAAS,CAAC,CAAC;IAC/C,MAAM,QAAQ,GAAG,kBAAkB,CAAC,OAAO,CAAC,CAAC;IAE7C,sBAAsB;IACtB,MAAM,SAAS,GAAG,QAAQ,CAAC,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,MAAM,KAAK,MAAM,CAAC,CAAC;IAC/D,IAAI,SAAS,KAAK,CAAC,CAAC,EAAE,CAAC;QACrB,MAAM,IAAI,KAAK,CAAC,QAAQ,MAAM,8BAA8B,CAAC,CAAC;IAChE,CAAC;IAED,MAAM,IAAI,GAAG,QAAQ,CAAC,SAAS,CAAC,CAAC;IACjC,MAAM,GAAG,GAAG,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE,CAAC;IAErC,gBAAgB;IAChB,IAAI,CAAC,MAAM,GAAG,MAAM,CAAC;IAErB,4CAA4C;IAC5C,IAAI,MAAM,KAAK,aAAa,IAAI,CAAC,IAAI,CAAC,SAAS,EAAE,CAAC;QAChD,IAAI,CAAC,SAAS,GAAG,GAAG,CAAC;IACvB,CAAC;IACD,IAAI,MAAM,KAAK,WAAW,IAAI,MAAM,KAAK,QAAQ,EAAE,CAAC;QAClD,IAAI,CAAC,WAAW,GAAG,GAAG,CAAC;IACzB,CAAC;IAED,kCAAkC;IAClC,IAAI,KAAK,EAAE,CAAC;QACV,IAAI,CAAC,KAAK,GAAG,KAAK,CAAC;IACrB,CAAC;IAED,mBAAmB;IACnB,MAAM,QAAQ,GAAG,qBAAqB,CAAC,QAAQ,CAAC,CAAC;IAEjD,sCAAsC;IACtC,MAAM,YAAY,GAAG,OAAO,CAAC,OAAO,CAAC,qBAAqB,CAAC,CAAC;IAC5D,IAAI,YAAY,KAAK,CAAC,CAAC,EAAE,CAAC;QACxB,MAAM,IAAI,KAAK,CAAC,oEAAoE,CAAC,CAAC;IACxF,CAAC;IAED,mCAAmC;IACnC,MAAM,YAAY,GAAG,OAAO,CAAC,KAAK,CAAC,YAAY,GAAG,qBAAqB,CAAC,MAAM,CAAC,CAAC;IAChF,MAAM,gBAAgB,GAAG,YAAY,CAAC,KAAK,CAAC,WAAW,CAAC,CAAC;IACzD,MAAM,iBAAiB,GAAG,gBAAgB;QACxC,CAAC,CAAC,YAAY,GAAG,qBAAqB,CAAC,MAAM,GAAG,gBAAgB,CAAC,KAAM;QACvE,CAAC,CAAC,OAAO,CAAC,MAAM,CAAC;IAEnB,kBAAkB;IAClB,MAAM,aAAa,GAAG,OAAO,CAAC,KAAK,CAAC,CAAC,EAAE,YAAY,CAAC,CAAC;IACrD,MAAM,gBAAgB,GAAG,OAAO,CAAC,KAAK,CAAC,iBAAiB,CAAC,CAAC;IAC1D,MAAM,UAAU,GAAG,GAAG,aAAa,GAAG,qBAAqB,OAAO,QAAQ,KAAK,gBAAgB,EAAE,CAAC;IAElG,MAAM,cAAc,CAAC,SAAS,EAAE,UAAU,CAAC,CAAC;AAC9C,CAAC;AAED;;;;;GAKG;AACH,MAAM,CAAC,KAAK,UAAU,eAAe,CAAC,SAAiB;IACrD,MAAM,QAAQ,GAAG,MAAM,eAAe,CAAC,SAAS,CAAC,CAAC;IAClD,OAAO,QAAQ;SACZ,MAAM,CAAC,IAAI,CAAC,EAAE,CAAC,IAAI,CAAC,MAAM,KAAK,SAAS,CAAC;SACzC,GAAG,CAAC,IAAI,CAAC,EAAE,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;AAC9B,CAAC;AAED;;;;;GAKG;AACH,MAAM,CAAC,KAAK,UAAU,cAAc,CAAC,SAAiB;IACpD,MAAM,QAAQ,GAAG,MAAM,eAAe,CAAC,SAAS,CAAC,CAAC;IAClD,MAAM,WAAW,GAAG,QAAQ,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,IAAI,CAAC,MAAM,KAAK,aAAa,CAAC,CAAC;IACzE,OAAO,WAAW,CAAC,CAAC,CAAC,WAAW,CAAC,MAAM,CAAC,CAAC,CAAC,IAAI,CAAC;AACjD,CAAC"}
|
|
@@ -6,9 +6,16 @@
|
|
|
6
6
|
*/
|
|
7
7
|
import { WorkflowExecutionState, WorkflowStateValidationResult } from '../types/workflow-state.js';
|
|
8
8
|
/**
|
|
9
|
-
* Get the path to the workflow state file
|
|
9
|
+
* Get the path to the workflow state file.
|
|
10
|
+
*
|
|
11
|
+
* SECURITY: storyId is sanitized using sanitizeStoryId() to prevent path traversal attacks.
|
|
12
|
+
* Never construct paths manually - always use this function to ensure proper sanitization.
|
|
13
|
+
*
|
|
14
|
+
* @param sdlcRoot - Path to the .ai-sdlc directory
|
|
15
|
+
* @param storyId - Optional story ID for per-story state isolation
|
|
16
|
+
* @returns Path to the workflow state file (story-specific or global)
|
|
10
17
|
*/
|
|
11
|
-
export declare function getStateFilePath(sdlcRoot: string): string;
|
|
18
|
+
export declare function getStateFilePath(sdlcRoot: string, storyId?: string): string;
|
|
12
19
|
/**
|
|
13
20
|
* Generate a unique workflow ID based on timestamp
|
|
14
21
|
*/
|
|
@@ -24,15 +31,17 @@ export declare function calculateStoryHash(storyPath: string): string;
|
|
|
24
31
|
*
|
|
25
32
|
* @param state - The workflow execution state to save
|
|
26
33
|
* @param sdlcRoot - Path to the .ai-sdlc directory
|
|
34
|
+
* @param storyId - Optional story ID for per-story state isolation
|
|
27
35
|
*/
|
|
28
|
-
export declare function saveWorkflowState(state: WorkflowExecutionState, sdlcRoot: string): Promise<void>;
|
|
36
|
+
export declare function saveWorkflowState(state: WorkflowExecutionState, sdlcRoot: string, storyId?: string): Promise<void>;
|
|
29
37
|
/**
|
|
30
38
|
* Load workflow state from disk
|
|
31
39
|
*
|
|
32
40
|
* @param sdlcRoot - Path to the .ai-sdlc directory
|
|
41
|
+
* @param storyId - Optional story ID for per-story state isolation
|
|
33
42
|
* @returns The workflow state, or null if no state file exists
|
|
34
43
|
*/
|
|
35
|
-
export declare function loadWorkflowState(sdlcRoot: string): Promise<WorkflowExecutionState | null>;
|
|
44
|
+
export declare function loadWorkflowState(sdlcRoot: string, storyId?: string): Promise<WorkflowExecutionState | null>;
|
|
36
45
|
/**
|
|
37
46
|
* Validate workflow state structure
|
|
38
47
|
*
|
|
@@ -44,13 +53,43 @@ export declare function validateWorkflowState(state: any): WorkflowStateValidati
|
|
|
44
53
|
* Clear workflow state (delete the state file)
|
|
45
54
|
*
|
|
46
55
|
* @param sdlcRoot - Path to the .ai-sdlc directory
|
|
56
|
+
* @param storyId - Optional story ID for per-story state isolation
|
|
47
57
|
*/
|
|
48
|
-
export declare function clearWorkflowState(sdlcRoot: string): Promise<void>;
|
|
58
|
+
export declare function clearWorkflowState(sdlcRoot: string, storyId?: string): Promise<void>;
|
|
49
59
|
/**
|
|
50
60
|
* Check if workflow state exists
|
|
51
61
|
*
|
|
52
62
|
* @param sdlcRoot - Path to the .ai-sdlc directory
|
|
63
|
+
* @param storyId - Optional story ID for per-story state isolation
|
|
53
64
|
* @returns True if state file exists
|
|
54
65
|
*/
|
|
55
|
-
export declare function hasWorkflowState(sdlcRoot: string): boolean;
|
|
66
|
+
export declare function hasWorkflowState(sdlcRoot: string, storyId?: string): boolean;
|
|
67
|
+
/**
|
|
68
|
+
* Migrate global workflow state to story-specific location.
|
|
69
|
+
*
|
|
70
|
+
* Call this function at CLI startup (before loading workflow state) to automatically
|
|
71
|
+
* move legacy global state files to the new per-story location.
|
|
72
|
+
*
|
|
73
|
+
* Migration behavior:
|
|
74
|
+
* - Extracts story ID from context.options.story, completedActions, or currentAction
|
|
75
|
+
* - Skips migration if workflow is currently in progress (currentAction set)
|
|
76
|
+
* - Skips migration if no story ID can be determined
|
|
77
|
+
* - Validates target file if it already exists before deleting global file
|
|
78
|
+
* - Deletes global file only after successful write to target location
|
|
79
|
+
*
|
|
80
|
+
* @param sdlcRoot - Path to the .ai-sdlc directory
|
|
81
|
+
* @returns Promise resolving to migration result object:
|
|
82
|
+
* - migrated: boolean - true if migration was performed, false if skipped
|
|
83
|
+
* - message: string - Human-readable description of what happened
|
|
84
|
+
*
|
|
85
|
+
* @example
|
|
86
|
+
* const result = await migrateGlobalWorkflowState(sdlcRoot);
|
|
87
|
+
* if (result.migrated) {
|
|
88
|
+
* console.log(result.message); // "Successfully migrated workflow state to story S-0033"
|
|
89
|
+
* }
|
|
90
|
+
*/
|
|
91
|
+
export declare function migrateGlobalWorkflowState(sdlcRoot: string): Promise<{
|
|
92
|
+
migrated: boolean;
|
|
93
|
+
message: string;
|
|
94
|
+
}>;
|
|
56
95
|
//# sourceMappingURL=workflow-state.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"workflow-state.d.ts","sourceRoot":"","sources":["../../src/core/workflow-state.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAMH,OAAO,EACL,sBAAsB,EACtB,6BAA6B,EAE9B,MAAM,4BAA4B,CAAC;
|
|
1
|
+
{"version":3,"file":"workflow-state.d.ts","sourceRoot":"","sources":["../../src/core/workflow-state.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAMH,OAAO,EACL,sBAAsB,EACtB,6BAA6B,EAE9B,MAAM,4BAA4B,CAAC;AAQpC;;;;;;;;;GASG;AACH,wBAAgB,gBAAgB,CAAC,QAAQ,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,MAAM,GAAG,MAAM,CAM3E;AAED;;GAEG;AACH,wBAAgB,kBAAkB,IAAI,MAAM,CAI3C;AAED;;GAEG;AACH,wBAAgB,kBAAkB,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM,CAQ5D;AAED;;;;;;;;GAQG;AACH,wBAAsB,iBAAiB,CACrC,KAAK,EAAE,sBAAsB,EAC7B,QAAQ,EAAE,MAAM,EAChB,OAAO,CAAC,EAAE,MAAM,GACf,OAAO,CAAC,IAAI,CAAC,CAkCf;AAED;;;;;;GAMG;AACH,wBAAsB,iBAAiB,CACrC,QAAQ,EAAE,MAAM,EAChB,OAAO,CAAC,EAAE,MAAM,GACf,OAAO,CAAC,sBAAsB,GAAG,IAAI,CAAC,CAuCxC;AAED;;;;;GAKG;AACH,wBAAgB,qBAAqB,CAAC,KAAK,EAAE,GAAG,GAAG,6BAA6B,CAyC/E;AAED;;;;;GAKG;AACH,wBAAsB,kBAAkB,CAAC,QAAQ,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAW1F;AAED;;;;;;GAMG;AACH,wBAAgB,gBAAgB,CAAC,QAAQ,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,MAAM,GAAG,OAAO,CAG5E;AAED;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,wBAAsB,0BAA0B,CAC9C,QAAQ,EAAE,MAAM,GACf,OAAO,CAAC;IAAE,QAAQ,EAAE,OAAO,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,CAAC,CA0IjD"}
|
|
@@ -8,12 +8,26 @@ import fs from 'fs';
|
|
|
8
8
|
import path from 'path';
|
|
9
9
|
import crypto from 'crypto';
|
|
10
10
|
import writeFileAtomic from 'write-file-atomic';
|
|
11
|
+
import { STORIES_FOLDER } from '../types/index.js';
|
|
12
|
+
import { sanitizeStoryId } from './story.js';
|
|
13
|
+
import { getLogger } from './logger.js';
|
|
11
14
|
const STATE_FILE_NAME = '.workflow-state.json';
|
|
12
15
|
const CURRENT_VERSION = '1.0';
|
|
13
16
|
/**
|
|
14
|
-
* Get the path to the workflow state file
|
|
17
|
+
* Get the path to the workflow state file.
|
|
18
|
+
*
|
|
19
|
+
* SECURITY: storyId is sanitized using sanitizeStoryId() to prevent path traversal attacks.
|
|
20
|
+
* Never construct paths manually - always use this function to ensure proper sanitization.
|
|
21
|
+
*
|
|
22
|
+
* @param sdlcRoot - Path to the .ai-sdlc directory
|
|
23
|
+
* @param storyId - Optional story ID for per-story state isolation
|
|
24
|
+
* @returns Path to the workflow state file (story-specific or global)
|
|
15
25
|
*/
|
|
16
|
-
export function getStateFilePath(sdlcRoot) {
|
|
26
|
+
export function getStateFilePath(sdlcRoot, storyId) {
|
|
27
|
+
if (storyId) {
|
|
28
|
+
const sanitized = sanitizeStoryId(storyId);
|
|
29
|
+
return path.join(sdlcRoot, STORIES_FOLDER, sanitized, STATE_FILE_NAME);
|
|
30
|
+
}
|
|
17
31
|
return path.join(sdlcRoot, STATE_FILE_NAME);
|
|
18
32
|
}
|
|
19
33
|
/**
|
|
@@ -44,17 +58,33 @@ export function calculateStoryHash(storyPath) {
|
|
|
44
58
|
*
|
|
45
59
|
* @param state - The workflow execution state to save
|
|
46
60
|
* @param sdlcRoot - Path to the .ai-sdlc directory
|
|
61
|
+
* @param storyId - Optional story ID for per-story state isolation
|
|
47
62
|
*/
|
|
48
|
-
export async function saveWorkflowState(state, sdlcRoot) {
|
|
49
|
-
const
|
|
63
|
+
export async function saveWorkflowState(state, sdlcRoot, storyId) {
|
|
64
|
+
const logger = getLogger();
|
|
65
|
+
const statePath = getStateFilePath(sdlcRoot, storyId);
|
|
50
66
|
const stateJson = JSON.stringify(state, null, 2);
|
|
67
|
+
logger.debug('workflow-state', 'Saving workflow state', {
|
|
68
|
+
workflowId: state.workflowId,
|
|
69
|
+
storyId,
|
|
70
|
+
actionCount: state.completedActions.length,
|
|
71
|
+
});
|
|
51
72
|
try {
|
|
52
|
-
// Ensure the directory exists
|
|
53
|
-
|
|
73
|
+
// Ensure the directory exists (including story subdirectories)
|
|
74
|
+
const stateDir = path.dirname(statePath);
|
|
75
|
+
await fs.promises.mkdir(stateDir, { recursive: true });
|
|
54
76
|
// Write atomically to prevent corruption
|
|
55
77
|
await writeFileAtomic(statePath, stateJson, { encoding: 'utf-8' });
|
|
56
78
|
}
|
|
57
79
|
catch (error) {
|
|
80
|
+
// Specific permission error handling
|
|
81
|
+
if (error && typeof error === 'object' && 'code' in error) {
|
|
82
|
+
const code = error.code;
|
|
83
|
+
if (code === 'EACCES' || code === 'EPERM') {
|
|
84
|
+
throw new Error(`Permission denied: Cannot write workflow state to ${statePath}. ` +
|
|
85
|
+
`Check file permissions for the directory and ensure it is writable.`);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
58
88
|
throw new Error(`Failed to save workflow state: ${error instanceof Error ? error.message : String(error)}`);
|
|
59
89
|
}
|
|
60
90
|
}
|
|
@@ -62,13 +92,16 @@ export async function saveWorkflowState(state, sdlcRoot) {
|
|
|
62
92
|
* Load workflow state from disk
|
|
63
93
|
*
|
|
64
94
|
* @param sdlcRoot - Path to the .ai-sdlc directory
|
|
95
|
+
* @param storyId - Optional story ID for per-story state isolation
|
|
65
96
|
* @returns The workflow state, or null if no state file exists
|
|
66
97
|
*/
|
|
67
|
-
export async function loadWorkflowState(sdlcRoot) {
|
|
68
|
-
const
|
|
98
|
+
export async function loadWorkflowState(sdlcRoot, storyId) {
|
|
99
|
+
const logger = getLogger();
|
|
100
|
+
const statePath = getStateFilePath(sdlcRoot, storyId);
|
|
69
101
|
try {
|
|
70
102
|
// Check if file exists
|
|
71
103
|
if (!fs.existsSync(statePath)) {
|
|
104
|
+
logger.debug('workflow-state', 'No workflow state found', { storyId });
|
|
72
105
|
return null;
|
|
73
106
|
}
|
|
74
107
|
// Read and parse the state file
|
|
@@ -79,10 +112,17 @@ export async function loadWorkflowState(sdlcRoot) {
|
|
|
79
112
|
if (!validation.valid) {
|
|
80
113
|
throw new Error(`Invalid state file: ${validation.errors.join(', ')}`);
|
|
81
114
|
}
|
|
115
|
+
logger.debug('workflow-state', 'Loaded workflow state', {
|
|
116
|
+
workflowId: state.workflowId,
|
|
117
|
+
storyId,
|
|
118
|
+
actionCount: state.completedActions.length,
|
|
119
|
+
ageMs: Date.now() - new Date(state.timestamp).getTime(),
|
|
120
|
+
});
|
|
82
121
|
return state;
|
|
83
122
|
}
|
|
84
123
|
catch (error) {
|
|
85
124
|
if (error instanceof SyntaxError) {
|
|
125
|
+
logger.error('workflow-state', 'Corrupted state file', { statePath });
|
|
86
126
|
throw new Error(`Corrupted workflow state file at ${statePath}. ` +
|
|
87
127
|
`Delete the file to start fresh: rm "${statePath}"`);
|
|
88
128
|
}
|
|
@@ -136,9 +176,10 @@ export function validateWorkflowState(state) {
|
|
|
136
176
|
* Clear workflow state (delete the state file)
|
|
137
177
|
*
|
|
138
178
|
* @param sdlcRoot - Path to the .ai-sdlc directory
|
|
179
|
+
* @param storyId - Optional story ID for per-story state isolation
|
|
139
180
|
*/
|
|
140
|
-
export async function clearWorkflowState(sdlcRoot) {
|
|
141
|
-
const statePath = getStateFilePath(sdlcRoot);
|
|
181
|
+
export async function clearWorkflowState(sdlcRoot, storyId) {
|
|
182
|
+
const statePath = getStateFilePath(sdlcRoot, storyId);
|
|
142
183
|
try {
|
|
143
184
|
if (fs.existsSync(statePath)) {
|
|
144
185
|
await fs.promises.unlink(statePath);
|
|
@@ -153,10 +194,158 @@ export async function clearWorkflowState(sdlcRoot) {
|
|
|
153
194
|
* Check if workflow state exists
|
|
154
195
|
*
|
|
155
196
|
* @param sdlcRoot - Path to the .ai-sdlc directory
|
|
197
|
+
* @param storyId - Optional story ID for per-story state isolation
|
|
156
198
|
* @returns True if state file exists
|
|
157
199
|
*/
|
|
158
|
-
export function hasWorkflowState(sdlcRoot) {
|
|
159
|
-
const statePath = getStateFilePath(sdlcRoot);
|
|
200
|
+
export function hasWorkflowState(sdlcRoot, storyId) {
|
|
201
|
+
const statePath = getStateFilePath(sdlcRoot, storyId);
|
|
160
202
|
return fs.existsSync(statePath);
|
|
161
203
|
}
|
|
204
|
+
/**
|
|
205
|
+
* Migrate global workflow state to story-specific location.
|
|
206
|
+
*
|
|
207
|
+
* Call this function at CLI startup (before loading workflow state) to automatically
|
|
208
|
+
* move legacy global state files to the new per-story location.
|
|
209
|
+
*
|
|
210
|
+
* Migration behavior:
|
|
211
|
+
* - Extracts story ID from context.options.story, completedActions, or currentAction
|
|
212
|
+
* - Skips migration if workflow is currently in progress (currentAction set)
|
|
213
|
+
* - Skips migration if no story ID can be determined
|
|
214
|
+
* - Validates target file if it already exists before deleting global file
|
|
215
|
+
* - Deletes global file only after successful write to target location
|
|
216
|
+
*
|
|
217
|
+
* @param sdlcRoot - Path to the .ai-sdlc directory
|
|
218
|
+
* @returns Promise resolving to migration result object:
|
|
219
|
+
* - migrated: boolean - true if migration was performed, false if skipped
|
|
220
|
+
* - message: string - Human-readable description of what happened
|
|
221
|
+
*
|
|
222
|
+
* @example
|
|
223
|
+
* const result = await migrateGlobalWorkflowState(sdlcRoot);
|
|
224
|
+
* if (result.migrated) {
|
|
225
|
+
* console.log(result.message); // "Successfully migrated workflow state to story S-0033"
|
|
226
|
+
* }
|
|
227
|
+
*/
|
|
228
|
+
export async function migrateGlobalWorkflowState(sdlcRoot) {
|
|
229
|
+
const globalStatePath = getStateFilePath(sdlcRoot);
|
|
230
|
+
// Check if global state file exists
|
|
231
|
+
if (!fs.existsSync(globalStatePath)) {
|
|
232
|
+
return { migrated: false, message: 'No global workflow state file found' };
|
|
233
|
+
}
|
|
234
|
+
try {
|
|
235
|
+
// Read and parse the global state
|
|
236
|
+
const content = await fs.promises.readFile(globalStatePath, 'utf-8');
|
|
237
|
+
const state = JSON.parse(content);
|
|
238
|
+
// Check if workflow in progress IMMEDIATELY after reading state
|
|
239
|
+
if (state.currentAction) {
|
|
240
|
+
return {
|
|
241
|
+
migrated: false,
|
|
242
|
+
message: 'Skipping migration: workflow currently in progress. Complete or reset workflow before migrating.',
|
|
243
|
+
};
|
|
244
|
+
}
|
|
245
|
+
// Determine the story ID from the state
|
|
246
|
+
let storyId;
|
|
247
|
+
// Try to get story ID from context.options.story first
|
|
248
|
+
if (state.context?.options?.story) {
|
|
249
|
+
storyId = state.context.options.story;
|
|
250
|
+
}
|
|
251
|
+
// Fallback to first completed action's storyId
|
|
252
|
+
if (!storyId && state.completedActions && state.completedActions.length > 0) {
|
|
253
|
+
const firstAction = state.completedActions[0];
|
|
254
|
+
if (firstAction && 'storyId' in firstAction) {
|
|
255
|
+
storyId = firstAction.storyId;
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
// If no story ID found, leave state in place
|
|
259
|
+
if (!storyId) {
|
|
260
|
+
return {
|
|
261
|
+
migrated: false,
|
|
262
|
+
message: 'Cannot migrate: no story ID found in workflow state. Manual migration required.',
|
|
263
|
+
};
|
|
264
|
+
}
|
|
265
|
+
// SECURITY: Sanitize story ID to prevent path traversal
|
|
266
|
+
let sanitizedStoryId;
|
|
267
|
+
try {
|
|
268
|
+
sanitizedStoryId = sanitizeStoryId(storyId);
|
|
269
|
+
}
|
|
270
|
+
catch (error) {
|
|
271
|
+
return {
|
|
272
|
+
migrated: false,
|
|
273
|
+
message: `Cannot migrate: invalid story ID "${storyId}". ${error instanceof Error ? error.message : String(error)}`,
|
|
274
|
+
};
|
|
275
|
+
}
|
|
276
|
+
// Check if target location already exists (idempotent)
|
|
277
|
+
const targetPath = getStateFilePath(sdlcRoot, sanitizedStoryId);
|
|
278
|
+
if (fs.existsSync(targetPath)) {
|
|
279
|
+
// Validate existing target is not corrupt before deleting global backup
|
|
280
|
+
try {
|
|
281
|
+
const targetContent = await fs.promises.readFile(targetPath, 'utf-8');
|
|
282
|
+
const targetState = JSON.parse(targetContent);
|
|
283
|
+
const validation = validateWorkflowState(targetState);
|
|
284
|
+
if (!validation.valid) {
|
|
285
|
+
return {
|
|
286
|
+
migrated: false,
|
|
287
|
+
message: `Target state file exists but is invalid. Keeping global file as backup. Manual intervention required. Errors: ${validation.errors.join(', ')}`,
|
|
288
|
+
};
|
|
289
|
+
}
|
|
290
|
+
// Target is valid, safe to delete global
|
|
291
|
+
await fs.promises.unlink(globalStatePath);
|
|
292
|
+
return {
|
|
293
|
+
migrated: true,
|
|
294
|
+
message: `Global workflow state already migrated to story ${sanitizedStoryId}. Removed duplicate global file.`,
|
|
295
|
+
};
|
|
296
|
+
}
|
|
297
|
+
catch (error) {
|
|
298
|
+
return {
|
|
299
|
+
migrated: false,
|
|
300
|
+
message: `Target state file exists but is corrupted. Keeping global file as backup. Manual intervention required. Error: ${error instanceof Error ? error.message : String(error)}`,
|
|
301
|
+
};
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
// Create target directory
|
|
305
|
+
const targetDir = path.dirname(targetPath);
|
|
306
|
+
await fs.promises.mkdir(targetDir, { recursive: true });
|
|
307
|
+
// Write to target location atomically
|
|
308
|
+
const stateJson = JSON.stringify(state, null, 2);
|
|
309
|
+
await writeFileAtomic(targetPath, stateJson, { encoding: 'utf-8' });
|
|
310
|
+
// Verify target file was created successfully
|
|
311
|
+
if (!fs.existsSync(targetPath)) {
|
|
312
|
+
throw new Error('Target state file was not created successfully');
|
|
313
|
+
}
|
|
314
|
+
// Delete global file only after successful write
|
|
315
|
+
await fs.promises.unlink(globalStatePath);
|
|
316
|
+
return {
|
|
317
|
+
migrated: true,
|
|
318
|
+
message: `Successfully migrated workflow state from global to story-specific location: ${sanitizedStoryId}`,
|
|
319
|
+
};
|
|
320
|
+
}
|
|
321
|
+
catch (error) {
|
|
322
|
+
// Specific error handling for common cases
|
|
323
|
+
if (error && typeof error === 'object' && 'code' in error) {
|
|
324
|
+
const code = error.code;
|
|
325
|
+
if (code === 'ENOENT') {
|
|
326
|
+
return {
|
|
327
|
+
migrated: false,
|
|
328
|
+
message: 'Global state file disappeared during migration. No action taken.',
|
|
329
|
+
};
|
|
330
|
+
}
|
|
331
|
+
if (code === 'EACCES' || code === 'EPERM') {
|
|
332
|
+
return {
|
|
333
|
+
migrated: false,
|
|
334
|
+
message: `Permission denied during migration. Check file permissions for: ${globalStatePath}`,
|
|
335
|
+
};
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
if (error instanceof SyntaxError) {
|
|
339
|
+
return {
|
|
340
|
+
migrated: false,
|
|
341
|
+
message: 'Global state file contains invalid JSON. Manual migration required.',
|
|
342
|
+
};
|
|
343
|
+
}
|
|
344
|
+
const errorMsg = error instanceof Error ? error.message : String(error);
|
|
345
|
+
return {
|
|
346
|
+
migrated: false,
|
|
347
|
+
message: `Migration failed: ${errorMsg}`,
|
|
348
|
+
};
|
|
349
|
+
}
|
|
350
|
+
}
|
|
162
351
|
//# sourceMappingURL=workflow-state.js.map
|