antigravity-ai-kit 3.1.1 → 3.2.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/.agent/agents/planner.md +205 -62
- package/.agent/contexts/plan-quality-log.md +30 -0
- package/.agent/engine/loading-rules.json +37 -3
- package/.agent/hooks/hooks.json +10 -0
- package/.agent/manifest.json +4 -3
- package/.agent/skills/plan-validation/SKILL.md +192 -0
- package/.agent/skills/plan-writing/SKILL.md +47 -8
- package/.agent/skills/plan-writing/domain-enhancers.md +114 -0
- package/.agent/skills/plan-writing/plan-retrospective.md +116 -0
- package/.agent/skills/plan-writing/plan-schema.md +119 -0
- package/.agent/workflows/plan.md +49 -5
- package/README.md +30 -29
- package/bin/ag-kit.js +26 -5
- package/lib/agent-registry.js +17 -3
- package/lib/agent-reputation.js +3 -11
- package/lib/circuit-breaker.js +195 -0
- package/lib/cli-commands.js +88 -1
- package/lib/config-validator.js +274 -0
- package/lib/conflict-detector.js +29 -22
- package/lib/constants.js +35 -0
- package/lib/engineering-manager.js +9 -27
- package/lib/error-budget.js +105 -29
- package/lib/hook-system.js +8 -4
- package/lib/identity.js +22 -27
- package/lib/io.js +74 -0
- package/lib/loading-engine.js +248 -35
- package/lib/logger.js +118 -0
- package/lib/marketplace.js +43 -20
- package/lib/plugin-system.js +55 -31
- package/lib/plugin-verifier.js +197 -0
- package/lib/rate-limiter.js +113 -0
- package/lib/security-scanner.js +1 -4
- package/lib/self-healing.js +58 -24
- package/lib/session-manager.js +51 -48
- package/lib/skill-sandbox.js +1 -1
- package/lib/task-governance.js +10 -11
- package/lib/task-model.js +42 -27
- package/lib/updater.js +1 -1
- package/lib/verify.js +4 -4
- package/lib/workflow-engine.js +88 -68
- package/lib/workflow-events.js +166 -0
- package/lib/workflow-persistence.js +19 -19
- package/package.json +2 -2
package/lib/session-manager.js
CHANGED
|
@@ -17,7 +17,8 @@ const path = require('path');
|
|
|
17
17
|
const crypto = require('crypto');
|
|
18
18
|
const { execSync } = require('child_process');
|
|
19
19
|
|
|
20
|
-
const AGENT_DIR = '
|
|
20
|
+
const { AGENT_DIR } = require('./constants');
|
|
21
|
+
const { writeJsonAtomic } = require('./io');
|
|
21
22
|
const STATE_FILENAME = 'session-state.json';
|
|
22
23
|
|
|
23
24
|
/**
|
|
@@ -56,12 +57,8 @@ function loadSessionState(projectRoot) {
|
|
|
56
57
|
*/
|
|
57
58
|
function writeSessionState(projectRoot, state) {
|
|
58
59
|
const filePath = resolveStatePath(projectRoot);
|
|
59
|
-
const
|
|
60
|
-
|
|
61
|
-
state.lastUpdated = new Date().toISOString();
|
|
62
|
-
|
|
63
|
-
fs.writeFileSync(tempPath, JSON.stringify(state, null, 2) + '\n', 'utf-8');
|
|
64
|
-
fs.renameSync(tempPath, filePath);
|
|
60
|
+
const updatedState = { ...state, lastUpdated: new Date().toISOString() };
|
|
61
|
+
writeJsonAtomic(filePath, updatedState);
|
|
65
62
|
}
|
|
66
63
|
|
|
67
64
|
/**
|
|
@@ -113,20 +110,22 @@ function getGitLastCommit(projectRoot) {
|
|
|
113
110
|
* @returns {{ sessionId: string, state: object }}
|
|
114
111
|
*/
|
|
115
112
|
function startSession(projectRoot, focus) {
|
|
116
|
-
const
|
|
113
|
+
const loadedState = loadSessionState(projectRoot);
|
|
117
114
|
const sessionId = crypto.randomUUID();
|
|
118
115
|
|
|
119
|
-
state
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
116
|
+
const state = {
|
|
117
|
+
...loadedState,
|
|
118
|
+
session: {
|
|
119
|
+
id: sessionId,
|
|
120
|
+
date: new Date().toISOString(),
|
|
121
|
+
focus: focus || null,
|
|
122
|
+
status: 'active',
|
|
123
|
+
},
|
|
124
|
+
repository: {
|
|
125
|
+
currentBranch: getGitBranch(projectRoot),
|
|
126
|
+
lastCommit: getGitLastCommit(projectRoot),
|
|
127
|
+
remoteSynced: false,
|
|
128
|
+
},
|
|
130
129
|
};
|
|
131
130
|
|
|
132
131
|
writeSessionState(projectRoot, state);
|
|
@@ -141,17 +140,18 @@ function startSession(projectRoot, focus) {
|
|
|
141
140
|
* @returns {{ success: boolean, sessionId: string | null }}
|
|
142
141
|
*/
|
|
143
142
|
function endSession(projectRoot) {
|
|
144
|
-
const
|
|
143
|
+
const loadedState = loadSessionState(projectRoot);
|
|
145
144
|
|
|
146
|
-
if (!
|
|
145
|
+
if (!loadedState.session || !loadedState.session.id) {
|
|
147
146
|
return { success: false, sessionId: null };
|
|
148
147
|
}
|
|
149
148
|
|
|
150
|
-
const sessionId =
|
|
151
|
-
state
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
149
|
+
const sessionId = loadedState.session.id;
|
|
150
|
+
const state = {
|
|
151
|
+
...loadedState,
|
|
152
|
+
session: { ...loadedState.session, status: 'completed' },
|
|
153
|
+
notes: `Session ${sessionId} completed at ${new Date().toISOString()}`,
|
|
154
|
+
};
|
|
155
155
|
|
|
156
156
|
writeSessionState(projectRoot, state);
|
|
157
157
|
|
|
@@ -167,7 +167,7 @@ function endSession(projectRoot) {
|
|
|
167
167
|
* @returns {{ taskId: string }}
|
|
168
168
|
*/
|
|
169
169
|
function addTask(projectRoot, title, description) {
|
|
170
|
-
const
|
|
170
|
+
const loadedState = loadSessionState(projectRoot);
|
|
171
171
|
const taskId = `TASK-${Date.now().toString(36).toUpperCase()}`;
|
|
172
172
|
|
|
173
173
|
const task = {
|
|
@@ -178,12 +178,12 @@ function addTask(projectRoot, title, description) {
|
|
|
178
178
|
status: 'open',
|
|
179
179
|
};
|
|
180
180
|
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
181
|
+
const existingTasks = Array.isArray(loadedState.openTasks) ? loadedState.openTasks : [];
|
|
182
|
+
const state = {
|
|
183
|
+
...loadedState,
|
|
184
|
+
openTasks: [...existingTasks, task],
|
|
185
|
+
currentTask: taskId,
|
|
186
|
+
};
|
|
187
187
|
|
|
188
188
|
writeSessionState(projectRoot, state);
|
|
189
189
|
|
|
@@ -198,32 +198,35 @@ function addTask(projectRoot, title, description) {
|
|
|
198
198
|
* @returns {{ success: boolean }}
|
|
199
199
|
*/
|
|
200
200
|
function completeTask(projectRoot, taskId) {
|
|
201
|
-
const
|
|
201
|
+
const loadedState = loadSessionState(projectRoot);
|
|
202
202
|
|
|
203
|
-
if (!Array.isArray(
|
|
203
|
+
if (!Array.isArray(loadedState.openTasks)) {
|
|
204
204
|
return { success: false };
|
|
205
205
|
}
|
|
206
206
|
|
|
207
|
-
const taskIndex =
|
|
207
|
+
const taskIndex = loadedState.openTasks.findIndex((task) => task.id === taskId);
|
|
208
208
|
|
|
209
209
|
if (taskIndex === -1) {
|
|
210
210
|
return { success: false };
|
|
211
211
|
}
|
|
212
212
|
|
|
213
|
-
const
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
state.completedTasks = [];
|
|
219
|
-
}
|
|
213
|
+
const completedTask = {
|
|
214
|
+
...loadedState.openTasks[taskIndex],
|
|
215
|
+
status: 'completed',
|
|
216
|
+
completedAt: new Date().toISOString(),
|
|
217
|
+
};
|
|
220
218
|
|
|
221
|
-
|
|
219
|
+
const remainingTasks = loadedState.openTasks.filter((_, i) => i !== taskIndex);
|
|
220
|
+
const existingCompleted = Array.isArray(loadedState.completedTasks) ? loadedState.completedTasks : [];
|
|
222
221
|
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
222
|
+
const state = {
|
|
223
|
+
...loadedState,
|
|
224
|
+
openTasks: remainingTasks,
|
|
225
|
+
completedTasks: [...existingCompleted, completedTask],
|
|
226
|
+
currentTask: loadedState.currentTask === taskId
|
|
227
|
+
? (remainingTasks.length > 0 ? remainingTasks[0].id : null)
|
|
228
|
+
: loadedState.currentTask,
|
|
229
|
+
};
|
|
227
230
|
|
|
228
231
|
writeSessionState(projectRoot, state);
|
|
229
232
|
|
package/lib/skill-sandbox.js
CHANGED
package/lib/task-governance.js
CHANGED
|
@@ -15,8 +15,8 @@ const fs = require('fs');
|
|
|
15
15
|
const path = require('path');
|
|
16
16
|
const taskModel = require('./task-model');
|
|
17
17
|
|
|
18
|
-
const AGENT_DIR = '
|
|
19
|
-
const
|
|
18
|
+
const { AGENT_DIR, ENGINE_DIR } = require('./constants');
|
|
19
|
+
const { writeJsonAtomic } = require('./io');
|
|
20
20
|
const LOCKS_DIR = 'locks';
|
|
21
21
|
const AUDIT_FILE = 'audit-log.json';
|
|
22
22
|
|
|
@@ -67,6 +67,11 @@ function resolveAuditPath(projectRoot) {
|
|
|
67
67
|
* @returns {{ success: boolean, error?: string }}
|
|
68
68
|
*/
|
|
69
69
|
function lockTask(projectRoot, taskId, identityId, reason) {
|
|
70
|
+
// Validate taskId format to prevent path traversal (M-12)
|
|
71
|
+
if (typeof taskId !== 'string' || !/^[a-zA-Z0-9_-]+$/.test(taskId)) {
|
|
72
|
+
return { success: false, error: `Invalid task ID format: ${taskId}` };
|
|
73
|
+
}
|
|
74
|
+
|
|
70
75
|
const task = taskModel.getTask(projectRoot, taskId);
|
|
71
76
|
if (!task) {
|
|
72
77
|
return { success: false, error: `Task not found: ${taskId}` };
|
|
@@ -99,9 +104,7 @@ function lockTask(projectRoot, taskId, identityId, reason) {
|
|
|
99
104
|
reason: reason || 'Working on task',
|
|
100
105
|
};
|
|
101
106
|
|
|
102
|
-
|
|
103
|
-
fs.writeFileSync(tempPath, JSON.stringify(lock, null, 2) + '\n', 'utf-8');
|
|
104
|
-
fs.renameSync(tempPath, lockFile);
|
|
107
|
+
writeJsonAtomic(lockFile, lock);
|
|
105
108
|
|
|
106
109
|
appendAudit(projectRoot, {
|
|
107
110
|
taskId,
|
|
@@ -239,9 +242,7 @@ function appendAudit(projectRoot, entry) {
|
|
|
239
242
|
|
|
240
243
|
auditLog.entries.push(entry);
|
|
241
244
|
|
|
242
|
-
|
|
243
|
-
fs.writeFileSync(tempPath, JSON.stringify(auditLog, null, 2) + '\n', 'utf-8');
|
|
244
|
-
fs.renameSync(tempPath, auditPath);
|
|
245
|
+
writeJsonAtomic(auditPath, auditLog);
|
|
245
246
|
}
|
|
246
247
|
|
|
247
248
|
/**
|
|
@@ -395,9 +396,7 @@ function recordDecision(projectRoot, { actor, actorType, action, files, outcome,
|
|
|
395
396
|
// Rotate if needed
|
|
396
397
|
auditLog = rotateIfNeeded(projectRoot, auditLog);
|
|
397
398
|
|
|
398
|
-
|
|
399
|
-
fs.writeFileSync(tempPath, JSON.stringify(auditLog, null, 2) + '\n', 'utf-8');
|
|
400
|
-
fs.renameSync(tempPath, auditPath);
|
|
399
|
+
writeJsonAtomic(auditPath, auditLog);
|
|
401
400
|
|
|
402
401
|
return entry;
|
|
403
402
|
}
|
package/lib/task-model.js
CHANGED
|
@@ -15,8 +15,8 @@ const fs = require('fs');
|
|
|
15
15
|
const path = require('path');
|
|
16
16
|
const crypto = require('crypto');
|
|
17
17
|
|
|
18
|
-
const AGENT_DIR = '
|
|
19
|
-
const
|
|
18
|
+
const { AGENT_DIR, ENGINE_DIR } = require('./constants');
|
|
19
|
+
const { writeJsonAtomic } = require('./io');
|
|
20
20
|
const TASKS_FILE = 'tasks.json';
|
|
21
21
|
|
|
22
22
|
/** Valid task status values */
|
|
@@ -88,12 +88,6 @@ function loadTasks(projectRoot) {
|
|
|
88
88
|
*/
|
|
89
89
|
function writeTasks(projectRoot, tasks) {
|
|
90
90
|
const tasksPath = resolveTasksPath(projectRoot);
|
|
91
|
-
const tempPath = `${tasksPath}.tmp`;
|
|
92
|
-
const dir = path.dirname(tasksPath);
|
|
93
|
-
|
|
94
|
-
if (!fs.existsSync(dir)) {
|
|
95
|
-
fs.mkdirSync(dir, { recursive: true });
|
|
96
|
-
}
|
|
97
91
|
|
|
98
92
|
const data = {
|
|
99
93
|
schemaVersion: '1.0.0',
|
|
@@ -101,8 +95,7 @@ function writeTasks(projectRoot, tasks) {
|
|
|
101
95
|
tasks,
|
|
102
96
|
};
|
|
103
97
|
|
|
104
|
-
|
|
105
|
-
fs.renameSync(tempPath, tasksPath);
|
|
98
|
+
writeJsonAtomic(tasksPath, data);
|
|
106
99
|
}
|
|
107
100
|
|
|
108
101
|
/**
|
|
@@ -136,8 +129,7 @@ function createTask(projectRoot, { title, description, assignee, priority }) {
|
|
|
136
129
|
deleted: false,
|
|
137
130
|
};
|
|
138
131
|
|
|
139
|
-
tasks
|
|
140
|
-
writeTasks(projectRoot, tasks);
|
|
132
|
+
writeTasks(projectRoot, [...tasks, task]);
|
|
141
133
|
|
|
142
134
|
return task;
|
|
143
135
|
}
|
|
@@ -197,16 +189,18 @@ function updateTask(projectRoot, taskId, updates) {
|
|
|
197
189
|
}
|
|
198
190
|
|
|
199
191
|
const allowedFields = ['title', 'description', 'assignee', 'priority'];
|
|
192
|
+
const fieldUpdates = {};
|
|
200
193
|
for (const field of allowedFields) {
|
|
201
194
|
if (updates[field] !== undefined) {
|
|
202
|
-
|
|
195
|
+
fieldUpdates[field] = updates[field];
|
|
203
196
|
}
|
|
204
197
|
}
|
|
205
198
|
|
|
206
|
-
tasks[taskIndex]
|
|
207
|
-
|
|
199
|
+
const updatedTask = { ...tasks[taskIndex], ...fieldUpdates, updatedAt: new Date().toISOString() };
|
|
200
|
+
const updatedTasks = tasks.map((t, i) => (i === taskIndex ? updatedTask : t));
|
|
201
|
+
writeTasks(projectRoot, updatedTasks);
|
|
208
202
|
|
|
209
|
-
return { success: true, task:
|
|
203
|
+
return { success: true, task: updatedTask };
|
|
210
204
|
}
|
|
211
205
|
|
|
212
206
|
/**
|
|
@@ -239,14 +233,15 @@ function transitionTask(projectRoot, taskId, newStatus) {
|
|
|
239
233
|
};
|
|
240
234
|
}
|
|
241
235
|
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
236
|
+
const now = new Date().toISOString();
|
|
237
|
+
const updatedTask = {
|
|
238
|
+
...tasks[taskIndex],
|
|
239
|
+
status: newStatus,
|
|
240
|
+
updatedAt: now,
|
|
241
|
+
completedAt: newStatus === 'done' ? now : tasks[taskIndex].completedAt,
|
|
242
|
+
};
|
|
243
|
+
const updatedTasks = tasks.map((t, i) => (i === taskIndex ? updatedTask : t));
|
|
244
|
+
writeTasks(projectRoot, updatedTasks);
|
|
250
245
|
|
|
251
246
|
return { success: true };
|
|
252
247
|
}
|
|
@@ -266,9 +261,10 @@ function deleteTask(projectRoot, taskId) {
|
|
|
266
261
|
return { success: false };
|
|
267
262
|
}
|
|
268
263
|
|
|
269
|
-
tasks
|
|
270
|
-
|
|
271
|
-
|
|
264
|
+
const updatedTasks = tasks.map((t, i) =>
|
|
265
|
+
i === taskIndex ? { ...t, deleted: true, updatedAt: new Date().toISOString() } : t
|
|
266
|
+
);
|
|
267
|
+
writeTasks(projectRoot, updatedTasks);
|
|
272
268
|
|
|
273
269
|
return { success: true };
|
|
274
270
|
}
|
|
@@ -306,6 +302,23 @@ function getTaskMetrics(projectRoot) {
|
|
|
306
302
|
};
|
|
307
303
|
}
|
|
308
304
|
|
|
305
|
+
/** Priority sort order: critical > high > medium > low */
|
|
306
|
+
const PRIORITY_ORDER = { critical: 0, high: 1, medium: 2, low: 3 };
|
|
307
|
+
|
|
308
|
+
/**
|
|
309
|
+
* Sorts tasks by priority (critical first, low last).
|
|
310
|
+
*
|
|
311
|
+
* @param {Task[]} tasks - Tasks to sort
|
|
312
|
+
* @returns {Task[]} New sorted array (does not mutate input)
|
|
313
|
+
*/
|
|
314
|
+
function sortByPriority(tasks) {
|
|
315
|
+
return [...tasks].sort((a, b) => {
|
|
316
|
+
const aPriority = PRIORITY_ORDER[a.priority] ?? 2;
|
|
317
|
+
const bPriority = PRIORITY_ORDER[b.priority] ?? 2;
|
|
318
|
+
return aPriority - bPriority;
|
|
319
|
+
});
|
|
320
|
+
}
|
|
321
|
+
|
|
309
322
|
module.exports = {
|
|
310
323
|
createTask,
|
|
311
324
|
getTask,
|
|
@@ -314,4 +327,6 @@ module.exports = {
|
|
|
314
327
|
transitionTask,
|
|
315
328
|
deleteTask,
|
|
316
329
|
getTaskMetrics,
|
|
330
|
+
sortByPriority,
|
|
331
|
+
PRIORITY_ORDER,
|
|
317
332
|
};
|
package/lib/updater.js
CHANGED
|
@@ -16,7 +16,7 @@ const fs = require('fs');
|
|
|
16
16
|
const path = require('path');
|
|
17
17
|
const crypto = require('crypto');
|
|
18
18
|
|
|
19
|
-
const AGENT_DIR = '
|
|
19
|
+
const { AGENT_DIR } = require('./constants');
|
|
20
20
|
|
|
21
21
|
/** Files that should never be overwritten during updates */
|
|
22
22
|
const PRESERVED_FILES = new Set([
|
package/lib/verify.js
CHANGED
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
const fs = require('fs');
|
|
16
16
|
const path = require('path');
|
|
17
17
|
|
|
18
|
-
const AGENT_DIR = '
|
|
18
|
+
const { AGENT_DIR, ENGINE_DIR, HOOKS_DIR } = require('./constants');
|
|
19
19
|
|
|
20
20
|
/**
|
|
21
21
|
* @typedef {object} CheckResult
|
|
@@ -186,14 +186,14 @@ function runAllChecks(projectRoot) {
|
|
|
186
186
|
// --- Check 10: Engine JSON files valid ---
|
|
187
187
|
const engineFiles = ['workflow-state.json', 'loading-rules.json', 'sdlc-map.json', 'reliability-config.json'];
|
|
188
188
|
for (const engineFile of engineFiles) {
|
|
189
|
-
results.push(checkJsonFile(path.join(agentDir,
|
|
189
|
+
results.push(checkJsonFile(path.join(agentDir, ENGINE_DIR, engineFile), `engine:${engineFile}`));
|
|
190
190
|
}
|
|
191
191
|
|
|
192
192
|
// --- Check 11: Hooks file valid ---
|
|
193
|
-
results.push(checkJsonFile(path.join(agentDir,
|
|
193
|
+
results.push(checkJsonFile(path.join(agentDir, HOOKS_DIR, 'hooks.json'), 'hooks-json'));
|
|
194
194
|
|
|
195
195
|
// --- Check 12: Cross-reference — loading-rules agents exist in manifest ---
|
|
196
|
-
const loadingRulesPath = path.join(agentDir,
|
|
196
|
+
const loadingRulesPath = path.join(agentDir, ENGINE_DIR, 'loading-rules.json');
|
|
197
197
|
if (fs.existsSync(loadingRulesPath)) {
|
|
198
198
|
try {
|
|
199
199
|
const loadingRules = JSON.parse(fs.readFileSync(loadingRulesPath, 'utf-8'));
|
package/lib/workflow-engine.js
CHANGED
|
@@ -14,6 +14,10 @@
|
|
|
14
14
|
|
|
15
15
|
const fs = require('fs');
|
|
16
16
|
const path = require('path');
|
|
17
|
+
const workflowEvents = require('./workflow-events');
|
|
18
|
+
const { writeJsonAtomic } = require('./io');
|
|
19
|
+
const { createLogger } = require('./logger');
|
|
20
|
+
const log = createLogger('workflow-engine');
|
|
17
21
|
|
|
18
22
|
/** @typedef {'IDLE' | 'EXPLORE' | 'PLAN' | 'IMPLEMENT' | 'VERIFY' | 'REVIEW' | 'DEPLOY' | 'MAINTAIN'} WorkflowPhase */
|
|
19
23
|
|
|
@@ -37,8 +41,7 @@ const path = require('path');
|
|
|
37
41
|
*/
|
|
38
42
|
|
|
39
43
|
const WORKFLOW_STATE_FILENAME = 'workflow-state.json';
|
|
40
|
-
const ENGINE_DIR = '
|
|
41
|
-
const AGENT_DIR = '.agent';
|
|
44
|
+
const { AGENT_DIR, ENGINE_DIR } = require('./constants');
|
|
42
45
|
|
|
43
46
|
/**
|
|
44
47
|
* Resolves the absolute path to workflow-state.json for a given project root.
|
|
@@ -177,9 +180,9 @@ function validateTransition(projectRoot, toPhase) {
|
|
|
177
180
|
* @returns {TransitionResult} Result of the transition attempt
|
|
178
181
|
*/
|
|
179
182
|
function executeTransition(projectRoot, toPhase, triggerOverride) {
|
|
180
|
-
const { state, filePath } = loadWorkflowState(projectRoot);
|
|
181
|
-
const currentPhase =
|
|
182
|
-
const transitions =
|
|
183
|
+
const { state: loadedState, filePath } = loadWorkflowState(projectRoot);
|
|
184
|
+
const currentPhase = loadedState.currentPhase;
|
|
185
|
+
const transitions = loadedState.transitions || [];
|
|
183
186
|
|
|
184
187
|
if (currentPhase === toPhase) {
|
|
185
188
|
return {
|
|
@@ -212,53 +215,53 @@ function executeTransition(projectRoot, toPhase, triggerOverride) {
|
|
|
212
215
|
const timestamp = new Date().toISOString();
|
|
213
216
|
const trigger = triggerOverride || match.trigger;
|
|
214
217
|
|
|
218
|
+
// Build updated state immutably
|
|
219
|
+
const updatedPhases = { ...loadedState.phases };
|
|
220
|
+
|
|
215
221
|
// Update previous phase completion timestamp
|
|
216
|
-
if (currentPhase !== 'IDLE' &&
|
|
217
|
-
|
|
218
|
-
|
|
222
|
+
if (currentPhase !== 'IDLE' && updatedPhases[currentPhase]) {
|
|
223
|
+
updatedPhases[currentPhase] = {
|
|
224
|
+
...updatedPhases[currentPhase],
|
|
225
|
+
completedAt: timestamp,
|
|
226
|
+
status: 'completed',
|
|
227
|
+
};
|
|
219
228
|
}
|
|
220
229
|
|
|
221
230
|
// Update target phase start timestamp
|
|
222
|
-
if (
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
state.currentPhase = toPhase;
|
|
230
|
-
|
|
231
|
-
if (!state.startedAt && currentPhase === 'IDLE') {
|
|
232
|
-
state.startedAt = timestamp;
|
|
233
|
-
}
|
|
234
|
-
|
|
235
|
-
// Append to history
|
|
236
|
-
if (!Array.isArray(state.history)) {
|
|
237
|
-
state.history = [];
|
|
231
|
+
if (updatedPhases[toPhase]) {
|
|
232
|
+
updatedPhases[toPhase] = {
|
|
233
|
+
...updatedPhases[toPhase],
|
|
234
|
+
startedAt: timestamp,
|
|
235
|
+
status: 'active',
|
|
236
|
+
completedAt: null,
|
|
237
|
+
};
|
|
238
238
|
}
|
|
239
239
|
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
240
|
+
const existingHistory = Array.isArray(loadedState.history) ? loadedState.history : [];
|
|
241
|
+
|
|
242
|
+
const state = {
|
|
243
|
+
...loadedState,
|
|
244
|
+
currentPhase: toPhase,
|
|
245
|
+
startedAt: (!loadedState.startedAt && currentPhase === 'IDLE') ? timestamp : loadedState.startedAt,
|
|
246
|
+
phases: updatedPhases,
|
|
247
|
+
history: [
|
|
248
|
+
...existingHistory,
|
|
249
|
+
{
|
|
250
|
+
from: currentPhase,
|
|
251
|
+
to: toPhase,
|
|
252
|
+
trigger,
|
|
253
|
+
timestamp,
|
|
254
|
+
},
|
|
255
|
+
],
|
|
256
|
+
};
|
|
246
257
|
|
|
247
|
-
//
|
|
248
|
-
|
|
258
|
+
// Emit transition start event
|
|
259
|
+
workflowEvents.emitTransitionStart(currentPhase, toPhase, trigger);
|
|
249
260
|
|
|
250
261
|
try {
|
|
251
|
-
|
|
252
|
-
fs.renameSync(tempPath, filePath);
|
|
262
|
+
writeJsonAtomic(filePath, state);
|
|
253
263
|
} catch (writeError) {
|
|
254
|
-
|
|
255
|
-
if (fs.existsSync(tempPath)) {
|
|
256
|
-
try {
|
|
257
|
-
fs.unlinkSync(tempPath);
|
|
258
|
-
} catch {
|
|
259
|
-
// Swallow cleanup errors
|
|
260
|
-
}
|
|
261
|
-
}
|
|
264
|
+
workflowEvents.emitTransitionFailed(currentPhase, toPhase, writeError.message);
|
|
262
265
|
return {
|
|
263
266
|
success: false,
|
|
264
267
|
fromPhase: currentPhase,
|
|
@@ -269,6 +272,9 @@ function executeTransition(projectRoot, toPhase, triggerOverride) {
|
|
|
269
272
|
};
|
|
270
273
|
}
|
|
271
274
|
|
|
275
|
+
// Emit transition complete event
|
|
276
|
+
workflowEvents.emitTransitionComplete(currentPhase, toPhase, trigger);
|
|
277
|
+
|
|
272
278
|
return {
|
|
273
279
|
success: true,
|
|
274
280
|
fromPhase: currentPhase,
|
|
@@ -287,36 +293,50 @@ function executeTransition(projectRoot, toPhase, triggerOverride) {
|
|
|
287
293
|
* @returns {{ success: boolean, previousPhase: string }}
|
|
288
294
|
*/
|
|
289
295
|
function resetWorkflow(projectRoot, preserveHistory = true) {
|
|
290
|
-
const { state, filePath } = loadWorkflowState(projectRoot);
|
|
291
|
-
const previousPhase =
|
|
292
|
-
|
|
293
|
-
state.currentPhase = 'IDLE';
|
|
294
|
-
state.startedAt = null;
|
|
295
|
-
|
|
296
|
-
// Reset all phase records
|
|
297
|
-
for (const phaseName of Object.keys(state.phases || {})) {
|
|
298
|
-
state.phases[phaseName].status = 'pending';
|
|
299
|
-
state.phases[phaseName].startedAt = null;
|
|
300
|
-
state.phases[phaseName].completedAt = null;
|
|
301
|
-
state.phases[phaseName].artifact = null;
|
|
302
|
-
}
|
|
296
|
+
const { state: loadedState, filePath } = loadWorkflowState(projectRoot);
|
|
297
|
+
const previousPhase = loadedState.currentPhase;
|
|
298
|
+
const timestamp = new Date().toISOString();
|
|
303
299
|
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
}
|
|
300
|
+
// Build reset phases immutably
|
|
301
|
+
const resetPhases = {};
|
|
302
|
+
for (const phaseName of Object.keys(loadedState.phases || {})) {
|
|
303
|
+
resetPhases[phaseName] = {
|
|
304
|
+
...loadedState.phases[phaseName],
|
|
305
|
+
status: 'pending',
|
|
306
|
+
startedAt: null,
|
|
307
|
+
completedAt: null,
|
|
308
|
+
artifact: null,
|
|
309
|
+
};
|
|
314
310
|
}
|
|
315
311
|
|
|
316
|
-
const
|
|
317
|
-
|
|
318
|
-
|
|
312
|
+
const existingHistory = Array.isArray(loadedState.history) ? loadedState.history : [];
|
|
313
|
+
const history = preserveHistory
|
|
314
|
+
? [
|
|
315
|
+
...existingHistory,
|
|
316
|
+
{
|
|
317
|
+
from: previousPhase,
|
|
318
|
+
to: 'IDLE',
|
|
319
|
+
trigger: 'Workflow reset',
|
|
320
|
+
timestamp,
|
|
321
|
+
},
|
|
322
|
+
]
|
|
323
|
+
: [];
|
|
324
|
+
|
|
325
|
+
const updatedState = {
|
|
326
|
+
...loadedState,
|
|
327
|
+
currentPhase: 'IDLE',
|
|
328
|
+
startedAt: null,
|
|
329
|
+
phases: resetPhases,
|
|
330
|
+
history,
|
|
331
|
+
};
|
|
332
|
+
|
|
333
|
+
try {
|
|
334
|
+
writeJsonAtomic(filePath, updatedState);
|
|
335
|
+
} catch (writeError) {
|
|
336
|
+
return { success: false, previousPhase, error: `Failed to write state: ${writeError.message}` };
|
|
337
|
+
}
|
|
319
338
|
|
|
339
|
+
workflowEvents.emitWorkflowReset(previousPhase);
|
|
320
340
|
return { success: true, previousPhase };
|
|
321
341
|
}
|
|
322
342
|
|