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.
- package/.claude-plugin/marketplace.json +6 -6
- package/.claude-plugin/plugin.json +1 -1
- package/CHANGELOG.md +88 -1
- package/README.md +173 -161
- package/SECURITY.md +25 -81
- package/adapters/codex/install.sh +58 -16
- package/adapters/opencode/install.sh +92 -23
- package/lib/index.js +47 -4
- package/lib/patterns/review-patterns.js +58 -11
- package/lib/patterns/slop-patterns.js +154 -147
- package/lib/platform/detect-platform.js +99 -350
- package/lib/platform/detection-configs.js +93 -0
- package/lib/platform/verify-tools.js +10 -78
- package/lib/schemas/README.md +195 -0
- package/lib/schemas/validator.js +247 -0
- package/lib/sources/custom-handler.js +199 -0
- package/lib/sources/policy-questions.js +239 -0
- package/lib/sources/source-cache.js +149 -0
- package/lib/state/workflow-state.js +363 -665
- package/lib/types/README.md +292 -0
- package/lib/types/agent-frontmatter.d.ts +134 -0
- package/lib/types/command-frontmatter.d.ts +107 -0
- package/lib/types/hook-frontmatter.d.ts +115 -0
- package/lib/types/index.d.ts +84 -0
- package/lib/types/plugin-manifest.d.ts +102 -0
- package/lib/types/skill-frontmatter.d.ts +89 -0
- package/lib/utils/cache-manager.js +154 -0
- package/lib/utils/context-optimizer.js +5 -36
- package/lib/utils/deprecation.js +37 -0
- package/lib/utils/shell-escape.js +88 -0
- package/mcp-server/index.js +513 -18
- package/package.json +6 -2
- package/plugins/deslop-around/.claude-plugin/plugin.json +1 -1
- package/plugins/deslop-around/lib/index.js +170 -0
- package/plugins/deslop-around/lib/patterns/review-patterns.js +58 -11
- package/plugins/deslop-around/lib/patterns/slop-patterns.js +170 -129
- package/plugins/deslop-around/lib/platform/detect-platform.js +212 -123
- package/plugins/deslop-around/lib/platform/detection-configs.js +93 -0
- package/plugins/deslop-around/lib/platform/verify-tools.js +10 -1
- package/plugins/deslop-around/lib/schemas/README.md +195 -0
- package/plugins/deslop-around/lib/schemas/validator.js +205 -0
- package/plugins/deslop-around/lib/sources/custom-handler.js +199 -0
- package/plugins/deslop-around/lib/sources/policy-questions.js +239 -0
- package/plugins/deslop-around/lib/sources/source-cache.js +149 -0
- package/plugins/deslop-around/lib/state/workflow-state.js +382 -484
- package/plugins/deslop-around/lib/types/README.md +292 -0
- package/plugins/deslop-around/lib/types/agent-frontmatter.d.ts +134 -0
- package/plugins/deslop-around/lib/types/command-frontmatter.d.ts +107 -0
- package/plugins/deslop-around/lib/types/hook-frontmatter.d.ts +115 -0
- package/plugins/deslop-around/lib/types/index.d.ts +84 -0
- package/plugins/deslop-around/lib/types/plugin-manifest.d.ts +102 -0
- package/plugins/deslop-around/lib/types/skill-frontmatter.d.ts +89 -0
- package/plugins/deslop-around/lib/utils/cache-manager.js +154 -0
- package/plugins/deslop-around/lib/utils/context-optimizer.js +115 -37
- package/plugins/deslop-around/lib/utils/deprecation.js +37 -0
- package/plugins/deslop-around/lib/utils/shell-escape.js +88 -0
- package/plugins/next-task/.claude-plugin/plugin.json +1 -1
- package/plugins/next-task/agents/delivery-validator.md +2 -2
- package/plugins/next-task/agents/implementation-agent.md +3 -4
- package/plugins/next-task/agents/planning-agent.md +77 -19
- package/plugins/next-task/agents/review-orchestrator.md +21 -122
- package/plugins/next-task/agents/task-discoverer.md +164 -23
- package/plugins/next-task/commands/next-task.md +180 -14
- package/plugins/next-task/lib/index.js +170 -0
- package/plugins/next-task/lib/patterns/review-patterns.js +58 -11
- package/plugins/next-task/lib/patterns/slop-patterns.js +170 -129
- package/plugins/next-task/lib/platform/detect-platform.js +212 -123
- package/plugins/next-task/lib/platform/detection-configs.js +93 -0
- package/plugins/next-task/lib/platform/verify-tools.js +10 -1
- package/plugins/next-task/lib/schemas/README.md +195 -0
- package/plugins/next-task/lib/schemas/validator.js +205 -0
- package/plugins/next-task/lib/sources/custom-handler.js +199 -0
- package/plugins/next-task/lib/sources/policy-questions.js +239 -0
- package/plugins/next-task/lib/sources/source-cache.js +149 -0
- package/plugins/next-task/lib/state/workflow-state.js +382 -484
- package/plugins/next-task/lib/types/README.md +292 -0
- package/plugins/next-task/lib/types/agent-frontmatter.d.ts +134 -0
- package/plugins/next-task/lib/types/command-frontmatter.d.ts +107 -0
- package/plugins/next-task/lib/types/hook-frontmatter.d.ts +115 -0
- package/plugins/next-task/lib/types/index.d.ts +84 -0
- package/plugins/next-task/lib/types/plugin-manifest.d.ts +102 -0
- package/plugins/next-task/lib/types/skill-frontmatter.d.ts +89 -0
- package/plugins/next-task/lib/utils/cache-manager.js +154 -0
- package/plugins/next-task/lib/utils/context-optimizer.js +115 -37
- package/plugins/next-task/lib/utils/deprecation.js +37 -0
- package/plugins/next-task/lib/utils/shell-escape.js +88 -0
- package/plugins/project-review/.claude-plugin/plugin.json +1 -1
- package/plugins/project-review/lib/index.js +170 -0
- package/plugins/project-review/lib/patterns/review-patterns.js +58 -11
- package/plugins/project-review/lib/patterns/slop-patterns.js +170 -129
- package/plugins/project-review/lib/platform/detect-platform.js +212 -123
- package/plugins/project-review/lib/platform/detection-configs.js +93 -0
- package/plugins/project-review/lib/platform/verify-tools.js +10 -1
- package/plugins/project-review/lib/schemas/README.md +195 -0
- package/plugins/project-review/lib/schemas/validator.js +205 -0
- package/plugins/project-review/lib/sources/custom-handler.js +199 -0
- package/plugins/project-review/lib/sources/policy-questions.js +239 -0
- package/plugins/project-review/lib/sources/source-cache.js +149 -0
- package/plugins/project-review/lib/state/workflow-state.js +382 -484
- package/plugins/project-review/lib/types/README.md +292 -0
- package/plugins/project-review/lib/types/agent-frontmatter.d.ts +134 -0
- package/plugins/project-review/lib/types/command-frontmatter.d.ts +107 -0
- package/plugins/project-review/lib/types/hook-frontmatter.d.ts +115 -0
- package/plugins/project-review/lib/types/index.d.ts +84 -0
- package/plugins/project-review/lib/types/plugin-manifest.d.ts +102 -0
- package/plugins/project-review/lib/types/skill-frontmatter.d.ts +89 -0
- package/plugins/project-review/lib/utils/cache-manager.js +154 -0
- package/plugins/project-review/lib/utils/context-optimizer.js +115 -37
- package/plugins/project-review/lib/utils/deprecation.js +37 -0
- package/plugins/project-review/lib/utils/shell-escape.js +88 -0
- package/plugins/reality-check/.claude-plugin/plugin.json +1 -1
- package/plugins/reality-check/agents/code-explorer.md +1 -1
- package/plugins/ship/.claude-plugin/plugin.json +1 -1
- package/plugins/ship/lib/index.js +170 -0
- package/plugins/ship/lib/patterns/review-patterns.js +58 -11
- package/plugins/ship/lib/patterns/slop-patterns.js +170 -129
- package/plugins/ship/lib/platform/detect-platform.js +212 -123
- package/plugins/ship/lib/platform/detection-configs.js +93 -0
- package/plugins/ship/lib/platform/verify-tools.js +10 -1
- package/plugins/ship/lib/schemas/README.md +195 -0
- package/plugins/ship/lib/schemas/validator.js +205 -0
- package/plugins/ship/lib/sources/custom-handler.js +199 -0
- package/plugins/ship/lib/sources/policy-questions.js +239 -0
- package/plugins/ship/lib/sources/source-cache.js +149 -0
- package/plugins/ship/lib/state/workflow-state.js +382 -484
- package/plugins/ship/lib/types/README.md +292 -0
- package/plugins/ship/lib/types/agent-frontmatter.d.ts +134 -0
- package/plugins/ship/lib/types/command-frontmatter.d.ts +107 -0
- package/plugins/ship/lib/types/hook-frontmatter.d.ts +115 -0
- package/plugins/ship/lib/types/index.d.ts +84 -0
- package/plugins/ship/lib/types/plugin-manifest.d.ts +102 -0
- package/plugins/ship/lib/types/skill-frontmatter.d.ts +89 -0
- package/plugins/ship/lib/utils/cache-manager.js +154 -0
- package/plugins/ship/lib/utils/context-optimizer.js +115 -37
- package/plugins/ship/lib/utils/deprecation.js +37 -0
- package/plugins/ship/lib/utils/shell-escape.js +88 -0
- package/lib/state/workflow-state.schema.json +0 -282
- package/plugins/deslop-around/lib/state/workflow-state.schema.json +0 -282
- package/plugins/next-task/agents/policy-selector.md +0 -248
- package/plugins/next-task/lib/state/tasks-registry.schema.json +0 -85
- package/plugins/next-task/lib/state/workflow-state.schema.json +0 -282
- package/plugins/next-task/lib/state/worktree-status.schema.json +0 -219
- package/plugins/project-review/lib/state/workflow-state.schema.json +0 -282
- package/plugins/ship/lib/state/workflow-state.schema.json +0 -282
|
@@ -1,130 +1,64 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* Simplified workflow state management
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
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
|
-
|
|
13
|
-
const
|
|
14
|
-
const
|
|
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
|
-
*
|
|
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
|
|
22
|
+
* @throws {Error} If path is invalid
|
|
67
23
|
*/
|
|
68
|
-
function
|
|
69
|
-
if (typeof
|
|
70
|
-
throw new Error('
|
|
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
|
-
|
|
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
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
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
|
-
*
|
|
113
|
-
* @
|
|
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
|
|
118
|
-
const
|
|
119
|
-
const
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
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-
|
|
138
|
-
'
|
|
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
|
-
*
|
|
154
|
-
* @param {string} phaseName - Phase to check
|
|
155
|
-
* @returns {boolean} True if valid phase
|
|
77
|
+
* Ensure .claude directory exists
|
|
156
78
|
*/
|
|
157
|
-
function
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
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
|
-
|
|
171
|
-
|
|
172
|
-
|
|
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
|
-
*
|
|
182
|
-
* @returns {string} Workflow ID in format: workflow-YYYYMMDD-HHMMSS-random
|
|
92
|
+
* Get path to tasks.json with validation
|
|
183
93
|
*/
|
|
184
|
-
function
|
|
185
|
-
const
|
|
186
|
-
const
|
|
187
|
-
|
|
188
|
-
|
|
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
|
-
*
|
|
194
|
-
*
|
|
195
|
-
*
|
|
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
|
|
199
|
-
const
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
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
|
-
*
|
|
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
|
|
214
|
-
|
|
215
|
-
const
|
|
216
|
-
|
|
217
|
-
|
|
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
|
-
*
|
|
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
|
|
232
|
-
const
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
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
|
-
*
|
|
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
|
|
278
|
-
const
|
|
279
|
-
|
|
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
|
-
*
|
|
319
|
-
*
|
|
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
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
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
|
-
|
|
336
|
-
|
|
164
|
+
// =============================================================================
|
|
165
|
+
// FLOW.JSON - Worktree directory
|
|
166
|
+
// =============================================================================
|
|
337
167
|
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
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
|
-
*
|
|
349
|
-
*
|
|
350
|
-
*
|
|
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
|
|
354
|
-
|
|
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
|
-
|
|
361
|
-
|
|
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
|
-
*
|
|
197
|
+
* Write flow.json to worktree
|
|
198
|
+
* Creates a copy to avoid mutating the original object
|
|
377
199
|
*/
|
|
378
|
-
|
|
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
|
-
*
|
|
382
|
-
*
|
|
383
|
-
*
|
|
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
|
|
389
|
-
|
|
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
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
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
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
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
|
-
//
|
|
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
|
-
|
|
232
|
+
flow[key] = value;
|
|
424
233
|
}
|
|
425
234
|
}
|
|
426
235
|
|
|
427
|
-
return
|
|
236
|
+
return writeFlow(flow, worktreePath);
|
|
428
237
|
}
|
|
429
238
|
|
|
430
239
|
/**
|
|
431
|
-
*
|
|
432
|
-
*
|
|
433
|
-
* @param {
|
|
434
|
-
* @
|
|
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
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
*
|
|
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
|
|
478
|
-
const
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
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
|
-
|
|
304
|
+
/**
|
|
305
|
+
* Check if phase is valid
|
|
306
|
+
*/
|
|
307
|
+
function isValidPhase(phase) {
|
|
308
|
+
return PHASES.includes(phase);
|
|
490
309
|
}
|
|
491
310
|
|
|
492
311
|
/**
|
|
493
|
-
*
|
|
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
|
|
499
|
-
|
|
500
|
-
|
|
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
|
-
|
|
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
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
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 = {},
|
|
524
|
-
const
|
|
525
|
-
if (
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
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(
|
|
552
|
-
if (!isValidPhase(
|
|
553
|
-
|
|
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
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
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
|
|
595
|
-
*
|
|
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
|
|
600
|
-
const
|
|
601
|
-
if (
|
|
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
|
|
608
|
-
const
|
|
609
|
-
const endTime = new Date(now).getTime();
|
|
364
|
+
const currentIndex = PHASES.indexOf(flow.phase);
|
|
365
|
+
const nextPhase = PHASES[currentIndex + 1] || 'complete';
|
|
610
366
|
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
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
|
-
}
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
updateFlow(updates, worktreePath);
|
|
382
|
+
return readFlow(worktreePath);
|
|
628
383
|
}
|
|
629
384
|
|
|
630
385
|
/**
|
|
631
|
-
*
|
|
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
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
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
|
-
*
|
|
652
|
-
* @param {string} [baseDir=process.cwd()] - Base directory
|
|
653
|
-
* @returns {boolean} Success status
|
|
398
|
+
* Mark workflow as failed
|
|
654
399
|
*/
|
|
655
|
-
function
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
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
|
-
*
|
|
671
|
-
*
|
|
672
|
-
* @
|
|
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
|
|
675
|
-
const
|
|
676
|
-
|
|
677
|
-
if
|
|
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
|
|
420
|
+
return updateFlow({
|
|
421
|
+
phase: 'complete',
|
|
422
|
+
status: 'completed',
|
|
423
|
+
completedAt: new Date().toISOString()
|
|
424
|
+
}, worktreePath);
|
|
680
425
|
}
|
|
681
426
|
|
|
682
427
|
/**
|
|
683
|
-
*
|
|
684
|
-
*
|
|
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
|
|
688
|
-
const
|
|
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
|
-
|
|
695
|
-
|
|
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
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
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
|
-
*
|
|
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
|
|
729
|
-
const
|
|
730
|
-
if (
|
|
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
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
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
|
-
*
|
|
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
|
|
756
|
-
const
|
|
757
|
-
if (
|
|
758
|
-
|
|
759
|
-
|
|
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
|
-
|
|
771
|
-
|
|
476
|
+
// =============================================================================
|
|
477
|
+
// BACKWARDS COMPATIBILITY ALIASES
|
|
478
|
+
// =============================================================================
|
|
772
479
|
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
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
|
-
//
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
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
|
};
|