ai-sdlc 0.2.0-alpha.6 → 0.2.0-alpha.61

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (143) hide show
  1. package/README.md +65 -1057
  2. package/dist/agents/implementation.d.ts +36 -1
  3. package/dist/agents/implementation.d.ts.map +1 -1
  4. package/dist/agents/implementation.js +259 -30
  5. package/dist/agents/implementation.js.map +1 -1
  6. package/dist/agents/index.d.ts +2 -0
  7. package/dist/agents/index.d.ts.map +1 -1
  8. package/dist/agents/index.js +2 -0
  9. package/dist/agents/index.js.map +1 -1
  10. package/dist/agents/orchestrator.d.ts +61 -0
  11. package/dist/agents/orchestrator.d.ts.map +1 -0
  12. package/dist/agents/orchestrator.js +443 -0
  13. package/dist/agents/orchestrator.js.map +1 -0
  14. package/dist/agents/planning.d.ts +1 -1
  15. package/dist/agents/planning.d.ts.map +1 -1
  16. package/dist/agents/planning.js +55 -4
  17. package/dist/agents/planning.js.map +1 -1
  18. package/dist/agents/refinement.d.ts.map +1 -1
  19. package/dist/agents/refinement.js +22 -3
  20. package/dist/agents/refinement.js.map +1 -1
  21. package/dist/agents/research.d.ts +85 -1
  22. package/dist/agents/research.d.ts.map +1 -1
  23. package/dist/agents/research.js +506 -16
  24. package/dist/agents/research.js.map +1 -1
  25. package/dist/agents/review.d.ts +116 -2
  26. package/dist/agents/review.d.ts.map +1 -1
  27. package/dist/agents/review.js +847 -93
  28. package/dist/agents/review.js.map +1 -1
  29. package/dist/agents/rework.d.ts.map +1 -1
  30. package/dist/agents/rework.js +25 -4
  31. package/dist/agents/rework.js.map +1 -1
  32. package/dist/agents/single-task.d.ts +41 -0
  33. package/dist/agents/single-task.d.ts.map +1 -0
  34. package/dist/agents/single-task.js +357 -0
  35. package/dist/agents/single-task.js.map +1 -0
  36. package/dist/agents/state-assessor.d.ts +3 -3
  37. package/dist/agents/state-assessor.d.ts.map +1 -1
  38. package/dist/agents/state-assessor.js +6 -6
  39. package/dist/agents/state-assessor.js.map +1 -1
  40. package/dist/agents/test-pattern-detector.d.ts +49 -0
  41. package/dist/agents/test-pattern-detector.d.ts.map +1 -0
  42. package/dist/agents/test-pattern-detector.js +273 -0
  43. package/dist/agents/test-pattern-detector.js.map +1 -0
  44. package/dist/agents/verification.d.ts +11 -0
  45. package/dist/agents/verification.d.ts.map +1 -1
  46. package/dist/agents/verification.js +99 -12
  47. package/dist/agents/verification.js.map +1 -1
  48. package/dist/cli/batch-processor.d.ts +64 -0
  49. package/dist/cli/batch-processor.d.ts.map +1 -0
  50. package/dist/cli/batch-processor.js +85 -0
  51. package/dist/cli/batch-processor.js.map +1 -0
  52. package/dist/cli/batch-validator.d.ts +80 -0
  53. package/dist/cli/batch-validator.d.ts.map +1 -0
  54. package/dist/cli/batch-validator.js +121 -0
  55. package/dist/cli/batch-validator.js.map +1 -0
  56. package/dist/cli/commands/migrate.js +1 -1
  57. package/dist/cli/commands/migrate.js.map +1 -1
  58. package/dist/cli/commands.d.ts +67 -3
  59. package/dist/cli/commands.d.ts.map +1 -1
  60. package/dist/cli/commands.js +1765 -198
  61. package/dist/cli/commands.js.map +1 -1
  62. package/dist/cli/daemon.d.ts.map +1 -1
  63. package/dist/cli/daemon.js +25 -3
  64. package/dist/cli/daemon.js.map +1 -1
  65. package/dist/cli/runner.d.ts.map +1 -1
  66. package/dist/cli/runner.js +35 -12
  67. package/dist/cli/runner.js.map +1 -1
  68. package/dist/core/auth.d.ts +43 -0
  69. package/dist/core/auth.d.ts.map +1 -1
  70. package/dist/core/auth.js +105 -1
  71. package/dist/core/auth.js.map +1 -1
  72. package/dist/core/client.d.ts +25 -1
  73. package/dist/core/client.d.ts.map +1 -1
  74. package/dist/core/client.js +247 -7
  75. package/dist/core/client.js.map +1 -1
  76. package/dist/core/config.d.ts +32 -1
  77. package/dist/core/config.d.ts.map +1 -1
  78. package/dist/core/config.js +146 -3
  79. package/dist/core/config.js.map +1 -1
  80. package/dist/core/conflict-detector.d.ts +108 -0
  81. package/dist/core/conflict-detector.d.ts.map +1 -0
  82. package/dist/core/conflict-detector.js +413 -0
  83. package/dist/core/conflict-detector.js.map +1 -0
  84. package/dist/core/git-utils.d.ts +28 -0
  85. package/dist/core/git-utils.d.ts.map +1 -0
  86. package/dist/core/git-utils.js +146 -0
  87. package/dist/core/git-utils.js.map +1 -0
  88. package/dist/core/index.d.ts +19 -0
  89. package/dist/core/index.d.ts.map +1 -0
  90. package/dist/core/index.js +19 -0
  91. package/dist/core/index.js.map +1 -0
  92. package/dist/core/kanban.d.ts +1 -1
  93. package/dist/core/kanban.d.ts.map +1 -1
  94. package/dist/core/kanban.js +3 -3
  95. package/dist/core/kanban.js.map +1 -1
  96. package/dist/core/llm-utils.d.ts +103 -0
  97. package/dist/core/llm-utils.d.ts.map +1 -0
  98. package/dist/core/llm-utils.js +368 -0
  99. package/dist/core/llm-utils.js.map +1 -0
  100. package/dist/core/logger.d.ts +92 -0
  101. package/dist/core/logger.d.ts.map +1 -0
  102. package/dist/core/logger.js +221 -0
  103. package/dist/core/logger.js.map +1 -0
  104. package/dist/core/process-manager.d.ts +15 -0
  105. package/dist/core/process-manager.d.ts.map +1 -0
  106. package/dist/core/process-manager.js +132 -0
  107. package/dist/core/process-manager.js.map +1 -0
  108. package/dist/core/story-logger.d.ts +102 -0
  109. package/dist/core/story-logger.d.ts.map +1 -0
  110. package/dist/core/story-logger.js +265 -0
  111. package/dist/core/story-logger.js.map +1 -0
  112. package/dist/core/story.d.ts +113 -20
  113. package/dist/core/story.d.ts.map +1 -1
  114. package/dist/core/story.js +328 -40
  115. package/dist/core/story.js.map +1 -1
  116. package/dist/core/task-parser.d.ts +59 -0
  117. package/dist/core/task-parser.d.ts.map +1 -0
  118. package/dist/core/task-parser.js +235 -0
  119. package/dist/core/task-parser.js.map +1 -0
  120. package/dist/core/task-progress.d.ts +92 -0
  121. package/dist/core/task-progress.d.ts.map +1 -0
  122. package/dist/core/task-progress.js +280 -0
  123. package/dist/core/task-progress.js.map +1 -0
  124. package/dist/core/workflow-state.d.ts +45 -6
  125. package/dist/core/workflow-state.d.ts.map +1 -1
  126. package/dist/core/workflow-state.js +201 -12
  127. package/dist/core/workflow-state.js.map +1 -1
  128. package/dist/core/worktree.d.ts +186 -0
  129. package/dist/core/worktree.d.ts.map +1 -0
  130. package/dist/core/worktree.js +554 -0
  131. package/dist/core/worktree.js.map +1 -0
  132. package/dist/index.js +146 -5
  133. package/dist/index.js.map +1 -1
  134. package/dist/services/error-classifier.d.ts +119 -0
  135. package/dist/services/error-classifier.d.ts.map +1 -0
  136. package/dist/services/error-classifier.js +182 -0
  137. package/dist/services/error-classifier.js.map +1 -0
  138. package/dist/types/index.d.ts +381 -1
  139. package/dist/types/index.d.ts.map +1 -1
  140. package/dist/types/index.js +1 -0
  141. package/dist/types/index.js.map +1 -1
  142. package/package.json +5 -2
  143. 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":"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"}
@@ -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;AAKpC;;GAEG;AACH,wBAAgB,gBAAgB,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,CAEzD;AAED;;GAEG;AACH,wBAAgB,kBAAkB,IAAI,MAAM,CAI3C;AAED;;GAEG;AACH,wBAAgB,kBAAkB,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM,CAQ5D;AAED;;;;;;;GAOG;AACH,wBAAsB,iBAAiB,CACrC,KAAK,EAAE,sBAAsB,EAC7B,QAAQ,EAAE,MAAM,GACf,OAAO,CAAC,IAAI,CAAC,CAef;AAED;;;;;GAKG;AACH,wBAAsB,iBAAiB,CACrC,QAAQ,EAAE,MAAM,GACf,OAAO,CAAC,sBAAsB,GAAG,IAAI,CAAC,CA6BxC;AAED;;;;;GAKG;AACH,wBAAgB,qBAAqB,CAAC,KAAK,EAAE,GAAG,GAAG,6BAA6B,CAyC/E;AAED;;;;GAIG;AACH,wBAAsB,kBAAkB,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAWxE;AAED;;;;;GAKG;AACH,wBAAgB,gBAAgB,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAG1D"}
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 statePath = getStateFilePath(sdlcRoot);
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
- await fs.promises.mkdir(sdlcRoot, { recursive: true });
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 statePath = getStateFilePath(sdlcRoot);
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