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.
Files changed (43) hide show
  1. package/.agent/agents/planner.md +205 -62
  2. package/.agent/contexts/plan-quality-log.md +30 -0
  3. package/.agent/engine/loading-rules.json +37 -3
  4. package/.agent/hooks/hooks.json +10 -0
  5. package/.agent/manifest.json +4 -3
  6. package/.agent/skills/plan-validation/SKILL.md +192 -0
  7. package/.agent/skills/plan-writing/SKILL.md +47 -8
  8. package/.agent/skills/plan-writing/domain-enhancers.md +114 -0
  9. package/.agent/skills/plan-writing/plan-retrospective.md +116 -0
  10. package/.agent/skills/plan-writing/plan-schema.md +119 -0
  11. package/.agent/workflows/plan.md +49 -5
  12. package/README.md +30 -29
  13. package/bin/ag-kit.js +26 -5
  14. package/lib/agent-registry.js +17 -3
  15. package/lib/agent-reputation.js +3 -11
  16. package/lib/circuit-breaker.js +195 -0
  17. package/lib/cli-commands.js +88 -1
  18. package/lib/config-validator.js +274 -0
  19. package/lib/conflict-detector.js +29 -22
  20. package/lib/constants.js +35 -0
  21. package/lib/engineering-manager.js +9 -27
  22. package/lib/error-budget.js +105 -29
  23. package/lib/hook-system.js +8 -4
  24. package/lib/identity.js +22 -27
  25. package/lib/io.js +74 -0
  26. package/lib/loading-engine.js +248 -35
  27. package/lib/logger.js +118 -0
  28. package/lib/marketplace.js +43 -20
  29. package/lib/plugin-system.js +55 -31
  30. package/lib/plugin-verifier.js +197 -0
  31. package/lib/rate-limiter.js +113 -0
  32. package/lib/security-scanner.js +1 -4
  33. package/lib/self-healing.js +58 -24
  34. package/lib/session-manager.js +51 -48
  35. package/lib/skill-sandbox.js +1 -1
  36. package/lib/task-governance.js +10 -11
  37. package/lib/task-model.js +42 -27
  38. package/lib/updater.js +1 -1
  39. package/lib/verify.js +4 -4
  40. package/lib/workflow-engine.js +88 -68
  41. package/lib/workflow-events.js +166 -0
  42. package/lib/workflow-persistence.js +19 -19
  43. package/package.json +2 -2
@@ -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 = '.agent';
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 tempPath = `${filePath}.tmp`;
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 state = loadSessionState(projectRoot);
113
+ const loadedState = loadSessionState(projectRoot);
117
114
  const sessionId = crypto.randomUUID();
118
115
 
119
- state.session = {
120
- id: sessionId,
121
- date: new Date().toISOString(),
122
- focus: focus || null,
123
- status: 'active',
124
- };
125
-
126
- state.repository = {
127
- currentBranch: getGitBranch(projectRoot),
128
- lastCommit: getGitLastCommit(projectRoot),
129
- remoteSynced: false,
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 state = loadSessionState(projectRoot);
143
+ const loadedState = loadSessionState(projectRoot);
145
144
 
146
- if (!state.session || !state.session.id) {
145
+ if (!loadedState.session || !loadedState.session.id) {
147
146
  return { success: false, sessionId: null };
148
147
  }
149
148
 
150
- const sessionId = state.session.id;
151
- state.session.status = 'completed';
152
-
153
- // Archive completed tasks count
154
- state.notes = `Session ${sessionId} completed at ${new Date().toISOString()}`;
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 state = loadSessionState(projectRoot);
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
- if (!Array.isArray(state.openTasks)) {
182
- state.openTasks = [];
183
- }
184
-
185
- state.openTasks.push(task);
186
- state.currentTask = taskId;
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 state = loadSessionState(projectRoot);
201
+ const loadedState = loadSessionState(projectRoot);
202
202
 
203
- if (!Array.isArray(state.openTasks)) {
203
+ if (!Array.isArray(loadedState.openTasks)) {
204
204
  return { success: false };
205
205
  }
206
206
 
207
- const taskIndex = state.openTasks.findIndex((task) => task.id === taskId);
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 [task] = state.openTasks.splice(taskIndex, 1);
214
- task.status = 'completed';
215
- task.completedAt = new Date().toISOString();
216
-
217
- if (!Array.isArray(state.completedTasks)) {
218
- state.completedTasks = [];
219
- }
213
+ const completedTask = {
214
+ ...loadedState.openTasks[taskIndex],
215
+ status: 'completed',
216
+ completedAt: new Date().toISOString(),
217
+ };
220
218
 
221
- state.completedTasks.push(task);
219
+ const remainingTasks = loadedState.openTasks.filter((_, i) => i !== taskIndex);
220
+ const existingCompleted = Array.isArray(loadedState.completedTasks) ? loadedState.completedTasks : [];
222
221
 
223
- // Update currentTask
224
- if (state.currentTask === taskId) {
225
- state.currentTask = state.openTasks.length > 0 ? state.openTasks[0].id : null;
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
 
@@ -14,7 +14,7 @@
14
14
  const fs = require('fs');
15
15
  const path = require('path');
16
16
 
17
- const AGENT_DIR = '.agent';
17
+ const { AGENT_DIR } = require('./constants');
18
18
  const SKILLS_DIR = 'skills';
19
19
 
20
20
  /** Valid permission levels (ordered from least to most privileged) */
@@ -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 = '.agent';
19
- const ENGINE_DIR = 'engine';
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
- const tempPath = `${lockFile}.tmp`;
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
- const tempPath = `${auditPath}.tmp`;
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
- const tempPath = `${auditPath}.tmp`;
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 = '.agent';
19
- const ENGINE_DIR = 'engine';
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
- fs.writeFileSync(tempPath, JSON.stringify(data, null, 2) + '\n', 'utf-8');
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.push(task);
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
- tasks[taskIndex][field] = updates[field];
195
+ fieldUpdates[field] = updates[field];
203
196
  }
204
197
  }
205
198
 
206
- tasks[taskIndex].updatedAt = new Date().toISOString();
207
- writeTasks(projectRoot, tasks);
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: tasks[taskIndex] };
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
- tasks[taskIndex].status = newStatus;
243
- tasks[taskIndex].updatedAt = new Date().toISOString();
244
-
245
- if (newStatus === 'done') {
246
- tasks[taskIndex].completedAt = new Date().toISOString();
247
- }
248
-
249
- writeTasks(projectRoot, tasks);
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[taskIndex].deleted = true;
270
- tasks[taskIndex].updatedAt = new Date().toISOString();
271
- writeTasks(projectRoot, tasks);
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 = '.agent';
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 = '.agent';
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, 'engine', engineFile), `engine:${engineFile}`));
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, 'hooks', 'hooks.json'), 'hooks-json'));
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, 'engine', 'loading-rules.json');
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'));
@@ -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 = 'engine';
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 = state.currentPhase;
182
- const transitions = state.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' && state.phases[currentPhase]) {
217
- state.phases[currentPhase].completedAt = timestamp;
218
- state.phases[currentPhase].status = 'completed';
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 (state.phases[toPhase]) {
223
- state.phases[toPhase].startedAt = timestamp;
224
- state.phases[toPhase].status = 'active';
225
- state.phases[toPhase].completedAt = null;
226
- }
227
-
228
- // Update top-level state
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
- state.history.push({
241
- from: currentPhase,
242
- to: toPhase,
243
- trigger,
244
- timestamp,
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
- // Atomic write: write to temp file, then rename
248
- const tempPath = `${filePath}.tmp`;
258
+ // Emit transition start event
259
+ workflowEvents.emitTransitionStart(currentPhase, toPhase, trigger);
249
260
 
250
261
  try {
251
- fs.writeFileSync(tempPath, JSON.stringify(state, null, 2) + '\n', 'utf-8');
252
- fs.renameSync(tempPath, filePath);
262
+ writeJsonAtomic(filePath, state);
253
263
  } catch (writeError) {
254
- // Clean up temp file if rename failed
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 = state.currentPhase;
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
- if (!preserveHistory) {
305
- state.history = [];
306
- } else {
307
- // Record the reset in history
308
- state.history.push({
309
- from: previousPhase,
310
- to: 'IDLE',
311
- trigger: 'Workflow reset',
312
- timestamp: new Date().toISOString(),
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 tempPath = `${filePath}.tmp`;
317
- fs.writeFileSync(tempPath, JSON.stringify(state, null, 2) + '\n', 'utf-8');
318
- fs.renameSync(tempPath, filePath);
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