awesome-slash 2.4.4 → 2.5.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (151) hide show
  1. package/.claude-plugin/marketplace.json +6 -6
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/CHANGELOG.md +123 -1
  4. package/README.md +186 -159
  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/state-dir.js +122 -0
  14. package/lib/platform/verify-tools.js +10 -78
  15. package/lib/schemas/README.md +195 -0
  16. package/lib/schemas/validator.js +247 -0
  17. package/lib/sources/custom-handler.js +199 -0
  18. package/lib/sources/policy-questions.js +239 -0
  19. package/lib/sources/source-cache.js +164 -0
  20. package/lib/state/workflow-state.js +368 -665
  21. package/lib/types/README.md +292 -0
  22. package/lib/types/agent-frontmatter.d.ts +134 -0
  23. package/lib/types/command-frontmatter.d.ts +107 -0
  24. package/lib/types/hook-frontmatter.d.ts +115 -0
  25. package/lib/types/index.d.ts +84 -0
  26. package/lib/types/plugin-manifest.d.ts +102 -0
  27. package/lib/types/skill-frontmatter.d.ts +89 -0
  28. package/lib/utils/cache-manager.js +154 -0
  29. package/lib/utils/context-optimizer.js +5 -36
  30. package/lib/utils/deprecation.js +37 -0
  31. package/lib/utils/shell-escape.js +88 -0
  32. package/mcp-server/index.js +513 -22
  33. package/package.json +6 -2
  34. package/plugins/deslop-around/.claude-plugin/plugin.json +1 -1
  35. package/plugins/deslop-around/lib/index.js +170 -0
  36. package/plugins/deslop-around/lib/patterns/review-patterns.js +58 -11
  37. package/plugins/deslop-around/lib/patterns/slop-patterns.js +169 -129
  38. package/plugins/deslop-around/lib/platform/detect-platform.js +162 -316
  39. package/plugins/deslop-around/lib/platform/detection-configs.js +93 -0
  40. package/plugins/deslop-around/lib/platform/state-dir.js +122 -0
  41. package/plugins/deslop-around/lib/platform/verify-tools.js +10 -78
  42. package/plugins/deslop-around/lib/schemas/README.md +195 -0
  43. package/plugins/deslop-around/lib/schemas/validator.js +247 -0
  44. package/plugins/deslop-around/lib/sources/custom-handler.js +199 -0
  45. package/plugins/deslop-around/lib/sources/policy-questions.js +239 -0
  46. package/plugins/deslop-around/lib/sources/source-cache.js +164 -0
  47. package/plugins/deslop-around/lib/state/workflow-state.js +387 -484
  48. package/plugins/deslop-around/lib/types/README.md +292 -0
  49. package/plugins/deslop-around/lib/types/agent-frontmatter.d.ts +134 -0
  50. package/plugins/deslop-around/lib/types/command-frontmatter.d.ts +107 -0
  51. package/plugins/deslop-around/lib/types/hook-frontmatter.d.ts +115 -0
  52. package/plugins/deslop-around/lib/types/index.d.ts +84 -0
  53. package/plugins/deslop-around/lib/types/plugin-manifest.d.ts +102 -0
  54. package/plugins/deslop-around/lib/types/skill-frontmatter.d.ts +89 -0
  55. package/plugins/deslop-around/lib/utils/cache-manager.js +154 -0
  56. package/plugins/deslop-around/lib/utils/context-optimizer.js +115 -37
  57. package/plugins/deslop-around/lib/utils/deprecation.js +37 -0
  58. package/plugins/deslop-around/lib/utils/shell-escape.js +88 -0
  59. package/plugins/next-task/.claude-plugin/plugin.json +1 -1
  60. package/plugins/next-task/agents/delivery-validator.md +2 -2
  61. package/plugins/next-task/agents/implementation-agent.md +3 -4
  62. package/plugins/next-task/agents/planning-agent.md +77 -19
  63. package/plugins/next-task/agents/review-orchestrator.md +21 -122
  64. package/plugins/next-task/agents/task-discoverer.md +164 -23
  65. package/plugins/next-task/commands/next-task.md +180 -14
  66. package/plugins/next-task/lib/index.js +170 -0
  67. package/plugins/next-task/lib/patterns/review-patterns.js +58 -11
  68. package/plugins/next-task/lib/patterns/slop-patterns.js +169 -129
  69. package/plugins/next-task/lib/platform/detect-platform.js +162 -316
  70. package/plugins/next-task/lib/platform/detection-configs.js +93 -0
  71. package/plugins/next-task/lib/platform/state-dir.js +122 -0
  72. package/plugins/next-task/lib/platform/verify-tools.js +10 -78
  73. package/plugins/next-task/lib/schemas/README.md +195 -0
  74. package/plugins/next-task/lib/schemas/validator.js +247 -0
  75. package/plugins/next-task/lib/sources/custom-handler.js +199 -0
  76. package/plugins/next-task/lib/sources/policy-questions.js +239 -0
  77. package/plugins/next-task/lib/sources/source-cache.js +164 -0
  78. package/plugins/next-task/lib/state/workflow-state.js +387 -484
  79. package/plugins/next-task/lib/types/README.md +292 -0
  80. package/plugins/next-task/lib/types/agent-frontmatter.d.ts +134 -0
  81. package/plugins/next-task/lib/types/command-frontmatter.d.ts +107 -0
  82. package/plugins/next-task/lib/types/hook-frontmatter.d.ts +115 -0
  83. package/plugins/next-task/lib/types/index.d.ts +84 -0
  84. package/plugins/next-task/lib/types/plugin-manifest.d.ts +102 -0
  85. package/plugins/next-task/lib/types/skill-frontmatter.d.ts +89 -0
  86. package/plugins/next-task/lib/utils/cache-manager.js +154 -0
  87. package/plugins/next-task/lib/utils/context-optimizer.js +115 -37
  88. package/plugins/next-task/lib/utils/deprecation.js +37 -0
  89. package/plugins/next-task/lib/utils/shell-escape.js +88 -0
  90. package/plugins/project-review/.claude-plugin/plugin.json +1 -1
  91. package/plugins/project-review/lib/index.js +170 -0
  92. package/plugins/project-review/lib/patterns/review-patterns.js +58 -11
  93. package/plugins/project-review/lib/patterns/slop-patterns.js +169 -129
  94. package/plugins/project-review/lib/platform/detect-platform.js +162 -316
  95. package/plugins/project-review/lib/platform/detection-configs.js +93 -0
  96. package/plugins/project-review/lib/platform/state-dir.js +122 -0
  97. package/plugins/project-review/lib/platform/verify-tools.js +10 -78
  98. package/plugins/project-review/lib/schemas/README.md +195 -0
  99. package/plugins/project-review/lib/schemas/validator.js +247 -0
  100. package/plugins/project-review/lib/sources/custom-handler.js +199 -0
  101. package/plugins/project-review/lib/sources/policy-questions.js +239 -0
  102. package/plugins/project-review/lib/sources/source-cache.js +164 -0
  103. package/plugins/project-review/lib/state/workflow-state.js +387 -484
  104. package/plugins/project-review/lib/types/README.md +292 -0
  105. package/plugins/project-review/lib/types/agent-frontmatter.d.ts +134 -0
  106. package/plugins/project-review/lib/types/command-frontmatter.d.ts +107 -0
  107. package/plugins/project-review/lib/types/hook-frontmatter.d.ts +115 -0
  108. package/plugins/project-review/lib/types/index.d.ts +84 -0
  109. package/plugins/project-review/lib/types/plugin-manifest.d.ts +102 -0
  110. package/plugins/project-review/lib/types/skill-frontmatter.d.ts +89 -0
  111. package/plugins/project-review/lib/utils/cache-manager.js +154 -0
  112. package/plugins/project-review/lib/utils/context-optimizer.js +115 -37
  113. package/plugins/project-review/lib/utils/deprecation.js +37 -0
  114. package/plugins/project-review/lib/utils/shell-escape.js +88 -0
  115. package/plugins/reality-check/.claude-plugin/plugin.json +1 -1
  116. package/plugins/reality-check/agents/code-explorer.md +1 -1
  117. package/plugins/ship/.claude-plugin/plugin.json +1 -1
  118. package/plugins/ship/lib/index.js +170 -0
  119. package/plugins/ship/lib/patterns/review-patterns.js +58 -11
  120. package/plugins/ship/lib/patterns/slop-patterns.js +169 -129
  121. package/plugins/ship/lib/platform/detect-platform.js +162 -316
  122. package/plugins/ship/lib/platform/detection-configs.js +93 -0
  123. package/plugins/ship/lib/platform/state-dir.js +122 -0
  124. package/plugins/ship/lib/platform/verify-tools.js +10 -78
  125. package/plugins/ship/lib/schemas/README.md +195 -0
  126. package/plugins/ship/lib/schemas/validator.js +247 -0
  127. package/plugins/ship/lib/sources/custom-handler.js +199 -0
  128. package/plugins/ship/lib/sources/policy-questions.js +239 -0
  129. package/plugins/ship/lib/sources/source-cache.js +164 -0
  130. package/plugins/ship/lib/state/workflow-state.js +387 -484
  131. package/plugins/ship/lib/types/README.md +292 -0
  132. package/plugins/ship/lib/types/agent-frontmatter.d.ts +134 -0
  133. package/plugins/ship/lib/types/command-frontmatter.d.ts +107 -0
  134. package/plugins/ship/lib/types/hook-frontmatter.d.ts +115 -0
  135. package/plugins/ship/lib/types/index.d.ts +84 -0
  136. package/plugins/ship/lib/types/plugin-manifest.d.ts +102 -0
  137. package/plugins/ship/lib/types/skill-frontmatter.d.ts +89 -0
  138. package/plugins/ship/lib/utils/cache-manager.js +154 -0
  139. package/plugins/ship/lib/utils/context-optimizer.js +115 -37
  140. package/plugins/ship/lib/utils/deprecation.js +37 -0
  141. package/plugins/ship/lib/utils/shell-escape.js +88 -0
  142. package/scripts/install/codex.sh +216 -72
  143. package/scripts/install/opencode.sh +197 -21
  144. package/lib/state/workflow-state.schema.json +0 -282
  145. package/plugins/deslop-around/lib/state/workflow-state.schema.json +0 -282
  146. package/plugins/next-task/agents/policy-selector.md +0 -248
  147. package/plugins/next-task/lib/state/tasks-registry.schema.json +0 -85
  148. package/plugins/next-task/lib/state/workflow-state.schema.json +0 -282
  149. package/plugins/next-task/lib/state/worktree-status.schema.json +0 -219
  150. package/plugins/project-review/lib/state/workflow-state.schema.json +0 -282
  151. package/plugins/ship/lib/state/workflow-state.schema.json +0 -282
@@ -1,18 +1,69 @@
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: {stateDir}/tasks.json (tracks active worktree/task)
6
+ * - Worktree: {stateDir}/flow.json (tracks workflow progress)
7
+ *
8
+ * State directory is platform-aware:
9
+ * - Claude Code: .claude/
10
+ * - OpenCode: .opencode/
11
+ * - Codex CLI: .codex/
6
12
  */
7
13
 
8
14
  const fs = require('fs');
9
15
  const path = require('path');
10
16
  const crypto = require('crypto');
17
+ const { getStateDir } = require('../platform/state-dir');
18
+
19
+ // File paths
20
+ const TASKS_FILE = 'tasks.json';
21
+ const FLOW_FILE = 'flow.json';
11
22
 
12
- const SCHEMA_VERSION = '2.0.0';
13
- const STATE_DIR = '.claude';
14
- const STATE_FILE = 'workflow-state.json';
23
+ /**
24
+ * Validate and resolve path to prevent path traversal attacks
25
+ * @param {string} basePath - Base directory path
26
+ * @returns {string} Validated absolute path
27
+ * @throws {Error} If path is invalid
28
+ */
29
+ function validatePath(basePath) {
30
+ if (typeof basePath !== 'string' || basePath.length === 0) {
31
+ throw new Error('Path must be a non-empty string');
32
+ }
33
+ const resolved = path.resolve(basePath);
34
+ if (resolved.includes('\0')) {
35
+ throw new Error('Path contains invalid null byte');
36
+ }
37
+ return resolved;
38
+ }
39
+
40
+ /**
41
+ * Validate that target path is within base directory
42
+ * @param {string} targetPath - Target file path
43
+ * @param {string} basePath - Base directory
44
+ * @throws {Error} If path traversal detected
45
+ */
46
+ function validatePathWithinBase(targetPath, basePath) {
47
+ const resolvedTarget = path.resolve(targetPath);
48
+ const resolvedBase = path.resolve(basePath);
49
+ if (!resolvedTarget.startsWith(resolvedBase + path.sep) && resolvedTarget !== resolvedBase) {
50
+ throw new Error('Path traversal detected');
51
+ }
52
+ }
15
53
 
54
+ /**
55
+ * Generate a unique workflow ID
56
+ * @returns {string} Workflow ID
57
+ */
58
+ function generateWorkflowId() {
59
+ const now = new Date();
60
+ const date = now.toISOString().slice(0, 10).replace(/-/g, '');
61
+ const time = now.toISOString().slice(11, 19).replace(/:/g, '');
62
+ const random = crypto.randomBytes(4).toString('hex');
63
+ return `workflow-${date}-${time}-${random}`;
64
+ }
65
+
66
+ // Valid phases for the workflow
16
67
  const PHASES = [
17
68
  'policy-selection',
18
69
  'task-discovery',
@@ -22,614 +73,466 @@ const PHASES = [
22
73
  'user-approval',
23
74
  'implementation',
24
75
  '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',
76
+ 'delivery-validation',
77
+ 'shipping',
34
78
  'complete'
35
79
  ];
36
80
 
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
81
  /**
48
- * Generate a unique workflow ID
49
- * @returns {string} Workflow ID in format: workflow-YYYYMMDD-HHMMSS-random
82
+ * Ensure state directory exists (platform-aware)
50
83
  */
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}`;
84
+ function ensureStateDir(basePath) {
85
+ const stateDir = path.join(basePath, getStateDir(basePath));
86
+ if (!fs.existsSync(stateDir)) {
87
+ fs.mkdirSync(stateDir, { recursive: true });
88
+ }
89
+ return stateDir;
57
90
  }
58
91
 
92
+ // =============================================================================
93
+ // TASKS.JSON - Main project directory
94
+ // =============================================================================
95
+
59
96
  /**
60
- * Get the state file path
61
- * @param {string} [baseDir=process.cwd()] - Base directory
62
- * @returns {string} Full path to state file
97
+ * Get path to tasks.json with validation
63
98
  */
64
- function getStatePath(baseDir = process.cwd()) {
65
- return path.join(baseDir, STATE_DIR, STATE_FILE);
99
+ function getTasksPath(projectPath = process.cwd()) {
100
+ const validatedBase = validatePath(projectPath);
101
+ const tasksPath = path.join(validatedBase, getStateDir(projectPath), TASKS_FILE);
102
+ validatePathWithinBase(tasksPath, validatedBase);
103
+ return tasksPath;
66
104
  }
67
105
 
68
106
  /**
69
- * Ensure state directory exists
70
- * @param {string} [baseDir=process.cwd()] - Base directory
107
+ * Read tasks.json from main project
108
+ * Returns { active: null } if file doesn't exist or is corrupted
109
+ * Logs critical error on corruption to prevent silent data loss
71
110
  */
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 });
111
+ function readTasks(projectPath = process.cwd()) {
112
+ const tasksPath = getTasksPath(projectPath);
113
+ if (!fs.existsSync(tasksPath)) {
114
+ return { active: null };
115
+ }
116
+ try {
117
+ const data = JSON.parse(fs.readFileSync(tasksPath, 'utf8'));
118
+ // Normalize legacy format that may not have 'active' field
119
+ if (!Object.prototype.hasOwnProperty.call(data, 'active')) {
120
+ return { active: null };
121
+ }
122
+ return data;
123
+ } catch (e) {
124
+ console.error(`[CRITICAL] Corrupted tasks.json at ${tasksPath}: ${e.message}`);
125
+ return { active: null };
76
126
  }
77
127
  }
78
128
 
79
129
  /**
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
130
+ * Write tasks.json to main project
84
131
  */
85
- function createState(type = 'next-task', policy = {}) {
86
- const now = new Date().toISOString();
132
+ function writeTasks(tasks, projectPath = process.cwd()) {
133
+ ensureStateDir(projectPath);
134
+ const tasksPath = getTasksPath(projectPath);
135
+ fs.writeFileSync(tasksPath, JSON.stringify(tasks, null, 2), 'utf8');
136
+ return true;
137
+ }
87
138
 
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
- }
139
+ /**
140
+ * Set active task in main project
141
+ */
142
+ function setActiveTask(task, projectPath = process.cwd()) {
143
+ const tasks = readTasks(projectPath);
144
+ tasks.active = {
145
+ ...task,
146
+ startedAt: new Date().toISOString()
121
147
  };
148
+ return writeTasks(tasks, projectPath);
122
149
  }
123
150
 
124
151
  /**
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
152
+ * Clear active task
128
153
  */
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
- }
154
+ function clearActiveTask(projectPath = process.cwd()) {
155
+ const tasks = readTasks(projectPath);
156
+ tasks.active = null;
157
+ return writeTasks(tasks, projectPath);
154
158
  }
155
159
 
156
160
  /**
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
161
+ * Check if there's an active task
162
+ * Uses != null to catch both null and undefined (legacy format safety)
161
163
  */
162
- function writeState(state, baseDir = process.cwd()) {
163
- ensureStateDir(baseDir);
164
- const statePath = getStatePath(baseDir);
164
+ function hasActiveTask(projectPath = process.cwd()) {
165
+ const tasks = readTasks(projectPath);
166
+ return tasks.active != null;
167
+ }
165
168
 
166
- try {
167
- // Update timestamp
168
- state.workflow.lastUpdatedAt = new Date().toISOString();
169
+ // =============================================================================
170
+ // FLOW.JSON - Worktree directory
171
+ // =============================================================================
169
172
 
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
- }
173
+ /**
174
+ * Get path to flow.json with validation
175
+ */
176
+ function getFlowPath(worktreePath = process.cwd()) {
177
+ const validatedBase = validatePath(worktreePath);
178
+ const flowPath = path.join(validatedBase, getStateDir(worktreePath), FLOW_FILE);
179
+ validatePathWithinBase(flowPath, validatedBase);
180
+ return flowPath;
177
181
  }
178
182
 
179
183
  /**
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
184
+ * Read flow.json from worktree
185
+ * Returns null if file doesn't exist or is corrupted
186
+ * Logs critical error on corruption to prevent silent data loss
184
187
  */
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}`);
188
+ function readFlow(worktreePath = process.cwd()) {
189
+ const flowPath = getFlowPath(worktreePath);
190
+ if (!fs.existsSync(flowPath)) {
190
191
  return null;
191
192
  }
192
- if (!state) {
193
- console.error('No existing state to update');
193
+ try {
194
+ return JSON.parse(fs.readFileSync(flowPath, 'utf8'));
195
+ } catch (e) {
196
+ console.error(`[CRITICAL] Corrupted flow.json at ${flowPath}: ${e.message}`);
194
197
  return null;
195
198
  }
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
199
  }
206
200
 
207
201
  /**
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
202
+ * Write flow.json to worktree
203
+ * Creates a copy to avoid mutating the original object
212
204
  */
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
- }
205
+ function writeFlow(flow, worktreePath = process.cwd()) {
206
+ ensureStateDir(worktreePath);
207
+ // Clone to avoid mutating the original object
208
+ const flowCopy = JSON.parse(JSON.stringify(flow));
209
+ flowCopy.lastUpdate = new Date().toISOString();
210
+ const flowPath = getFlowPath(worktreePath);
211
+ fs.writeFileSync(flowPath, JSON.stringify(flowCopy, null, 2), 'utf8');
212
+ return true;
213
+ }
225
214
 
226
- const sourceVal = source[key];
227
- const targetVal = result[key];
215
+ /**
216
+ * Update flow.json with partial updates
217
+ * Handles null values correctly (null overwrites existing values)
218
+ * Deep merges nested objects when both exist
219
+ */
220
+ function updateFlow(updates, worktreePath = process.cwd()) {
221
+ const flow = readFlow(worktreePath) || {};
228
222
 
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;
223
+ for (const [key, value] of Object.entries(updates)) {
224
+ // Null explicitly overwrites
225
+ if (value === null) {
226
+ flow[key] = null;
236
227
  }
237
- // Recursively merge plain objects
238
- else if (sourceVal && typeof sourceVal === 'object' && !Array.isArray(sourceVal)) {
239
- result[key] = deepMerge(targetVal || {}, sourceVal);
228
+ // Deep merge if both source and target are non-null objects
229
+ else if (
230
+ value && typeof value === 'object' && !Array.isArray(value) &&
231
+ flow[key] && typeof flow[key] === 'object' && !Array.isArray(flow[key])
232
+ ) {
233
+ flow[key] = { ...flow[key], ...value };
240
234
  }
241
- // Replace arrays and primitives
235
+ // Otherwise direct assignment
242
236
  else {
243
- result[key] = sourceVal;
237
+ flow[key] = value;
244
238
  }
245
239
  }
246
240
 
247
- return result;
241
+ return writeFlow(flow, worktreePath);
248
242
  }
249
243
 
250
244
  /**
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
245
+ * Create initial flow for a new task
246
+ * Also registers the task as active in the main project's tasks.json
247
+ * @param {Object} task - Task object with id, title, source, url
248
+ * @param {Object} policy - Policy object with stoppingPoint
249
+ * @param {string} worktreePath - Path to worktree
250
+ * @param {string} projectPath - Path to main project (for tasks.json registration)
255
251
  */
256
- function startPhase(phaseName, baseDir = process.cwd()) {
257
- if (!PHASES.includes(phaseName)) {
258
- console.error(`Invalid phase: ${phaseName}`);
259
- return null;
260
- }
252
+ function createFlow(task, policy, worktreePath = process.cwd(), projectPath = null) {
253
+ const flow = {
254
+ task: {
255
+ id: task.id,
256
+ title: task.title,
257
+ source: task.source,
258
+ url: task.url || null
259
+ },
260
+ policy: {
261
+ stoppingPoint: policy.stoppingPoint || 'merged'
262
+ },
263
+ phase: 'policy-selection',
264
+ status: 'in_progress',
265
+ lastUpdate: new Date().toISOString(),
266
+ userNotes: '',
267
+ git: {
268
+ branch: null,
269
+ baseBranch: 'main'
270
+ },
271
+ pr: null,
272
+ exploration: null,
273
+ plan: null,
274
+ // Store projectPath so completeWorkflow knows where to clear the task
275
+ projectPath: projectPath
276
+ };
261
277
 
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
- }
278
+ writeFlow(flow, worktreePath);
271
279
 
272
- const history = state.phases?.history || [];
280
+ // Register task as active in main project
281
+ if (projectPath) {
282
+ setActiveTask({
283
+ taskId: task.id,
284
+ title: task.title,
285
+ worktree: worktreePath,
286
+ branch: flow.git.branch
287
+ }, projectPath);
288
+ }
273
289
 
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);
290
+ return flow;
288
291
  }
289
292
 
290
293
  /**
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
294
+ * Delete flow.json
296
295
  */
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;
296
+ function deleteFlow(worktreePath = process.cwd()) {
297
+ const flowPath = getFlowPath(worktreePath);
298
+ if (fs.existsSync(flowPath)) {
299
+ fs.unlinkSync(flowPath);
300
+ return true;
307
301
  }
302
+ return false;
303
+ }
304
+
305
+ // =============================================================================
306
+ // PHASE MANAGEMENT
307
+ // =============================================================================
308
308
 
309
- return history;
309
+ /**
310
+ * Check if phase is valid
311
+ */
312
+ function isValidPhase(phase) {
313
+ return PHASES.includes(phase);
310
314
  }
311
315
 
312
316
  /**
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
317
+ * Set current phase
317
318
  */
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;
319
+ function setPhase(phase, worktreePath = process.cwd()) {
320
+ if (!isValidPhase(phase)) {
321
+ throw new Error(`Invalid phase: ${phase}`);
323
322
  }
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';
323
+ return updateFlow({ phase, status: 'in_progress' }, worktreePath);
324
+ }
329
325
 
330
- return updateState({
331
- phases: { current: nextPhase, history },
332
- checkpoints: { resumeFrom: nextPhase, resumeContext: null }
333
- }, baseDir);
326
+ /**
327
+ * Start a phase (alias for setPhase, for backwards compatibility)
328
+ */
329
+ function startPhase(phase, worktreePath = process.cwd()) {
330
+ return setPhase(phase, worktreePath);
334
331
  }
335
332
 
336
333
  /**
337
334
  * 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
335
  */
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);
336
+ function failPhase(reason, context = {}, worktreePath = process.cwd()) {
337
+ const flow = readFlow(worktreePath);
338
+ if (!flow) return null;
339
+
340
+ return updateFlow({
341
+ status: 'failed',
342
+ error: reason,
343
+ failContext: context
344
+ }, worktreePath);
362
345
  }
363
346
 
364
347
  /**
365
348
  * 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
349
  */
371
- function skipToPhase(phaseName, reason = 'manual skip', baseDir = process.cwd()) {
372
- if (!PHASES.includes(phaseName)) {
373
- console.error(`Invalid phase: ${phaseName}`);
374
- return null;
350
+ function skipToPhase(phase, reason = 'manual skip', worktreePath = process.cwd()) {
351
+ if (!isValidPhase(phase)) {
352
+ throw new Error(`Invalid phase: ${phase}`);
375
353
  }
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
- });
400
- }
401
-
402
- return updateState({
403
- phases: {
404
- current: phaseName,
405
- history
406
- },
407
- checkpoints: {
408
- resumeFrom: phaseName
409
- }
410
- }, baseDir);
354
+ return updateFlow({
355
+ phase,
356
+ status: 'in_progress',
357
+ skipReason: reason
358
+ }, worktreePath);
411
359
  }
412
360
 
413
361
  /**
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
362
+ * Complete current phase and move to next
363
+ * Uses updateFlow pattern to avoid direct mutation issues
418
364
  */
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;
365
+ function completePhase(result = null, worktreePath = process.cwd()) {
366
+ const flow = readFlow(worktreePath);
367
+ if (!flow) return null;
426
368
 
427
- const now = new Date().toISOString();
428
- const startTime = new Date(state.workflow.startedAt).getTime();
429
- const endTime = new Date(now).getTime();
369
+ const currentIndex = PHASES.indexOf(flow.phase);
370
+ const nextPhase = PHASES[currentIndex + 1] || 'complete';
430
371
 
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
372
+ // Build updates object
373
+ const updates = {
374
+ phase: nextPhase,
375
+ status: nextPhase === 'complete' ? 'completed' : 'in_progress'
376
+ };
377
+
378
+ // Store result in appropriate field
379
+ if (result) {
380
+ const resultField = getResultField(flow.phase);
381
+ if (resultField) {
382
+ updates[resultField] = result;
446
383
  }
447
- }, baseDir);
384
+ }
385
+
386
+ updateFlow(updates, worktreePath);
387
+ return readFlow(worktreePath);
448
388
  }
449
389
 
450
390
  /**
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
391
+ * Map phase to result field
455
392
  */
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);
393
+ function getResultField(phase) {
394
+ const mapping = {
395
+ 'exploration': 'exploration',
396
+ 'planning': 'plan',
397
+ 'review-loop': 'reviewResult'
398
+ };
399
+ return mapping[phase] || null;
468
400
  }
469
401
 
470
402
  /**
471
- * Delete workflow state (cleanup)
472
- * @param {string} [baseDir=process.cwd()] - Base directory
473
- * @returns {boolean} Success status
403
+ * Mark workflow as failed
474
404
  */
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
- }
405
+ function failWorkflow(error, worktreePath = process.cwd()) {
406
+ return updateFlow({
407
+ status: 'failed',
408
+ error: error?.message || String(error)
409
+ }, worktreePath);
487
410
  }
488
411
 
489
412
  /**
490
- * Check if a workflow is in progress
491
- * @param {string} [baseDir=process.cwd()] - Base directory
492
- * @returns {boolean} True if workflow is active
413
+ * Mark workflow as complete
414
+ * Automatically clears the active task from tasks.json using stored projectPath
415
+ * @param {string} worktreePath - Path to worktree
493
416
  */
494
- function hasActiveWorkflow(baseDir = process.cwd()) {
495
- const state = readState(baseDir);
496
- if (state instanceof Error) return false;
497
- if (!state) return false;
417
+ function completeWorkflow(worktreePath = process.cwd()) {
418
+ const flow = readFlow(worktreePath);
419
+
420
+ // Clear active task from main project if projectPath is stored
421
+ if (flow && flow.projectPath) {
422
+ clearActiveTask(flow.projectPath);
423
+ }
498
424
 
499
- return ['pending', 'in_progress', 'paused'].includes(state.workflow.status);
425
+ return updateFlow({
426
+ phase: 'complete',
427
+ status: 'completed',
428
+ completedAt: new Date().toISOString()
429
+ }, worktreePath);
500
430
  }
501
431
 
502
432
  /**
503
- * Get workflow summary for display
504
- * @param {string} [baseDir=process.cwd()] - Base directory
505
- * @returns {Object|null} Summary object or null
433
+ * Abort workflow
434
+ * Also clears the active task from tasks.json using stored projectPath
506
435
  */
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;
436
+ function abortWorkflow(reason, worktreePath = process.cwd()) {
437
+ const flow = readFlow(worktreePath);
513
438
 
514
- const completedPhases = state.phases.history?.filter(p => p.status === 'completed').length || 0;
515
- const totalPhases = PHASES.length - 1; // Exclude 'complete'
439
+ // Clear active task from main project if projectPath is stored
440
+ if (flow && flow.projectPath) {
441
+ clearActiveTask(flow.projectPath);
442
+ }
516
443
 
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
- };
444
+ return updateFlow({
445
+ status: 'aborted',
446
+ abortReason: reason,
447
+ abortedAt: new Date().toISOString()
448
+ }, worktreePath);
539
449
  }
540
450
 
451
+ // =============================================================================
452
+ // CONVENIENCE FUNCTIONS
453
+ // =============================================================================
454
+
541
455
  /**
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
456
+ * Get workflow summary for display
547
457
  */
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;
458
+ function getFlowSummary(worktreePath = process.cwd()) {
459
+ const flow = readFlow(worktreePath);
460
+ if (!flow) return null;
555
461
 
556
- const agents = state.agents || {
557
- lastRun: {},
558
- totalIterations: 0,
559
- totalIssuesFound: 0,
560
- totalIssuesFixed: 0
462
+ return {
463
+ task: flow.task?.title || 'Unknown',
464
+ taskId: flow.task?.id,
465
+ phase: flow.phase,
466
+ status: flow.status,
467
+ lastUpdate: flow.lastUpdate,
468
+ pr: flow.pr?.number ? `#${flow.pr.number}` : null
561
469
  };
562
-
563
- agents.lastRun[agentName] = result;
564
- agents.totalIssuesFound += result.issues || 0;
565
-
566
- return updateState({ agents }, baseDir);
567
470
  }
568
471
 
569
472
  /**
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
473
+ * Check if workflow can be resumed
574
474
  */
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
- };
475
+ function canResume(worktreePath = process.cwd()) {
476
+ const flow = readFlow(worktreePath);
477
+ if (!flow) return false;
478
+ return flow.status === 'in_progress' && flow.phase !== 'complete';
479
+ }
589
480
 
590
- agents.totalIterations += 1;
591
- agents.totalIssuesFixed += result.fixed || 0;
481
+ // =============================================================================
482
+ // BACKWARDS COMPATIBILITY ALIASES
483
+ // =============================================================================
592
484
 
593
- return updateState({
594
- phases: {
595
- currentIteration: state.phases.currentIteration + 1
596
- },
597
- agents
598
- }, baseDir);
599
- }
485
+ // These maintain compatibility with existing agent code
486
+ const readState = readFlow;
487
+ const writeState = writeFlow;
488
+ const updateState = updateFlow;
489
+ const createState = (type, policy) => createFlow({ id: 'manual', title: 'Manual task', source: 'manual' }, policy);
490
+ const deleteState = deleteFlow;
491
+ const hasActiveWorkflow = hasActiveTask;
492
+ const getWorkflowSummary = getFlowSummary;
600
493
 
601
- // Export all functions
602
494
  module.exports = {
603
495
  // Constants
604
- SCHEMA_VERSION,
605
496
  PHASES,
606
- DEFAULT_POLICY,
607
-
608
- // Core functions
609
- generateWorkflowId,
610
- getStatePath,
611
- ensureStateDir,
612
497
 
613
- // CRUD operations
614
- createState,
615
- readState,
616
- writeState,
617
- updateState,
618
- deleteState,
498
+ // Tasks (main project)
499
+ getTasksPath,
500
+ readTasks,
501
+ writeTasks,
502
+ setActiveTask,
503
+ clearActiveTask,
504
+ hasActiveTask,
505
+
506
+ // Flow (worktree)
507
+ getFlowPath,
508
+ readFlow,
509
+ writeFlow,
510
+ updateFlow,
511
+ createFlow,
512
+ deleteFlow,
619
513
 
620
514
  // Phase management
515
+ isValidPhase,
516
+ setPhase,
621
517
  startPhase,
622
518
  completePhase,
623
519
  failPhase,
624
520
  skipToPhase,
625
-
626
- // Workflow lifecycle
521
+ failWorkflow,
627
522
  completeWorkflow,
628
523
  abortWorkflow,
629
- hasActiveWorkflow,
630
- getWorkflowSummary,
631
524
 
632
- // Agent management
633
- updateAgentResult,
634
- incrementIteration
525
+ // Convenience
526
+ getFlowSummary,
527
+ canResume,
528
+ generateWorkflowId,
529
+
530
+ // Backwards compatibility
531
+ readState,
532
+ writeState,
533
+ updateState,
534
+ createState,
535
+ deleteState,
536
+ hasActiveWorkflow,
537
+ getWorkflowSummary
635
538
  };