awesome-slash 2.4.4 → 2.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (144) hide show
  1. package/.claude-plugin/marketplace.json +6 -6
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/CHANGELOG.md +88 -1
  4. package/README.md +173 -161
  5. package/SECURITY.md +25 -81
  6. package/adapters/codex/install.sh +58 -16
  7. package/adapters/opencode/install.sh +92 -23
  8. package/lib/index.js +47 -4
  9. package/lib/patterns/review-patterns.js +58 -11
  10. package/lib/patterns/slop-patterns.js +154 -147
  11. package/lib/platform/detect-platform.js +99 -350
  12. package/lib/platform/detection-configs.js +93 -0
  13. package/lib/platform/verify-tools.js +10 -78
  14. package/lib/schemas/README.md +195 -0
  15. package/lib/schemas/validator.js +247 -0
  16. package/lib/sources/custom-handler.js +199 -0
  17. package/lib/sources/policy-questions.js +239 -0
  18. package/lib/sources/source-cache.js +149 -0
  19. package/lib/state/workflow-state.js +363 -665
  20. package/lib/types/README.md +292 -0
  21. package/lib/types/agent-frontmatter.d.ts +134 -0
  22. package/lib/types/command-frontmatter.d.ts +107 -0
  23. package/lib/types/hook-frontmatter.d.ts +115 -0
  24. package/lib/types/index.d.ts +84 -0
  25. package/lib/types/plugin-manifest.d.ts +102 -0
  26. package/lib/types/skill-frontmatter.d.ts +89 -0
  27. package/lib/utils/cache-manager.js +154 -0
  28. package/lib/utils/context-optimizer.js +5 -36
  29. package/lib/utils/deprecation.js +37 -0
  30. package/lib/utils/shell-escape.js +88 -0
  31. package/mcp-server/index.js +513 -18
  32. package/package.json +6 -2
  33. package/plugins/deslop-around/.claude-plugin/plugin.json +1 -1
  34. package/plugins/deslop-around/lib/index.js +170 -0
  35. package/plugins/deslop-around/lib/patterns/review-patterns.js +58 -11
  36. package/plugins/deslop-around/lib/patterns/slop-patterns.js +170 -129
  37. package/plugins/deslop-around/lib/platform/detect-platform.js +212 -123
  38. package/plugins/deslop-around/lib/platform/detection-configs.js +93 -0
  39. package/plugins/deslop-around/lib/platform/verify-tools.js +10 -1
  40. package/plugins/deslop-around/lib/schemas/README.md +195 -0
  41. package/plugins/deslop-around/lib/schemas/validator.js +205 -0
  42. package/plugins/deslop-around/lib/sources/custom-handler.js +199 -0
  43. package/plugins/deslop-around/lib/sources/policy-questions.js +239 -0
  44. package/plugins/deslop-around/lib/sources/source-cache.js +149 -0
  45. package/plugins/deslop-around/lib/state/workflow-state.js +382 -484
  46. package/plugins/deslop-around/lib/types/README.md +292 -0
  47. package/plugins/deslop-around/lib/types/agent-frontmatter.d.ts +134 -0
  48. package/plugins/deslop-around/lib/types/command-frontmatter.d.ts +107 -0
  49. package/plugins/deslop-around/lib/types/hook-frontmatter.d.ts +115 -0
  50. package/plugins/deslop-around/lib/types/index.d.ts +84 -0
  51. package/plugins/deslop-around/lib/types/plugin-manifest.d.ts +102 -0
  52. package/plugins/deslop-around/lib/types/skill-frontmatter.d.ts +89 -0
  53. package/plugins/deslop-around/lib/utils/cache-manager.js +154 -0
  54. package/plugins/deslop-around/lib/utils/context-optimizer.js +115 -37
  55. package/plugins/deslop-around/lib/utils/deprecation.js +37 -0
  56. package/plugins/deslop-around/lib/utils/shell-escape.js +88 -0
  57. package/plugins/next-task/.claude-plugin/plugin.json +1 -1
  58. package/plugins/next-task/agents/delivery-validator.md +2 -2
  59. package/plugins/next-task/agents/implementation-agent.md +3 -4
  60. package/plugins/next-task/agents/planning-agent.md +77 -19
  61. package/plugins/next-task/agents/review-orchestrator.md +21 -122
  62. package/plugins/next-task/agents/task-discoverer.md +164 -23
  63. package/plugins/next-task/commands/next-task.md +180 -14
  64. package/plugins/next-task/lib/index.js +170 -0
  65. package/plugins/next-task/lib/patterns/review-patterns.js +58 -11
  66. package/plugins/next-task/lib/patterns/slop-patterns.js +170 -129
  67. package/plugins/next-task/lib/platform/detect-platform.js +212 -123
  68. package/plugins/next-task/lib/platform/detection-configs.js +93 -0
  69. package/plugins/next-task/lib/platform/verify-tools.js +10 -1
  70. package/plugins/next-task/lib/schemas/README.md +195 -0
  71. package/plugins/next-task/lib/schemas/validator.js +205 -0
  72. package/plugins/next-task/lib/sources/custom-handler.js +199 -0
  73. package/plugins/next-task/lib/sources/policy-questions.js +239 -0
  74. package/plugins/next-task/lib/sources/source-cache.js +149 -0
  75. package/plugins/next-task/lib/state/workflow-state.js +382 -484
  76. package/plugins/next-task/lib/types/README.md +292 -0
  77. package/plugins/next-task/lib/types/agent-frontmatter.d.ts +134 -0
  78. package/plugins/next-task/lib/types/command-frontmatter.d.ts +107 -0
  79. package/plugins/next-task/lib/types/hook-frontmatter.d.ts +115 -0
  80. package/plugins/next-task/lib/types/index.d.ts +84 -0
  81. package/plugins/next-task/lib/types/plugin-manifest.d.ts +102 -0
  82. package/plugins/next-task/lib/types/skill-frontmatter.d.ts +89 -0
  83. package/plugins/next-task/lib/utils/cache-manager.js +154 -0
  84. package/plugins/next-task/lib/utils/context-optimizer.js +115 -37
  85. package/plugins/next-task/lib/utils/deprecation.js +37 -0
  86. package/plugins/next-task/lib/utils/shell-escape.js +88 -0
  87. package/plugins/project-review/.claude-plugin/plugin.json +1 -1
  88. package/plugins/project-review/lib/index.js +170 -0
  89. package/plugins/project-review/lib/patterns/review-patterns.js +58 -11
  90. package/plugins/project-review/lib/patterns/slop-patterns.js +170 -129
  91. package/plugins/project-review/lib/platform/detect-platform.js +212 -123
  92. package/plugins/project-review/lib/platform/detection-configs.js +93 -0
  93. package/plugins/project-review/lib/platform/verify-tools.js +10 -1
  94. package/plugins/project-review/lib/schemas/README.md +195 -0
  95. package/plugins/project-review/lib/schemas/validator.js +205 -0
  96. package/plugins/project-review/lib/sources/custom-handler.js +199 -0
  97. package/plugins/project-review/lib/sources/policy-questions.js +239 -0
  98. package/plugins/project-review/lib/sources/source-cache.js +149 -0
  99. package/plugins/project-review/lib/state/workflow-state.js +382 -484
  100. package/plugins/project-review/lib/types/README.md +292 -0
  101. package/plugins/project-review/lib/types/agent-frontmatter.d.ts +134 -0
  102. package/plugins/project-review/lib/types/command-frontmatter.d.ts +107 -0
  103. package/plugins/project-review/lib/types/hook-frontmatter.d.ts +115 -0
  104. package/plugins/project-review/lib/types/index.d.ts +84 -0
  105. package/plugins/project-review/lib/types/plugin-manifest.d.ts +102 -0
  106. package/plugins/project-review/lib/types/skill-frontmatter.d.ts +89 -0
  107. package/plugins/project-review/lib/utils/cache-manager.js +154 -0
  108. package/plugins/project-review/lib/utils/context-optimizer.js +115 -37
  109. package/plugins/project-review/lib/utils/deprecation.js +37 -0
  110. package/plugins/project-review/lib/utils/shell-escape.js +88 -0
  111. package/plugins/reality-check/.claude-plugin/plugin.json +1 -1
  112. package/plugins/reality-check/agents/code-explorer.md +1 -1
  113. package/plugins/ship/.claude-plugin/plugin.json +1 -1
  114. package/plugins/ship/lib/index.js +170 -0
  115. package/plugins/ship/lib/patterns/review-patterns.js +58 -11
  116. package/plugins/ship/lib/patterns/slop-patterns.js +170 -129
  117. package/plugins/ship/lib/platform/detect-platform.js +212 -123
  118. package/plugins/ship/lib/platform/detection-configs.js +93 -0
  119. package/plugins/ship/lib/platform/verify-tools.js +10 -1
  120. package/plugins/ship/lib/schemas/README.md +195 -0
  121. package/plugins/ship/lib/schemas/validator.js +205 -0
  122. package/plugins/ship/lib/sources/custom-handler.js +199 -0
  123. package/plugins/ship/lib/sources/policy-questions.js +239 -0
  124. package/plugins/ship/lib/sources/source-cache.js +149 -0
  125. package/plugins/ship/lib/state/workflow-state.js +382 -484
  126. package/plugins/ship/lib/types/README.md +292 -0
  127. package/plugins/ship/lib/types/agent-frontmatter.d.ts +134 -0
  128. package/plugins/ship/lib/types/command-frontmatter.d.ts +107 -0
  129. package/plugins/ship/lib/types/hook-frontmatter.d.ts +115 -0
  130. package/plugins/ship/lib/types/index.d.ts +84 -0
  131. package/plugins/ship/lib/types/plugin-manifest.d.ts +102 -0
  132. package/plugins/ship/lib/types/skill-frontmatter.d.ts +89 -0
  133. package/plugins/ship/lib/utils/cache-manager.js +154 -0
  134. package/plugins/ship/lib/utils/context-optimizer.js +115 -37
  135. package/plugins/ship/lib/utils/deprecation.js +37 -0
  136. package/plugins/ship/lib/utils/shell-escape.js +88 -0
  137. package/lib/state/workflow-state.schema.json +0 -282
  138. package/plugins/deslop-around/lib/state/workflow-state.schema.json +0 -282
  139. package/plugins/next-task/agents/policy-selector.md +0 -248
  140. package/plugins/next-task/lib/state/tasks-registry.schema.json +0 -85
  141. package/plugins/next-task/lib/state/workflow-state.schema.json +0 -282
  142. package/plugins/next-task/lib/state/worktree-status.schema.json +0 -219
  143. package/plugins/project-review/lib/state/workflow-state.schema.json +0 -282
  144. package/plugins/ship/lib/state/workflow-state.schema.json +0 -282
@@ -1,130 +1,64 @@
1
1
  /**
2
- * Workflow State Management
2
+ * Simplified workflow state management
3
3
  *
4
- * Persistent state management for next-task workflow orchestration.
5
- * Enables resume capability, multi-agent coordination, and progress tracking.
4
+ * Two files:
5
+ * - Main project: .claude/tasks.json (tracks active worktree/task)
6
+ * - Worktree: .claude/flow.json (tracks workflow progress)
6
7
  */
7
8
 
8
9
  const fs = require('fs');
9
10
  const path = require('path');
10
11
  const crypto = require('crypto');
11
12
 
12
- const SCHEMA_VERSION = '2.0.0';
13
- const STATE_DIR = '.claude';
14
- const STATE_FILE = 'workflow-state.json';
13
+ // File paths
14
+ const CLAUDE_DIR = '.claude';
15
+ const TASKS_FILE = 'tasks.json';
16
+ const FLOW_FILE = 'flow.json';
15
17
 
16
18
  /**
17
- * State cache configuration
18
- */
19
- const STATE_CACHE_TTL_MS = 200; // Cache TTL for rapid successive reads
20
- const _stateCache = new Map(); // Cache keyed by resolved base directory
21
-
22
- /**
23
- * Get cached state if valid
24
- * @param {string} cacheKey - Cache key (resolved base path)
25
- * @returns {Object|null} Cached state or null if expired/missing
26
- */
27
- function getCachedState(cacheKey) {
28
- const cached = _stateCache.get(cacheKey);
29
- if (cached && Date.now() < cached.expiry) {
30
- return cached.state;
31
- }
32
- return null;
33
- }
34
-
35
- /**
36
- * Set state cache
37
- * @param {string} cacheKey - Cache key (resolved base path)
38
- * @param {Object} state - State to cache
39
- */
40
- function setCachedState(cacheKey, state) {
41
- _stateCache.set(cacheKey, {
42
- state,
43
- expiry: Date.now() + STATE_CACHE_TTL_MS
44
- });
45
- }
46
-
47
- /**
48
- * Invalidate state cache for a directory
49
- * @param {string} cacheKey - Cache key (resolved base path)
50
- */
51
- function invalidateStateCache(cacheKey) {
52
- _stateCache.delete(cacheKey);
53
- }
54
-
55
- /**
56
- * Clear all state caches (useful for testing)
57
- */
58
- function clearAllStateCaches() {
59
- _stateCache.clear();
60
- }
61
-
62
- /**
63
- * Validate and normalize base directory path to prevent path traversal
64
- * @param {string} baseDir - Base directory path
19
+ * Validate and resolve path to prevent path traversal attacks
20
+ * @param {string} basePath - Base directory path
65
21
  * @returns {string} Validated absolute path
66
- * @throws {Error} If path is invalid or potentially dangerous
22
+ * @throws {Error} If path is invalid
67
23
  */
68
- function validateBasePath(baseDir) {
69
- if (typeof baseDir !== 'string' || baseDir.length === 0) {
70
- throw new Error('Base directory must be a non-empty string');
24
+ function validatePath(basePath) {
25
+ if (typeof basePath !== 'string' || basePath.length === 0) {
26
+ throw new Error('Path must be a non-empty string');
71
27
  }
72
-
73
- // Resolve to absolute path
74
- const resolvedPath = path.resolve(baseDir);
75
-
76
- // Check for null bytes (path traversal via null byte injection)
77
- if (resolvedPath.includes('\0')) {
28
+ const resolved = path.resolve(basePath);
29
+ if (resolved.includes('\0')) {
78
30
  throw new Error('Path contains invalid null byte');
79
31
  }
32
+ return resolved;
33
+ }
80
34
 
81
- // Ensure the path exists and is a directory
82
- try {
83
- const stats = fs.statSync(resolvedPath);
84
- if (!stats.isDirectory()) {
85
- throw new Error('Path is not a directory');
86
- }
87
- } catch (error) {
88
- if (error.code === 'ENOENT') {
89
- // Directory doesn't exist yet - that's OK, it will be created
90
- // But the parent must exist and be a directory
91
- const parentDir = path.dirname(resolvedPath);
92
- try {
93
- const parentStats = fs.statSync(parentDir);
94
- if (!parentStats.isDirectory()) {
95
- throw new Error('Parent path is not a directory');
96
- }
97
- } catch (parentError) {
98
- if (parentError.code === 'ENOENT') {
99
- throw new Error('Parent directory does not exist');
100
- }
101
- throw parentError;
102
- }
103
- } else if (error.message) {
104
- throw error;
105
- }
35
+ /**
36
+ * Validate that target path is within base directory
37
+ * @param {string} targetPath - Target file path
38
+ * @param {string} basePath - Base directory
39
+ * @throws {Error} If path traversal detected
40
+ */
41
+ function validatePathWithinBase(targetPath, basePath) {
42
+ const resolvedTarget = path.resolve(targetPath);
43
+ const resolvedBase = path.resolve(basePath);
44
+ if (!resolvedTarget.startsWith(resolvedBase + path.sep) && resolvedTarget !== resolvedBase) {
45
+ throw new Error('Path traversal detected');
106
46
  }
107
-
108
- return resolvedPath;
109
47
  }
110
48
 
111
49
  /**
112
- * Validate that the final state path is within the base directory
113
- * @param {string} statePath - The full state file path
114
- * @param {string} baseDir - The validated base directory
115
- * @throws {Error} If path traversal is detected
50
+ * Generate a unique workflow ID
51
+ * @returns {string} Workflow ID
116
52
  */
117
- function validateStatePathWithinBase(statePath, baseDir) {
118
- const resolvedStatePath = path.resolve(statePath);
119
- const resolvedBaseDir = path.resolve(baseDir);
120
-
121
- // Ensure state path is within base directory
122
- if (!resolvedStatePath.startsWith(resolvedBaseDir + path.sep) &&
123
- resolvedStatePath !== resolvedBaseDir) {
124
- throw new Error('Path traversal detected: state path is outside base directory');
125
- }
53
+ function generateWorkflowId() {
54
+ const now = new Date();
55
+ const date = now.toISOString().slice(0, 10).replace(/-/g, '');
56
+ const time = now.toISOString().slice(11, 19).replace(/:/g, '');
57
+ const random = crypto.randomBytes(4).toString('hex');
58
+ return `workflow-${date}-${time}-${random}`;
126
59
  }
127
60
 
61
+ // Valid phases for the workflow
128
62
  const PHASES = [
129
63
  'policy-selection',
130
64
  'task-discovery',
@@ -134,702 +68,466 @@ const PHASES = [
134
68
  'user-approval',
135
69
  'implementation',
136
70
  'review-loop',
137
- 'delivery-approval',
138
- 'ship-prep',
139
- 'create-pr',
140
- 'ci-wait',
141
- 'comment-fix',
142
- 'merge',
143
- 'production-ci',
144
- 'deploy',
145
- 'production-release',
71
+ 'delivery-validation',
72
+ 'shipping',
146
73
  'complete'
147
74
  ];
148
75
 
149
- // Pre-computed phase index map for O(1) lookup (vs O(n) array indexOf)
150
- const PHASE_INDEX = new Map(PHASES.map((phase, index) => [phase, index]));
151
-
152
76
  /**
153
- * Check if a phase name is valid (O(1) lookup)
154
- * @param {string} phaseName - Phase to check
155
- * @returns {boolean} True if valid phase
77
+ * Ensure .claude directory exists
156
78
  */
157
- function isValidPhase(phaseName) {
158
- return PHASE_INDEX.has(phaseName);
159
- }
160
-
161
- /**
162
- * Get the index of a phase (O(1) lookup)
163
- * @param {string} phaseName - Phase name
164
- * @returns {number} Phase index or -1 if invalid
165
- */
166
- function getPhaseIndex(phaseName) {
167
- return PHASE_INDEX.has(phaseName) ? PHASE_INDEX.get(phaseName) : -1;
79
+ function ensureClaudeDir(basePath) {
80
+ const claudeDir = path.join(basePath, CLAUDE_DIR);
81
+ if (!fs.existsSync(claudeDir)) {
82
+ fs.mkdirSync(claudeDir, { recursive: true });
83
+ }
84
+ return claudeDir;
168
85
  }
169
86
 
170
- const DEFAULT_POLICY = {
171
- taskSource: 'gh-issues',
172
- priorityFilter: 'continue',
173
- platform: 'detected',
174
- stoppingPoint: 'merged',
175
- mergeStrategy: 'squash',
176
- autoFix: true,
177
- maxReviewIterations: 3
178
- };
87
+ // =============================================================================
88
+ // TASKS.JSON - Main project directory
89
+ // =============================================================================
179
90
 
180
91
  /**
181
- * Generate a unique workflow ID
182
- * @returns {string} Workflow ID in format: workflow-YYYYMMDD-HHMMSS-random
92
+ * Get path to tasks.json with validation
183
93
  */
184
- function generateWorkflowId() {
185
- const now = new Date();
186
- const date = now.toISOString().slice(0, 10).replace(/-/g, '');
187
- const time = now.toISOString().slice(11, 19).replace(/:/g, '');
188
- const random = crypto.randomBytes(4).toString('hex');
189
- return `workflow-${date}-${time}-${random}`;
94
+ function getTasksPath(projectPath = process.cwd()) {
95
+ const validatedBase = validatePath(projectPath);
96
+ const tasksPath = path.join(validatedBase, CLAUDE_DIR, TASKS_FILE);
97
+ validatePathWithinBase(tasksPath, validatedBase);
98
+ return tasksPath;
190
99
  }
191
100
 
192
101
  /**
193
- * Get the state file path with validation
194
- * @param {string} [baseDir=process.cwd()] - Base directory
195
- * @returns {string} Full path to state file
196
- * @throws {Error} If path validation fails
102
+ * Read tasks.json from main project
103
+ * Returns { active: null } if file doesn't exist or is corrupted
104
+ * Logs critical error on corruption to prevent silent data loss
197
105
  */
198
- function getStatePath(baseDir = process.cwd()) {
199
- const validatedBase = validateBasePath(baseDir);
200
- const statePath = path.join(validatedBase, STATE_DIR, STATE_FILE);
201
-
202
- // Verify the state path is still within the base directory
203
- validateStatePathWithinBase(statePath, validatedBase);
204
-
205
- return statePath;
106
+ function readTasks(projectPath = process.cwd()) {
107
+ const tasksPath = getTasksPath(projectPath);
108
+ if (!fs.existsSync(tasksPath)) {
109
+ return { active: null };
110
+ }
111
+ try {
112
+ const data = JSON.parse(fs.readFileSync(tasksPath, 'utf8'));
113
+ // Normalize legacy format that may not have 'active' field
114
+ if (!Object.prototype.hasOwnProperty.call(data, 'active')) {
115
+ return { active: null };
116
+ }
117
+ return data;
118
+ } catch (e) {
119
+ console.error(`[CRITICAL] Corrupted tasks.json at ${tasksPath}: ${e.message}`);
120
+ return { active: null };
121
+ }
206
122
  }
207
123
 
208
124
  /**
209
- * Ensure state directory exists with validation
210
- * @param {string} [baseDir=process.cwd()] - Base directory
211
- * @throws {Error} If path validation fails
125
+ * Write tasks.json to main project
212
126
  */
213
- function ensureStateDir(baseDir = process.cwd()) {
214
- const validatedBase = validateBasePath(baseDir);
215
- const stateDir = path.join(validatedBase, STATE_DIR);
216
-
217
- // Verify the state dir path is still within the base directory
218
- validateStatePathWithinBase(stateDir, validatedBase);
219
-
220
- if (!fs.existsSync(stateDir)) {
221
- fs.mkdirSync(stateDir, { recursive: true });
222
- }
127
+ function writeTasks(tasks, projectPath = process.cwd()) {
128
+ ensureClaudeDir(projectPath);
129
+ const tasksPath = getTasksPath(projectPath);
130
+ fs.writeFileSync(tasksPath, JSON.stringify(tasks, null, 2), 'utf8');
131
+ return true;
223
132
  }
224
133
 
225
134
  /**
226
- * Create a new workflow state
227
- * @param {string} [type='next-task'] - Workflow type
228
- * @param {Object} [policy={}] - Policy overrides
229
- * @returns {Object} New workflow state
135
+ * Set active task in main project
230
136
  */
231
- function createState(type = 'next-task', policy = {}) {
232
- const now = new Date().toISOString();
233
-
234
- return {
235
- version: SCHEMA_VERSION,
236
- workflow: {
237
- id: generateWorkflowId(),
238
- type,
239
- status: 'pending',
240
- startedAt: now,
241
- lastUpdatedAt: now,
242
- completedAt: null
243
- },
244
- policy: { ...DEFAULT_POLICY, ...policy },
245
- task: null,
246
- git: null,
247
- pr: null,
248
- phases: {
249
- current: 'policy-selection',
250
- currentIteration: 0,
251
- history: []
252
- },
253
- agents: null,
254
- checkpoints: {
255
- canResume: true,
256
- resumeFrom: null,
257
- resumeContext: null
258
- },
259
- metrics: {
260
- totalDuration: 0,
261
- tokensUsed: 0,
262
- toolCalls: 0,
263
- filesModified: 0,
264
- linesAdded: 0,
265
- linesRemoved: 0
266
- }
137
+ function setActiveTask(task, projectPath = process.cwd()) {
138
+ const tasks = readTasks(projectPath);
139
+ tasks.active = {
140
+ ...task,
141
+ startedAt: new Date().toISOString()
267
142
  };
143
+ return writeTasks(tasks, projectPath);
268
144
  }
269
145
 
270
146
  /**
271
- * Read workflow state from file (with caching for rapid successive reads)
272
- * @param {string} [baseDir=process.cwd()] - Base directory
273
- * @param {Object} [options={}] - Options
274
- * @param {boolean} [options.skipCache=false] - Skip cache and read from file
275
- * @returns {Object|Error|null} Workflow state, Error if corrupted, or null if not found
147
+ * Clear active task
276
148
  */
277
- function readState(baseDir = process.cwd(), options = {}) {
278
- const statePath = getStatePath(baseDir);
279
- const cacheKey = path.resolve(baseDir);
280
-
281
- // Check cache first (unless skipCache is true)
282
- if (!options.skipCache) {
283
- const cached = getCachedState(cacheKey);
284
- if (cached !== null) {
285
- // Return a deep copy to prevent mutations affecting cache
286
- return JSON.parse(JSON.stringify(cached));
287
- }
288
- }
289
-
290
- if (!fs.existsSync(statePath)) {
291
- return null;
292
- }
293
-
294
- try {
295
- const content = fs.readFileSync(statePath, 'utf8');
296
- const state = JSON.parse(content);
297
-
298
- // Version check
299
- if (state.version !== SCHEMA_VERSION) {
300
- console.warn(`State version mismatch: ${state.version} vs ${SCHEMA_VERSION}`);
301
- // Future: Add migration logic here
302
- }
303
-
304
- // Cache the state
305
- setCachedState(cacheKey, state);
306
-
307
- return state;
308
- } catch (error) {
309
- const corrupted = new Error(`Corrupted workflow state: ${error.message}`);
310
- corrupted.code = 'ERR_STATE_CORRUPTED';
311
- corrupted.cause = error;
312
- console.error(corrupted.message);
313
- return corrupted;
314
- }
149
+ function clearActiveTask(projectPath = process.cwd()) {
150
+ const tasks = readTasks(projectPath);
151
+ tasks.active = null;
152
+ return writeTasks(tasks, projectPath);
315
153
  }
316
154
 
317
155
  /**
318
- * Write workflow state to file (invalidates cache)
319
- * @param {Object} state - Workflow state
320
- * @param {string} [baseDir=process.cwd()] - Base directory
321
- * @returns {boolean} Success status
156
+ * Check if there's an active task
157
+ * Uses != null to catch both null and undefined (legacy format safety)
322
158
  */
323
- function writeState(state, baseDir = process.cwd()) {
324
- ensureStateDir(baseDir);
325
- const statePath = getStatePath(baseDir);
326
- const cacheKey = path.resolve(baseDir);
327
-
328
- try {
329
- // Update timestamp
330
- state.workflow.lastUpdatedAt = new Date().toISOString();
331
-
332
- const content = JSON.stringify(state, null, 2);
333
- fs.writeFileSync(statePath, content, 'utf8');
159
+ function hasActiveTask(projectPath = process.cwd()) {
160
+ const tasks = readTasks(projectPath);
161
+ return tasks.active != null;
162
+ }
334
163
 
335
- // Update cache with new state
336
- setCachedState(cacheKey, state);
164
+ // =============================================================================
165
+ // FLOW.JSON - Worktree directory
166
+ // =============================================================================
337
167
 
338
- return true;
339
- } catch (error) {
340
- // Invalidate cache on write error
341
- invalidateStateCache(cacheKey);
342
- console.error(`Error writing state: ${error.message}`);
343
- return false;
344
- }
168
+ /**
169
+ * Get path to flow.json with validation
170
+ */
171
+ function getFlowPath(worktreePath = process.cwd()) {
172
+ const validatedBase = validatePath(worktreePath);
173
+ const flowPath = path.join(validatedBase, CLAUDE_DIR, FLOW_FILE);
174
+ validatePathWithinBase(flowPath, validatedBase);
175
+ return flowPath;
345
176
  }
346
177
 
347
178
  /**
348
- * Update specific fields in workflow state
349
- * @param {Object} updates - Fields to update (deep merge)
350
- * @param {string} [baseDir=process.cwd()] - Base directory
351
- * @returns {Object|null} Updated state or null on error
179
+ * Read flow.json from worktree
180
+ * Returns null if file doesn't exist or is corrupted
181
+ * Logs critical error on corruption to prevent silent data loss
352
182
  */
353
- function updateState(updates, baseDir = process.cwd()) {
354
- let state = readState(baseDir);
355
-
356
- if (state instanceof Error) {
357
- console.error(`Cannot update state: ${state.message}`);
183
+ function readFlow(worktreePath = process.cwd()) {
184
+ const flowPath = getFlowPath(worktreePath);
185
+ if (!fs.existsSync(flowPath)) {
358
186
  return null;
359
187
  }
360
- if (!state) {
361
- console.error('No existing state to update');
188
+ try {
189
+ return JSON.parse(fs.readFileSync(flowPath, 'utf8'));
190
+ } catch (e) {
191
+ console.error(`[CRITICAL] Corrupted flow.json at ${flowPath}: ${e.message}`);
362
192
  return null;
363
193
  }
364
-
365
- // Deep merge updates
366
- state = deepMerge(state, updates);
367
-
368
- if (writeState(state, baseDir)) {
369
- return state;
370
- }
371
-
372
- return null;
373
194
  }
374
195
 
375
196
  /**
376
- * Maximum recursion depth for deepMerge to prevent stack overflow attacks
197
+ * Write flow.json to worktree
198
+ * Creates a copy to avoid mutating the original object
377
199
  */
378
- const MAX_MERGE_DEPTH = 50;
200
+ function writeFlow(flow, worktreePath = process.cwd()) {
201
+ ensureClaudeDir(worktreePath);
202
+ // Clone to avoid mutating the original object
203
+ const flowCopy = JSON.parse(JSON.stringify(flow));
204
+ flowCopy.lastUpdate = new Date().toISOString();
205
+ const flowPath = getFlowPath(worktreePath);
206
+ fs.writeFileSync(flowPath, JSON.stringify(flowCopy, null, 2), 'utf8');
207
+ return true;
208
+ }
379
209
 
380
210
  /**
381
- * Deep merge two objects (with prototype pollution and stack overflow protection)
382
- * @param {Object} target - Target object
383
- * @param {Object} source - Source object
384
- * @param {number} [depth=0] - Current recursion depth (internal)
385
- * @returns {Object} Merged object
386
- * @throws {Error} If recursion depth exceeds MAX_MERGE_DEPTH
211
+ * Update flow.json with partial updates
212
+ * Handles null values correctly (null overwrites existing values)
213
+ * Deep merges nested objects when both exist
387
214
  */
388
- function deepMerge(target, source, depth = 0) {
389
- // Protect against stack overflow from deeply nested objects
390
- if (depth > MAX_MERGE_DEPTH) {
391
- throw new Error(`Maximum merge depth (${MAX_MERGE_DEPTH}) exceeded - possible circular reference or attack`);
392
- }
393
-
394
- // Handle null/undefined cases
395
- if (!source || typeof source !== 'object') return target;
396
- if (!target || typeof target !== 'object') return source;
215
+ function updateFlow(updates, worktreePath = process.cwd()) {
216
+ const flow = readFlow(worktreePath) || {};
397
217
 
398
- const result = { ...target };
399
-
400
- for (const key of Object.keys(source)) {
401
- // Protect against prototype pollution
402
- if (key === '__proto__' || key === 'constructor' || key === 'prototype') {
403
- continue;
218
+ for (const [key, value] of Object.entries(updates)) {
219
+ // Null explicitly overwrites
220
+ if (value === null) {
221
+ flow[key] = null;
404
222
  }
405
-
406
- const sourceVal = source[key];
407
- const targetVal = result[key];
408
-
409
- // Handle Date objects - preserve as-is
410
- if (sourceVal instanceof Date) {
411
- result[key] = new Date(sourceVal.getTime());
223
+ // Deep merge if both source and target are non-null objects
224
+ else if (
225
+ value && typeof value === 'object' && !Array.isArray(value) &&
226
+ flow[key] && typeof flow[key] === 'object' && !Array.isArray(flow[key])
227
+ ) {
228
+ flow[key] = { ...flow[key], ...value };
412
229
  }
413
- // Handle null explicitly - allow overwriting with null
414
- else if (sourceVal === null) {
415
- result[key] = null;
416
- }
417
- // Recursively merge plain objects (with depth tracking)
418
- else if (sourceVal && typeof sourceVal === 'object' && !Array.isArray(sourceVal)) {
419
- result[key] = deepMerge(targetVal || {}, sourceVal, depth + 1);
420
- }
421
- // Replace arrays and primitives
230
+ // Otherwise direct assignment
422
231
  else {
423
- result[key] = sourceVal;
232
+ flow[key] = value;
424
233
  }
425
234
  }
426
235
 
427
- return result;
236
+ return writeFlow(flow, worktreePath);
428
237
  }
429
238
 
430
239
  /**
431
- * Start a new phase
432
- * @param {string} phaseName - Phase name
433
- * @param {string} [baseDir=process.cwd()] - Base directory
434
- * @returns {Object|null} Updated state or null on error
240
+ * Create initial flow for a new task
241
+ * Also registers the task as active in the main project's tasks.json
242
+ * @param {Object} task - Task object with id, title, source, url
243
+ * @param {Object} policy - Policy object with stoppingPoint
244
+ * @param {string} worktreePath - Path to worktree
245
+ * @param {string} projectPath - Path to main project (for tasks.json registration)
435
246
  */
436
- function startPhase(phaseName, baseDir = process.cwd()) {
437
- if (!isValidPhase(phaseName)) {
438
- console.error(`Invalid phase: ${phaseName}`);
439
- return null;
440
- }
247
+ function createFlow(task, policy, worktreePath = process.cwd(), projectPath = null) {
248
+ const flow = {
249
+ task: {
250
+ id: task.id,
251
+ title: task.title,
252
+ source: task.source,
253
+ url: task.url || null
254
+ },
255
+ policy: {
256
+ stoppingPoint: policy.stoppingPoint || 'merged'
257
+ },
258
+ phase: 'policy-selection',
259
+ status: 'in_progress',
260
+ lastUpdate: new Date().toISOString(),
261
+ userNotes: '',
262
+ git: {
263
+ branch: null,
264
+ baseBranch: 'main'
265
+ },
266
+ pr: null,
267
+ exploration: null,
268
+ plan: null,
269
+ // Store projectPath so completeWorkflow knows where to clear the task
270
+ projectPath: projectPath
271
+ };
441
272
 
442
- const state = readState(baseDir);
443
- if (state instanceof Error) {
444
- console.error(`Cannot start phase: ${state.message}`);
445
- return null;
446
- }
447
- if (!state) {
448
- console.error('No workflow state exists. Create a workflow first.');
449
- return null;
450
- }
273
+ writeFlow(flow, worktreePath);
451
274
 
452
- const history = state.phases?.history || [];
275
+ // Register task as active in main project
276
+ if (projectPath) {
277
+ setActiveTask({
278
+ taskId: task.id,
279
+ title: task.title,
280
+ worktree: worktreePath,
281
+ branch: flow.git.branch
282
+ }, projectPath);
283
+ }
453
284
 
454
- history.push({
455
- phase: phaseName,
456
- status: 'in_progress',
457
- startedAt: new Date().toISOString(),
458
- completedAt: null,
459
- duration: null,
460
- result: null
461
- });
462
-
463
- return updateState({
464
- workflow: { status: 'in_progress' },
465
- phases: { current: phaseName, history },
466
- checkpoints: { canResume: true, resumeFrom: phaseName, resumeContext: null }
467
- }, baseDir);
285
+ return flow;
468
286
  }
469
287
 
470
288
  /**
471
- * Update the current phase entry with completion data
472
- * @param {Object} state - Current state
473
- * @param {string} status - New status (completed/failed)
474
- * @param {Object} result - Result data
475
- * @returns {Object} Updated history
289
+ * Delete flow.json
476
290
  */
477
- function finalizePhaseEntry(state, status, result) {
478
- const history = state.phases.history || [];
479
- const entry = history[history.length - 1];
480
-
481
- if (entry) {
482
- const now = new Date().toISOString();
483
- entry.status = status;
484
- entry.completedAt = now;
485
- entry.duration = new Date(now).getTime() - new Date(entry.startedAt).getTime();
486
- entry.result = result;
291
+ function deleteFlow(worktreePath = process.cwd()) {
292
+ const flowPath = getFlowPath(worktreePath);
293
+ if (fs.existsSync(flowPath)) {
294
+ fs.unlinkSync(flowPath);
295
+ return true;
487
296
  }
297
+ return false;
298
+ }
299
+
300
+ // =============================================================================
301
+ // PHASE MANAGEMENT
302
+ // =============================================================================
488
303
 
489
- return history;
304
+ /**
305
+ * Check if phase is valid
306
+ */
307
+ function isValidPhase(phase) {
308
+ return PHASES.includes(phase);
490
309
  }
491
310
 
492
311
  /**
493
- * Complete the current phase
494
- * @param {Object} [result={}] - Phase result data
495
- * @param {string} [baseDir=process.cwd()] - Base directory
496
- * @returns {Object|null} Updated state or null on error
312
+ * Set current phase
497
313
  */
498
- function completePhase(result = {}, baseDir = process.cwd()) {
499
- const state = readState(baseDir);
500
- if (state instanceof Error) {
501
- console.error(`Cannot complete phase: ${state.message}`);
502
- return null;
314
+ function setPhase(phase, worktreePath = process.cwd()) {
315
+ if (!isValidPhase(phase)) {
316
+ throw new Error(`Invalid phase: ${phase}`);
503
317
  }
504
- if (!state) return null;
505
-
506
- const history = finalizePhaseEntry(state, 'completed', result);
507
- const currentIndex = getPhaseIndex(state.phases.current);
508
- const nextPhase = currentIndex < PHASES.length - 1 ? PHASES[currentIndex + 1] : 'complete';
318
+ return updateFlow({ phase, status: 'in_progress' }, worktreePath);
319
+ }
509
320
 
510
- return updateState({
511
- phases: { current: nextPhase, history },
512
- checkpoints: { resumeFrom: nextPhase, resumeContext: null }
513
- }, baseDir);
321
+ /**
322
+ * Start a phase (alias for setPhase, for backwards compatibility)
323
+ */
324
+ function startPhase(phase, worktreePath = process.cwd()) {
325
+ return setPhase(phase, worktreePath);
514
326
  }
515
327
 
516
328
  /**
517
329
  * Fail the current phase
518
- * @param {string} reason - Failure reason
519
- * @param {Object} [context={}] - Context for resume
520
- * @param {string} [baseDir=process.cwd()] - Base directory
521
- * @returns {Object|null} Updated state or null on error
522
330
  */
523
- function failPhase(reason, context = {}, baseDir = process.cwd()) {
524
- const state = readState(baseDir);
525
- if (state instanceof Error) {
526
- console.error(`Cannot fail phase: ${state.message}`);
527
- return null;
528
- }
529
- if (!state) return null;
530
-
531
- const history = finalizePhaseEntry(state, 'failed', { error: reason });
532
-
533
- return updateState({
534
- workflow: { status: 'failed' },
535
- phases: { history },
536
- checkpoints: {
537
- canResume: true,
538
- resumeFrom: state.phases.current,
539
- resumeContext: { reason, ...context }
540
- }
541
- }, baseDir);
331
+ function failPhase(reason, context = {}, worktreePath = process.cwd()) {
332
+ const flow = readFlow(worktreePath);
333
+ if (!flow) return null;
334
+
335
+ return updateFlow({
336
+ status: 'failed',
337
+ error: reason,
338
+ failContext: context
339
+ }, worktreePath);
542
340
  }
543
341
 
544
342
  /**
545
343
  * Skip to a specific phase
546
- * @param {string} phaseName - Phase to skip to
547
- * @param {string} [reason='manual skip'] - Skip reason
548
- * @param {string} [baseDir=process.cwd()] - Base directory
549
- * @returns {Object|null} Updated state or null on error
550
344
  */
551
- function skipToPhase(phaseName, reason = 'manual skip', baseDir = process.cwd()) {
552
- if (!isValidPhase(phaseName)) {
553
- console.error(`Invalid phase: ${phaseName}`);
554
- return null;
555
- }
556
-
557
- const state = readState(baseDir);
558
- if (state instanceof Error) {
559
- console.error(`Cannot skip to phase: ${state.message}`);
560
- return null;
561
- }
562
- if (!state) return null;
563
-
564
- const currentIndex = getPhaseIndex(state.phases.current);
565
- const targetIndex = getPhaseIndex(phaseName);
566
-
567
- // Add skipped entries for phases we're jumping over
568
- const history = [...(state.phases.history || [])];
569
- const now = new Date().toISOString();
570
-
571
- for (let i = currentIndex; i < targetIndex; i++) {
572
- history.push({
573
- phase: PHASES[i],
574
- status: 'skipped',
575
- startedAt: now,
576
- completedAt: now,
577
- duration: 0,
578
- result: { skippedReason: reason }
579
- });
345
+ function skipToPhase(phase, reason = 'manual skip', worktreePath = process.cwd()) {
346
+ if (!isValidPhase(phase)) {
347
+ throw new Error(`Invalid phase: ${phase}`);
580
348
  }
581
-
582
- return updateState({
583
- phases: {
584
- current: phaseName,
585
- history
586
- },
587
- checkpoints: {
588
- resumeFrom: phaseName
589
- }
590
- }, baseDir);
349
+ return updateFlow({
350
+ phase,
351
+ status: 'in_progress',
352
+ skipReason: reason
353
+ }, worktreePath);
591
354
  }
592
355
 
593
356
  /**
594
- * Complete the entire workflow
595
- * @param {Object} [result={}] - Final result data
596
- * @param {string} [baseDir=process.cwd()] - Base directory
597
- * @returns {Object|null} Updated state or null on error
357
+ * Complete current phase and move to next
358
+ * Uses updateFlow pattern to avoid direct mutation issues
598
359
  */
599
- function completeWorkflow(result = {}, baseDir = process.cwd()) {
600
- const state = readState(baseDir);
601
- if (state instanceof Error) {
602
- console.error(`Cannot complete workflow: ${state.message}`);
603
- return null;
604
- }
605
- if (!state) return null;
360
+ function completePhase(result = null, worktreePath = process.cwd()) {
361
+ const flow = readFlow(worktreePath);
362
+ if (!flow) return null;
606
363
 
607
- const now = new Date().toISOString();
608
- const startTime = new Date(state.workflow.startedAt).getTime();
609
- const endTime = new Date(now).getTime();
364
+ const currentIndex = PHASES.indexOf(flow.phase);
365
+ const nextPhase = PHASES[currentIndex + 1] || 'complete';
610
366
 
611
- return updateState({
612
- workflow: {
613
- status: 'completed',
614
- completedAt: now
615
- },
616
- phases: {
617
- current: 'complete'
618
- },
619
- checkpoints: {
620
- canResume: false,
621
- resumeFrom: null
622
- },
623
- metrics: {
624
- totalDuration: endTime - startTime,
625
- ...result.metrics
367
+ // Build updates object
368
+ const updates = {
369
+ phase: nextPhase,
370
+ status: nextPhase === 'complete' ? 'completed' : 'in_progress'
371
+ };
372
+
373
+ // Store result in appropriate field
374
+ if (result) {
375
+ const resultField = getResultField(flow.phase);
376
+ if (resultField) {
377
+ updates[resultField] = result;
626
378
  }
627
- }, baseDir);
379
+ }
380
+
381
+ updateFlow(updates, worktreePath);
382
+ return readFlow(worktreePath);
628
383
  }
629
384
 
630
385
  /**
631
- * Abort the workflow
632
- * @param {string} [reason='user aborted'] - Abort reason
633
- * @param {string} [baseDir=process.cwd()] - Base directory
634
- * @returns {Object|null} Updated state or null on error
386
+ * Map phase to result field
635
387
  */
636
- function abortWorkflow(reason = 'user aborted', baseDir = process.cwd()) {
637
- return updateState({
638
- workflow: {
639
- status: 'aborted',
640
- completedAt: new Date().toISOString()
641
- },
642
- checkpoints: {
643
- canResume: false,
644
- resumeFrom: null,
645
- resumeContext: { abortReason: reason }
646
- }
647
- }, baseDir);
388
+ function getResultField(phase) {
389
+ const mapping = {
390
+ 'exploration': 'exploration',
391
+ 'planning': 'plan',
392
+ 'review-loop': 'reviewResult'
393
+ };
394
+ return mapping[phase] || null;
648
395
  }
649
396
 
650
397
  /**
651
- * Delete workflow state (cleanup)
652
- * @param {string} [baseDir=process.cwd()] - Base directory
653
- * @returns {boolean} Success status
398
+ * Mark workflow as failed
654
399
  */
655
- function deleteState(baseDir = process.cwd()) {
656
- const statePath = getStatePath(baseDir);
657
-
658
- try {
659
- if (fs.existsSync(statePath)) {
660
- fs.unlinkSync(statePath);
661
- }
662
- return true;
663
- } catch (error) {
664
- console.error(`Error deleting state: ${error.message}`);
665
- return false;
666
- }
400
+ function failWorkflow(error, worktreePath = process.cwd()) {
401
+ return updateFlow({
402
+ status: 'failed',
403
+ error: error?.message || String(error)
404
+ }, worktreePath);
667
405
  }
668
406
 
669
407
  /**
670
- * Check if a workflow is in progress
671
- * @param {string} [baseDir=process.cwd()] - Base directory
672
- * @returns {boolean} True if workflow is active
408
+ * Mark workflow as complete
409
+ * Automatically clears the active task from tasks.json using stored projectPath
410
+ * @param {string} worktreePath - Path to worktree
673
411
  */
674
- function hasActiveWorkflow(baseDir = process.cwd()) {
675
- const state = readState(baseDir);
676
- if (state instanceof Error) return false;
677
- if (!state) return false;
412
+ function completeWorkflow(worktreePath = process.cwd()) {
413
+ const flow = readFlow(worktreePath);
414
+
415
+ // Clear active task from main project if projectPath is stored
416
+ if (flow && flow.projectPath) {
417
+ clearActiveTask(flow.projectPath);
418
+ }
678
419
 
679
- return ['pending', 'in_progress', 'paused'].includes(state.workflow.status);
420
+ return updateFlow({
421
+ phase: 'complete',
422
+ status: 'completed',
423
+ completedAt: new Date().toISOString()
424
+ }, worktreePath);
680
425
  }
681
426
 
682
427
  /**
683
- * Get workflow summary for display
684
- * @param {string} [baseDir=process.cwd()] - Base directory
685
- * @returns {Object|null} Summary object or null
428
+ * Abort workflow
429
+ * Also clears the active task from tasks.json using stored projectPath
686
430
  */
687
- function getWorkflowSummary(baseDir = process.cwd()) {
688
- const state = readState(baseDir);
689
- if (state instanceof Error) {
690
- return { error: state.message, code: state.code };
691
- }
692
- if (!state) return null;
431
+ function abortWorkflow(reason, worktreePath = process.cwd()) {
432
+ const flow = readFlow(worktreePath);
693
433
 
694
- const completedPhases = state.phases.history?.filter(p => p.status === 'completed').length || 0;
695
- const totalPhases = PHASES.length - 1; // Exclude 'complete'
434
+ // Clear active task from main project if projectPath is stored
435
+ if (flow && flow.projectPath) {
436
+ clearActiveTask(flow.projectPath);
437
+ }
696
438
 
697
- return {
698
- id: state.workflow.id,
699
- type: state.workflow.type,
700
- status: state.workflow.status,
701
- currentPhase: state.phases.current,
702
- progress: `${completedPhases}/${totalPhases}`,
703
- progressPercent: Math.round((completedPhases / totalPhases) * 100),
704
- task: state.task ? {
705
- id: state.task.id,
706
- title: state.task.title,
707
- source: state.task.source
708
- } : null,
709
- pr: state.pr ? {
710
- number: state.pr.number,
711
- url: state.pr.url,
712
- ciStatus: state.pr.ciStatus
713
- } : null,
714
- canResume: state.checkpoints.canResume,
715
- resumeFrom: state.checkpoints.resumeFrom,
716
- startedAt: state.workflow.startedAt,
717
- duration: state.metrics?.totalDuration || 0
718
- };
439
+ return updateFlow({
440
+ status: 'aborted',
441
+ abortReason: reason,
442
+ abortedAt: new Date().toISOString()
443
+ }, worktreePath);
719
444
  }
720
445
 
446
+ // =============================================================================
447
+ // CONVENIENCE FUNCTIONS
448
+ // =============================================================================
449
+
721
450
  /**
722
- * Update agent results
723
- * @param {string} agentName - Agent identifier
724
- * @param {Object} result - Agent result
725
- * @param {string} [baseDir=process.cwd()] - Base directory
726
- * @returns {Object|null} Updated state or null on error
451
+ * Get workflow summary for display
727
452
  */
728
- function updateAgentResult(agentName, result, baseDir = process.cwd()) {
729
- const state = readState(baseDir);
730
- if (state instanceof Error) {
731
- console.error(`Cannot update agent result: ${state.message}`);
732
- return null;
733
- }
734
- if (!state) return null;
453
+ function getFlowSummary(worktreePath = process.cwd()) {
454
+ const flow = readFlow(worktreePath);
455
+ if (!flow) return null;
735
456
 
736
- const agents = state.agents || {
737
- lastRun: {},
738
- totalIterations: 0,
739
- totalIssuesFound: 0,
740
- totalIssuesFixed: 0
457
+ return {
458
+ task: flow.task?.title || 'Unknown',
459
+ taskId: flow.task?.id,
460
+ phase: flow.phase,
461
+ status: flow.status,
462
+ lastUpdate: flow.lastUpdate,
463
+ pr: flow.pr?.number ? `#${flow.pr.number}` : null
741
464
  };
742
-
743
- agents.lastRun[agentName] = result;
744
- agents.totalIssuesFound += result.issues || 0;
745
-
746
- return updateState({ agents }, baseDir);
747
465
  }
748
466
 
749
467
  /**
750
- * Increment review iteration
751
- * @param {Object} [result={}] - Iteration result
752
- * @param {string} [baseDir=process.cwd()] - Base directory
753
- * @returns {Object|null} Updated state or null on error
468
+ * Check if workflow can be resumed
754
469
  */
755
- function incrementIteration(result = {}, baseDir = process.cwd()) {
756
- const state = readState(baseDir);
757
- if (state instanceof Error) {
758
- console.error(`Cannot increment iteration: ${state.message}`);
759
- return null;
760
- }
761
- if (!state) return null;
762
-
763
- const agents = state.agents || {
764
- lastRun: {},
765
- totalIterations: 0,
766
- totalIssuesFound: 0,
767
- totalIssuesFixed: 0
768
- };
470
+ function canResume(worktreePath = process.cwd()) {
471
+ const flow = readFlow(worktreePath);
472
+ if (!flow) return false;
473
+ return flow.status === 'in_progress' && flow.phase !== 'complete';
474
+ }
769
475
 
770
- agents.totalIterations += 1;
771
- agents.totalIssuesFixed += result.fixed || 0;
476
+ // =============================================================================
477
+ // BACKWARDS COMPATIBILITY ALIASES
478
+ // =============================================================================
772
479
 
773
- return updateState({
774
- phases: {
775
- currentIteration: state.phases.currentIteration + 1
776
- },
777
- agents
778
- }, baseDir);
779
- }
480
+ // These maintain compatibility with existing agent code
481
+ const readState = readFlow;
482
+ const writeState = writeFlow;
483
+ const updateState = updateFlow;
484
+ const createState = (type, policy) => createFlow({ id: 'manual', title: 'Manual task', source: 'manual' }, policy);
485
+ const deleteState = deleteFlow;
486
+ const hasActiveWorkflow = hasActiveTask;
487
+ const getWorkflowSummary = getFlowSummary;
780
488
 
781
- // Export all functions
782
489
  module.exports = {
783
490
  // Constants
784
- SCHEMA_VERSION,
785
491
  PHASES,
786
- PHASE_INDEX,
787
- DEFAULT_POLICY,
788
- MAX_MERGE_DEPTH,
789
-
790
- // Phase helpers (O(1) lookup)
791
- isValidPhase,
792
- getPhaseIndex,
793
492
 
794
- // Core functions
795
- generateWorkflowId,
796
- getStatePath,
797
- ensureStateDir,
798
-
799
- // CRUD operations
800
- createState,
801
- readState,
802
- writeState,
803
- updateState,
804
- deleteState,
493
+ // Tasks (main project)
494
+ getTasksPath,
495
+ readTasks,
496
+ writeTasks,
497
+ setActiveTask,
498
+ clearActiveTask,
499
+ hasActiveTask,
500
+
501
+ // Flow (worktree)
502
+ getFlowPath,
503
+ readFlow,
504
+ writeFlow,
505
+ updateFlow,
506
+ createFlow,
507
+ deleteFlow,
805
508
 
806
509
  // Phase management
510
+ isValidPhase,
511
+ setPhase,
807
512
  startPhase,
808
513
  completePhase,
809
514
  failPhase,
810
515
  skipToPhase,
811
-
812
- // Workflow lifecycle
516
+ failWorkflow,
813
517
  completeWorkflow,
814
518
  abortWorkflow,
519
+
520
+ // Convenience
521
+ getFlowSummary,
522
+ canResume,
523
+ generateWorkflowId,
524
+
525
+ // Backwards compatibility
526
+ readState,
527
+ writeState,
528
+ updateState,
529
+ createState,
530
+ deleteState,
815
531
  hasActiveWorkflow,
816
- getWorkflowSummary,
817
-
818
- // Agent management
819
- updateAgentResult,
820
- incrementIteration,
821
-
822
- // Cache management
823
- clearAllStateCaches,
824
-
825
- // Internal functions for testing
826
- _internal: {
827
- validateBasePath,
828
- validateStatePathWithinBase,
829
- deepMerge,
830
- getCachedState,
831
- setCachedState,
832
- invalidateStateCache,
833
- STATE_CACHE_TTL_MS
834
- }
532
+ getWorkflowSummary
835
533
  };