awesome-slash 2.4.3 → 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 (146) hide show
  1. package/.claude-plugin/marketplace.json +6 -6
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/CHANGELOG.md +99 -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/ci-monitor.md +19 -0
  59. package/plugins/next-task/agents/delivery-validator.md +2 -2
  60. package/plugins/next-task/agents/implementation-agent.md +3 -4
  61. package/plugins/next-task/agents/planning-agent.md +77 -19
  62. package/plugins/next-task/agents/review-orchestrator.md +21 -122
  63. package/plugins/next-task/agents/task-discoverer.md +164 -23
  64. package/plugins/next-task/commands/next-task.md +180 -14
  65. package/plugins/next-task/lib/index.js +170 -0
  66. package/plugins/next-task/lib/patterns/review-patterns.js +58 -11
  67. package/plugins/next-task/lib/patterns/slop-patterns.js +170 -129
  68. package/plugins/next-task/lib/platform/detect-platform.js +212 -123
  69. package/plugins/next-task/lib/platform/detection-configs.js +93 -0
  70. package/plugins/next-task/lib/platform/verify-tools.js +10 -1
  71. package/plugins/next-task/lib/schemas/README.md +195 -0
  72. package/plugins/next-task/lib/schemas/validator.js +205 -0
  73. package/plugins/next-task/lib/sources/custom-handler.js +199 -0
  74. package/plugins/next-task/lib/sources/policy-questions.js +239 -0
  75. package/plugins/next-task/lib/sources/source-cache.js +149 -0
  76. package/plugins/next-task/lib/state/workflow-state.js +382 -484
  77. package/plugins/next-task/lib/types/README.md +292 -0
  78. package/plugins/next-task/lib/types/agent-frontmatter.d.ts +134 -0
  79. package/plugins/next-task/lib/types/command-frontmatter.d.ts +107 -0
  80. package/plugins/next-task/lib/types/hook-frontmatter.d.ts +115 -0
  81. package/plugins/next-task/lib/types/index.d.ts +84 -0
  82. package/plugins/next-task/lib/types/plugin-manifest.d.ts +102 -0
  83. package/plugins/next-task/lib/types/skill-frontmatter.d.ts +89 -0
  84. package/plugins/next-task/lib/utils/cache-manager.js +154 -0
  85. package/plugins/next-task/lib/utils/context-optimizer.js +115 -37
  86. package/plugins/next-task/lib/utils/deprecation.js +37 -0
  87. package/plugins/next-task/lib/utils/shell-escape.js +88 -0
  88. package/plugins/project-review/.claude-plugin/plugin.json +1 -1
  89. package/plugins/project-review/lib/index.js +170 -0
  90. package/plugins/project-review/lib/patterns/review-patterns.js +58 -11
  91. package/plugins/project-review/lib/patterns/slop-patterns.js +170 -129
  92. package/plugins/project-review/lib/platform/detect-platform.js +212 -123
  93. package/plugins/project-review/lib/platform/detection-configs.js +93 -0
  94. package/plugins/project-review/lib/platform/verify-tools.js +10 -1
  95. package/plugins/project-review/lib/schemas/README.md +195 -0
  96. package/plugins/project-review/lib/schemas/validator.js +205 -0
  97. package/plugins/project-review/lib/sources/custom-handler.js +199 -0
  98. package/plugins/project-review/lib/sources/policy-questions.js +239 -0
  99. package/plugins/project-review/lib/sources/source-cache.js +149 -0
  100. package/plugins/project-review/lib/state/workflow-state.js +382 -484
  101. package/plugins/project-review/lib/types/README.md +292 -0
  102. package/plugins/project-review/lib/types/agent-frontmatter.d.ts +134 -0
  103. package/plugins/project-review/lib/types/command-frontmatter.d.ts +107 -0
  104. package/plugins/project-review/lib/types/hook-frontmatter.d.ts +115 -0
  105. package/plugins/project-review/lib/types/index.d.ts +84 -0
  106. package/plugins/project-review/lib/types/plugin-manifest.d.ts +102 -0
  107. package/plugins/project-review/lib/types/skill-frontmatter.d.ts +89 -0
  108. package/plugins/project-review/lib/utils/cache-manager.js +154 -0
  109. package/plugins/project-review/lib/utils/context-optimizer.js +115 -37
  110. package/plugins/project-review/lib/utils/deprecation.js +37 -0
  111. package/plugins/project-review/lib/utils/shell-escape.js +88 -0
  112. package/plugins/reality-check/.claude-plugin/plugin.json +1 -1
  113. package/plugins/reality-check/agents/code-explorer.md +1 -1
  114. package/plugins/ship/.claude-plugin/plugin.json +1 -1
  115. package/plugins/ship/commands/ship-ci-review-loop.md +19 -0
  116. package/plugins/ship/lib/index.js +170 -0
  117. package/plugins/ship/lib/patterns/review-patterns.js +58 -11
  118. package/plugins/ship/lib/patterns/slop-patterns.js +170 -129
  119. package/plugins/ship/lib/platform/detect-platform.js +212 -123
  120. package/plugins/ship/lib/platform/detection-configs.js +93 -0
  121. package/plugins/ship/lib/platform/verify-tools.js +10 -1
  122. package/plugins/ship/lib/schemas/README.md +195 -0
  123. package/plugins/ship/lib/schemas/validator.js +205 -0
  124. package/plugins/ship/lib/sources/custom-handler.js +199 -0
  125. package/plugins/ship/lib/sources/policy-questions.js +239 -0
  126. package/plugins/ship/lib/sources/source-cache.js +149 -0
  127. package/plugins/ship/lib/state/workflow-state.js +382 -484
  128. package/plugins/ship/lib/types/README.md +292 -0
  129. package/plugins/ship/lib/types/agent-frontmatter.d.ts +134 -0
  130. package/plugins/ship/lib/types/command-frontmatter.d.ts +107 -0
  131. package/plugins/ship/lib/types/hook-frontmatter.d.ts +115 -0
  132. package/plugins/ship/lib/types/index.d.ts +84 -0
  133. package/plugins/ship/lib/types/plugin-manifest.d.ts +102 -0
  134. package/plugins/ship/lib/types/skill-frontmatter.d.ts +89 -0
  135. package/plugins/ship/lib/utils/cache-manager.js +154 -0
  136. package/plugins/ship/lib/utils/context-optimizer.js +115 -37
  137. package/plugins/ship/lib/utils/deprecation.js +37 -0
  138. package/plugins/ship/lib/utils/shell-escape.js +88 -0
  139. package/lib/state/workflow-state.schema.json +0 -282
  140. package/plugins/deslop-around/lib/state/workflow-state.schema.json +0 -282
  141. package/plugins/next-task/agents/policy-selector.md +0 -248
  142. package/plugins/next-task/lib/state/tasks-registry.schema.json +0 -85
  143. package/plugins/next-task/lib/state/workflow-state.schema.json +0 -282
  144. package/plugins/next-task/lib/state/worktree-status.schema.json +0 -219
  145. package/plugins/project-review/lib/state/workflow-state.schema.json +0 -282
  146. package/plugins/ship/lib/state/workflow-state.schema.json +0 -282
@@ -1,18 +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
 
18
+ /**
19
+ * Validate and resolve path to prevent path traversal attacks
20
+ * @param {string} basePath - Base directory path
21
+ * @returns {string} Validated absolute path
22
+ * @throws {Error} If path is invalid
23
+ */
24
+ function validatePath(basePath) {
25
+ if (typeof basePath !== 'string' || basePath.length === 0) {
26
+ throw new Error('Path must be a non-empty string');
27
+ }
28
+ const resolved = path.resolve(basePath);
29
+ if (resolved.includes('\0')) {
30
+ throw new Error('Path contains invalid null byte');
31
+ }
32
+ return resolved;
33
+ }
34
+
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');
46
+ }
47
+ }
48
+
49
+ /**
50
+ * Generate a unique workflow ID
51
+ * @returns {string} Workflow ID
52
+ */
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}`;
59
+ }
60
+
61
+ // Valid phases for the workflow
16
62
  const PHASES = [
17
63
  'policy-selection',
18
64
  'task-discovery',
@@ -22,614 +68,466 @@ const PHASES = [
22
68
  'user-approval',
23
69
  'implementation',
24
70
  'review-loop',
25
- 'delivery-approval',
26
- 'ship-prep',
27
- 'create-pr',
28
- 'ci-wait',
29
- 'comment-fix',
30
- 'merge',
31
- 'production-ci',
32
- 'deploy',
33
- 'production-release',
71
+ 'delivery-validation',
72
+ 'shipping',
34
73
  'complete'
35
74
  ];
36
75
 
37
- const DEFAULT_POLICY = {
38
- taskSource: 'gh-issues',
39
- priorityFilter: 'continue',
40
- platform: 'detected',
41
- stoppingPoint: 'merged',
42
- mergeStrategy: 'squash',
43
- autoFix: true,
44
- maxReviewIterations: 3
45
- };
46
-
47
76
  /**
48
- * Generate a unique workflow ID
49
- * @returns {string} Workflow ID in format: workflow-YYYYMMDD-HHMMSS-random
77
+ * Ensure .claude directory exists
50
78
  */
51
- function generateWorkflowId() {
52
- const now = new Date();
53
- const date = now.toISOString().slice(0, 10).replace(/-/g, '');
54
- const time = now.toISOString().slice(11, 19).replace(/:/g, '');
55
- const random = crypto.randomBytes(4).toString('hex');
56
- return `workflow-${date}-${time}-${random}`;
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;
57
85
  }
58
86
 
87
+ // =============================================================================
88
+ // TASKS.JSON - Main project directory
89
+ // =============================================================================
90
+
59
91
  /**
60
- * Get the state file path
61
- * @param {string} [baseDir=process.cwd()] - Base directory
62
- * @returns {string} Full path to state file
92
+ * Get path to tasks.json with validation
63
93
  */
64
- function getStatePath(baseDir = process.cwd()) {
65
- return path.join(baseDir, STATE_DIR, STATE_FILE);
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;
66
99
  }
67
100
 
68
101
  /**
69
- * Ensure state directory exists
70
- * @param {string} [baseDir=process.cwd()] - Base directory
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
71
105
  */
72
- function ensureStateDir(baseDir = process.cwd()) {
73
- const stateDir = path.join(baseDir, STATE_DIR);
74
- if (!fs.existsSync(stateDir)) {
75
- fs.mkdirSync(stateDir, { recursive: true });
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 };
76
121
  }
77
122
  }
78
123
 
79
124
  /**
80
- * Create a new workflow state
81
- * @param {string} [type='next-task'] - Workflow type
82
- * @param {Object} [policy={}] - Policy overrides
83
- * @returns {Object} New workflow state
125
+ * Write tasks.json to main project
84
126
  */
85
- function createState(type = 'next-task', policy = {}) {
86
- const now = new Date().toISOString();
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;
132
+ }
87
133
 
88
- return {
89
- version: SCHEMA_VERSION,
90
- workflow: {
91
- id: generateWorkflowId(),
92
- type,
93
- status: 'pending',
94
- startedAt: now,
95
- lastUpdatedAt: now,
96
- completedAt: null
97
- },
98
- policy: { ...DEFAULT_POLICY, ...policy },
99
- task: null,
100
- git: null,
101
- pr: null,
102
- phases: {
103
- current: 'policy-selection',
104
- currentIteration: 0,
105
- history: []
106
- },
107
- agents: null,
108
- checkpoints: {
109
- canResume: true,
110
- resumeFrom: null,
111
- resumeContext: null
112
- },
113
- metrics: {
114
- totalDuration: 0,
115
- tokensUsed: 0,
116
- toolCalls: 0,
117
- filesModified: 0,
118
- linesAdded: 0,
119
- linesRemoved: 0
120
- }
134
+ /**
135
+ * Set active task in main project
136
+ */
137
+ function setActiveTask(task, projectPath = process.cwd()) {
138
+ const tasks = readTasks(projectPath);
139
+ tasks.active = {
140
+ ...task,
141
+ startedAt: new Date().toISOString()
121
142
  };
143
+ return writeTasks(tasks, projectPath);
122
144
  }
123
145
 
124
146
  /**
125
- * Read workflow state from file
126
- * @param {string} [baseDir=process.cwd()] - Base directory
127
- * @returns {Object|Error|null} Workflow state, Error if corrupted, or null if not found
147
+ * Clear active task
128
148
  */
129
- function readState(baseDir = process.cwd()) {
130
- const statePath = getStatePath(baseDir);
131
-
132
- if (!fs.existsSync(statePath)) {
133
- return null;
134
- }
135
-
136
- try {
137
- const content = fs.readFileSync(statePath, 'utf8');
138
- const state = JSON.parse(content);
139
-
140
- // Version check
141
- if (state.version !== SCHEMA_VERSION) {
142
- console.warn(`State version mismatch: ${state.version} vs ${SCHEMA_VERSION}`);
143
- // Future: Add migration logic here
144
- }
145
-
146
- return state;
147
- } catch (error) {
148
- const corrupted = new Error(`Corrupted workflow state: ${error.message}`);
149
- corrupted.code = 'ERR_STATE_CORRUPTED';
150
- corrupted.cause = error;
151
- console.error(corrupted.message);
152
- return corrupted;
153
- }
149
+ function clearActiveTask(projectPath = process.cwd()) {
150
+ const tasks = readTasks(projectPath);
151
+ tasks.active = null;
152
+ return writeTasks(tasks, projectPath);
154
153
  }
155
154
 
156
155
  /**
157
- * Write workflow state to file
158
- * @param {Object} state - Workflow state
159
- * @param {string} [baseDir=process.cwd()] - Base directory
160
- * @returns {boolean} Success status
156
+ * Check if there's an active task
157
+ * Uses != null to catch both null and undefined (legacy format safety)
161
158
  */
162
- function writeState(state, baseDir = process.cwd()) {
163
- ensureStateDir(baseDir);
164
- const statePath = getStatePath(baseDir);
159
+ function hasActiveTask(projectPath = process.cwd()) {
160
+ const tasks = readTasks(projectPath);
161
+ return tasks.active != null;
162
+ }
165
163
 
166
- try {
167
- // Update timestamp
168
- state.workflow.lastUpdatedAt = new Date().toISOString();
164
+ // =============================================================================
165
+ // FLOW.JSON - Worktree directory
166
+ // =============================================================================
169
167
 
170
- const content = JSON.stringify(state, null, 2);
171
- fs.writeFileSync(statePath, content, 'utf8');
172
- return true;
173
- } catch (error) {
174
- console.error(`Error writing state: ${error.message}`);
175
- return false;
176
- }
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;
177
176
  }
178
177
 
179
178
  /**
180
- * Update specific fields in workflow state
181
- * @param {Object} updates - Fields to update (deep merge)
182
- * @param {string} [baseDir=process.cwd()] - Base directory
183
- * @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
184
182
  */
185
- function updateState(updates, baseDir = process.cwd()) {
186
- let state = readState(baseDir);
187
-
188
- if (state instanceof Error) {
189
- console.error(`Cannot update state: ${state.message}`);
183
+ function readFlow(worktreePath = process.cwd()) {
184
+ const flowPath = getFlowPath(worktreePath);
185
+ if (!fs.existsSync(flowPath)) {
190
186
  return null;
191
187
  }
192
- if (!state) {
193
- 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}`);
194
192
  return null;
195
193
  }
196
-
197
- // Deep merge updates
198
- state = deepMerge(state, updates);
199
-
200
- if (writeState(state, baseDir)) {
201
- return state;
202
- }
203
-
204
- return null;
205
194
  }
206
195
 
207
196
  /**
208
- * Deep merge two objects (with prototype pollution protection)
209
- * @param {Object} target - Target object
210
- * @param {Object} source - Source object
211
- * @returns {Object} Merged object
197
+ * Write flow.json to worktree
198
+ * Creates a copy to avoid mutating the original object
212
199
  */
213
- function deepMerge(target, source) {
214
- // Handle null/undefined cases
215
- if (!source || typeof source !== 'object') return target;
216
- if (!target || typeof target !== 'object') return source;
217
-
218
- const result = { ...target };
219
-
220
- for (const key of Object.keys(source)) {
221
- // Protect against prototype pollution
222
- if (key === '__proto__' || key === 'constructor' || key === 'prototype') {
223
- continue;
224
- }
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
+ }
225
209
 
226
- const sourceVal = source[key];
227
- const targetVal = result[key];
210
+ /**
211
+ * Update flow.json with partial updates
212
+ * Handles null values correctly (null overwrites existing values)
213
+ * Deep merges nested objects when both exist
214
+ */
215
+ function updateFlow(updates, worktreePath = process.cwd()) {
216
+ const flow = readFlow(worktreePath) || {};
228
217
 
229
- // Handle Date objects - preserve as-is
230
- if (sourceVal instanceof Date) {
231
- result[key] = new Date(sourceVal.getTime());
232
- }
233
- // Handle null explicitly - allow overwriting with null
234
- else if (sourceVal === null) {
235
- result[key] = null;
218
+ for (const [key, value] of Object.entries(updates)) {
219
+ // Null explicitly overwrites
220
+ if (value === null) {
221
+ flow[key] = null;
236
222
  }
237
- // Recursively merge plain objects
238
- else if (sourceVal && typeof sourceVal === 'object' && !Array.isArray(sourceVal)) {
239
- result[key] = deepMerge(targetVal || {}, sourceVal);
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 };
240
229
  }
241
- // Replace arrays and primitives
230
+ // Otherwise direct assignment
242
231
  else {
243
- result[key] = sourceVal;
232
+ flow[key] = value;
244
233
  }
245
234
  }
246
235
 
247
- return result;
236
+ return writeFlow(flow, worktreePath);
248
237
  }
249
238
 
250
239
  /**
251
- * Start a new phase
252
- * @param {string} phaseName - Phase name
253
- * @param {string} [baseDir=process.cwd()] - Base directory
254
- * @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)
255
246
  */
256
- function startPhase(phaseName, baseDir = process.cwd()) {
257
- if (!PHASES.includes(phaseName)) {
258
- console.error(`Invalid phase: ${phaseName}`);
259
- return null;
260
- }
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
+ };
261
272
 
262
- const state = readState(baseDir);
263
- if (state instanceof Error) {
264
- console.error(`Cannot start phase: ${state.message}`);
265
- return null;
266
- }
267
- if (!state) {
268
- console.error('No workflow state exists. Create a workflow first.');
269
- return null;
270
- }
273
+ writeFlow(flow, worktreePath);
271
274
 
272
- 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
+ }
273
284
 
274
- history.push({
275
- phase: phaseName,
276
- status: 'in_progress',
277
- startedAt: new Date().toISOString(),
278
- completedAt: null,
279
- duration: null,
280
- result: null
281
- });
282
-
283
- return updateState({
284
- workflow: { status: 'in_progress' },
285
- phases: { current: phaseName, history },
286
- checkpoints: { canResume: true, resumeFrom: phaseName, resumeContext: null }
287
- }, baseDir);
285
+ return flow;
288
286
  }
289
287
 
290
288
  /**
291
- * Update the current phase entry with completion data
292
- * @param {Object} state - Current state
293
- * @param {string} status - New status (completed/failed)
294
- * @param {Object} result - Result data
295
- * @returns {Object} Updated history
289
+ * Delete flow.json
296
290
  */
297
- function finalizePhaseEntry(state, status, result) {
298
- const history = state.phases.history || [];
299
- const entry = history[history.length - 1];
300
-
301
- if (entry) {
302
- const now = new Date().toISOString();
303
- entry.status = status;
304
- entry.completedAt = now;
305
- entry.duration = new Date(now).getTime() - new Date(entry.startedAt).getTime();
306
- 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;
307
296
  }
297
+ return false;
298
+ }
308
299
 
309
- return history;
300
+ // =============================================================================
301
+ // PHASE MANAGEMENT
302
+ // =============================================================================
303
+
304
+ /**
305
+ * Check if phase is valid
306
+ */
307
+ function isValidPhase(phase) {
308
+ return PHASES.includes(phase);
310
309
  }
311
310
 
312
311
  /**
313
- * Complete the current phase
314
- * @param {Object} [result={}] - Phase result data
315
- * @param {string} [baseDir=process.cwd()] - Base directory
316
- * @returns {Object|null} Updated state or null on error
312
+ * Set current phase
317
313
  */
318
- function completePhase(result = {}, baseDir = process.cwd()) {
319
- const state = readState(baseDir);
320
- if (state instanceof Error) {
321
- console.error(`Cannot complete phase: ${state.message}`);
322
- return null;
314
+ function setPhase(phase, worktreePath = process.cwd()) {
315
+ if (!isValidPhase(phase)) {
316
+ throw new Error(`Invalid phase: ${phase}`);
323
317
  }
324
- if (!state) return null;
325
-
326
- const history = finalizePhaseEntry(state, 'completed', result);
327
- const currentIndex = PHASES.indexOf(state.phases.current);
328
- const nextPhase = currentIndex < PHASES.length - 1 ? PHASES[currentIndex + 1] : 'complete';
318
+ return updateFlow({ phase, status: 'in_progress' }, worktreePath);
319
+ }
329
320
 
330
- return updateState({
331
- phases: { current: nextPhase, history },
332
- checkpoints: { resumeFrom: nextPhase, resumeContext: null }
333
- }, 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);
334
326
  }
335
327
 
336
328
  /**
337
329
  * Fail the current phase
338
- * @param {string} reason - Failure reason
339
- * @param {Object} [context={}] - Context for resume
340
- * @param {string} [baseDir=process.cwd()] - Base directory
341
- * @returns {Object|null} Updated state or null on error
342
330
  */
343
- function failPhase(reason, context = {}, baseDir = process.cwd()) {
344
- const state = readState(baseDir);
345
- if (state instanceof Error) {
346
- console.error(`Cannot fail phase: ${state.message}`);
347
- return null;
348
- }
349
- if (!state) return null;
350
-
351
- const history = finalizePhaseEntry(state, 'failed', { error: reason });
352
-
353
- return updateState({
354
- workflow: { status: 'failed' },
355
- phases: { history },
356
- checkpoints: {
357
- canResume: true,
358
- resumeFrom: state.phases.current,
359
- resumeContext: { reason, ...context }
360
- }
361
- }, 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);
362
340
  }
363
341
 
364
342
  /**
365
343
  * Skip to a specific phase
366
- * @param {string} phaseName - Phase to skip to
367
- * @param {string} [reason='manual skip'] - Skip reason
368
- * @param {string} [baseDir=process.cwd()] - Base directory
369
- * @returns {Object|null} Updated state or null on error
370
344
  */
371
- function skipToPhase(phaseName, reason = 'manual skip', baseDir = process.cwd()) {
372
- if (!PHASES.includes(phaseName)) {
373
- console.error(`Invalid phase: ${phaseName}`);
374
- return null;
375
- }
376
-
377
- const state = readState(baseDir);
378
- if (state instanceof Error) {
379
- console.error(`Cannot skip to phase: ${state.message}`);
380
- return null;
381
- }
382
- if (!state) return null;
383
-
384
- const currentIndex = PHASES.indexOf(state.phases.current);
385
- const targetIndex = PHASES.indexOf(phaseName);
386
-
387
- // Add skipped entries for phases we're jumping over
388
- const history = [...(state.phases.history || [])];
389
- const now = new Date().toISOString();
390
-
391
- for (let i = currentIndex; i < targetIndex; i++) {
392
- history.push({
393
- phase: PHASES[i],
394
- status: 'skipped',
395
- startedAt: now,
396
- completedAt: now,
397
- duration: 0,
398
- result: { skippedReason: reason }
399
- });
345
+ function skipToPhase(phase, reason = 'manual skip', worktreePath = process.cwd()) {
346
+ if (!isValidPhase(phase)) {
347
+ throw new Error(`Invalid phase: ${phase}`);
400
348
  }
401
-
402
- return updateState({
403
- phases: {
404
- current: phaseName,
405
- history
406
- },
407
- checkpoints: {
408
- resumeFrom: phaseName
409
- }
410
- }, baseDir);
349
+ return updateFlow({
350
+ phase,
351
+ status: 'in_progress',
352
+ skipReason: reason
353
+ }, worktreePath);
411
354
  }
412
355
 
413
356
  /**
414
- * Complete the entire workflow
415
- * @param {Object} [result={}] - Final result data
416
- * @param {string} [baseDir=process.cwd()] - Base directory
417
- * @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
418
359
  */
419
- function completeWorkflow(result = {}, baseDir = process.cwd()) {
420
- const state = readState(baseDir);
421
- if (state instanceof Error) {
422
- console.error(`Cannot complete workflow: ${state.message}`);
423
- return null;
424
- }
425
- if (!state) return null;
360
+ function completePhase(result = null, worktreePath = process.cwd()) {
361
+ const flow = readFlow(worktreePath);
362
+ if (!flow) return null;
426
363
 
427
- const now = new Date().toISOString();
428
- const startTime = new Date(state.workflow.startedAt).getTime();
429
- const endTime = new Date(now).getTime();
364
+ const currentIndex = PHASES.indexOf(flow.phase);
365
+ const nextPhase = PHASES[currentIndex + 1] || 'complete';
430
366
 
431
- return updateState({
432
- workflow: {
433
- status: 'completed',
434
- completedAt: now
435
- },
436
- phases: {
437
- current: 'complete'
438
- },
439
- checkpoints: {
440
- canResume: false,
441
- resumeFrom: null
442
- },
443
- metrics: {
444
- totalDuration: endTime - startTime,
445
- ...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;
446
378
  }
447
- }, baseDir);
379
+ }
380
+
381
+ updateFlow(updates, worktreePath);
382
+ return readFlow(worktreePath);
448
383
  }
449
384
 
450
385
  /**
451
- * Abort the workflow
452
- * @param {string} [reason='user aborted'] - Abort reason
453
- * @param {string} [baseDir=process.cwd()] - Base directory
454
- * @returns {Object|null} Updated state or null on error
386
+ * Map phase to result field
455
387
  */
456
- function abortWorkflow(reason = 'user aborted', baseDir = process.cwd()) {
457
- return updateState({
458
- workflow: {
459
- status: 'aborted',
460
- completedAt: new Date().toISOString()
461
- },
462
- checkpoints: {
463
- canResume: false,
464
- resumeFrom: null,
465
- resumeContext: { abortReason: reason }
466
- }
467
- }, baseDir);
388
+ function getResultField(phase) {
389
+ const mapping = {
390
+ 'exploration': 'exploration',
391
+ 'planning': 'plan',
392
+ 'review-loop': 'reviewResult'
393
+ };
394
+ return mapping[phase] || null;
468
395
  }
469
396
 
470
397
  /**
471
- * Delete workflow state (cleanup)
472
- * @param {string} [baseDir=process.cwd()] - Base directory
473
- * @returns {boolean} Success status
398
+ * Mark workflow as failed
474
399
  */
475
- function deleteState(baseDir = process.cwd()) {
476
- const statePath = getStatePath(baseDir);
477
-
478
- try {
479
- if (fs.existsSync(statePath)) {
480
- fs.unlinkSync(statePath);
481
- }
482
- return true;
483
- } catch (error) {
484
- console.error(`Error deleting state: ${error.message}`);
485
- return false;
486
- }
400
+ function failWorkflow(error, worktreePath = process.cwd()) {
401
+ return updateFlow({
402
+ status: 'failed',
403
+ error: error?.message || String(error)
404
+ }, worktreePath);
487
405
  }
488
406
 
489
407
  /**
490
- * Check if a workflow is in progress
491
- * @param {string} [baseDir=process.cwd()] - Base directory
492
- * @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
493
411
  */
494
- function hasActiveWorkflow(baseDir = process.cwd()) {
495
- const state = readState(baseDir);
496
- if (state instanceof Error) return false;
497
- if (!state) return false;
412
+ function completeWorkflow(worktreePath = process.cwd()) {
413
+ const flow = readFlow(worktreePath);
498
414
 
499
- return ['pending', 'in_progress', 'paused'].includes(state.workflow.status);
415
+ // Clear active task from main project if projectPath is stored
416
+ if (flow && flow.projectPath) {
417
+ clearActiveTask(flow.projectPath);
418
+ }
419
+
420
+ return updateFlow({
421
+ phase: 'complete',
422
+ status: 'completed',
423
+ completedAt: new Date().toISOString()
424
+ }, worktreePath);
500
425
  }
501
426
 
502
427
  /**
503
- * Get workflow summary for display
504
- * @param {string} [baseDir=process.cwd()] - Base directory
505
- * @returns {Object|null} Summary object or null
428
+ * Abort workflow
429
+ * Also clears the active task from tasks.json using stored projectPath
506
430
  */
507
- function getWorkflowSummary(baseDir = process.cwd()) {
508
- const state = readState(baseDir);
509
- if (state instanceof Error) {
510
- return { error: state.message, code: state.code };
511
- }
512
- if (!state) return null;
431
+ function abortWorkflow(reason, worktreePath = process.cwd()) {
432
+ const flow = readFlow(worktreePath);
513
433
 
514
- const completedPhases = state.phases.history?.filter(p => p.status === 'completed').length || 0;
515
- 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
+ }
516
438
 
517
- return {
518
- id: state.workflow.id,
519
- type: state.workflow.type,
520
- status: state.workflow.status,
521
- currentPhase: state.phases.current,
522
- progress: `${completedPhases}/${totalPhases}`,
523
- progressPercent: Math.round((completedPhases / totalPhases) * 100),
524
- task: state.task ? {
525
- id: state.task.id,
526
- title: state.task.title,
527
- source: state.task.source
528
- } : null,
529
- pr: state.pr ? {
530
- number: state.pr.number,
531
- url: state.pr.url,
532
- ciStatus: state.pr.ciStatus
533
- } : null,
534
- canResume: state.checkpoints.canResume,
535
- resumeFrom: state.checkpoints.resumeFrom,
536
- startedAt: state.workflow.startedAt,
537
- duration: state.metrics?.totalDuration || 0
538
- };
439
+ return updateFlow({
440
+ status: 'aborted',
441
+ abortReason: reason,
442
+ abortedAt: new Date().toISOString()
443
+ }, worktreePath);
539
444
  }
540
445
 
446
+ // =============================================================================
447
+ // CONVENIENCE FUNCTIONS
448
+ // =============================================================================
449
+
541
450
  /**
542
- * Update agent results
543
- * @param {string} agentName - Agent identifier
544
- * @param {Object} result - Agent result
545
- * @param {string} [baseDir=process.cwd()] - Base directory
546
- * @returns {Object|null} Updated state or null on error
451
+ * Get workflow summary for display
547
452
  */
548
- function updateAgentResult(agentName, result, baseDir = process.cwd()) {
549
- const state = readState(baseDir);
550
- if (state instanceof Error) {
551
- console.error(`Cannot update agent result: ${state.message}`);
552
- return null;
553
- }
554
- if (!state) return null;
453
+ function getFlowSummary(worktreePath = process.cwd()) {
454
+ const flow = readFlow(worktreePath);
455
+ if (!flow) return null;
555
456
 
556
- const agents = state.agents || {
557
- lastRun: {},
558
- totalIterations: 0,
559
- totalIssuesFound: 0,
560
- 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
561
464
  };
562
-
563
- agents.lastRun[agentName] = result;
564
- agents.totalIssuesFound += result.issues || 0;
565
-
566
- return updateState({ agents }, baseDir);
567
465
  }
568
466
 
569
467
  /**
570
- * Increment review iteration
571
- * @param {Object} [result={}] - Iteration result
572
- * @param {string} [baseDir=process.cwd()] - Base directory
573
- * @returns {Object|null} Updated state or null on error
468
+ * Check if workflow can be resumed
574
469
  */
575
- function incrementIteration(result = {}, baseDir = process.cwd()) {
576
- const state = readState(baseDir);
577
- if (state instanceof Error) {
578
- console.error(`Cannot increment iteration: ${state.message}`);
579
- return null;
580
- }
581
- if (!state) return null;
582
-
583
- const agents = state.agents || {
584
- lastRun: {},
585
- totalIterations: 0,
586
- totalIssuesFound: 0,
587
- totalIssuesFixed: 0
588
- };
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
+ }
589
475
 
590
- agents.totalIterations += 1;
591
- agents.totalIssuesFixed += result.fixed || 0;
476
+ // =============================================================================
477
+ // BACKWARDS COMPATIBILITY ALIASES
478
+ // =============================================================================
592
479
 
593
- return updateState({
594
- phases: {
595
- currentIteration: state.phases.currentIteration + 1
596
- },
597
- agents
598
- }, baseDir);
599
- }
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;
600
488
 
601
- // Export all functions
602
489
  module.exports = {
603
490
  // Constants
604
- SCHEMA_VERSION,
605
491
  PHASES,
606
- DEFAULT_POLICY,
607
-
608
- // Core functions
609
- generateWorkflowId,
610
- getStatePath,
611
- ensureStateDir,
612
492
 
613
- // CRUD operations
614
- createState,
615
- readState,
616
- writeState,
617
- updateState,
618
- 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,
619
508
 
620
509
  // Phase management
510
+ isValidPhase,
511
+ setPhase,
621
512
  startPhase,
622
513
  completePhase,
623
514
  failPhase,
624
515
  skipToPhase,
625
-
626
- // Workflow lifecycle
516
+ failWorkflow,
627
517
  completeWorkflow,
628
518
  abortWorkflow,
629
- hasActiveWorkflow,
630
- getWorkflowSummary,
631
519
 
632
- // Agent management
633
- updateAgentResult,
634
- incrementIteration
520
+ // Convenience
521
+ getFlowSummary,
522
+ canResume,
523
+ generateWorkflowId,
524
+
525
+ // Backwards compatibility
526
+ readState,
527
+ writeState,
528
+ updateState,
529
+ createState,
530
+ deleteState,
531
+ hasActiveWorkflow,
532
+ getWorkflowSummary
635
533
  };