antigravity-ai-kit 2.1.0 → 3.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (114) hide show
  1. package/.agent/README.md +4 -4
  2. package/.agent/agents/README.md +16 -12
  3. package/.agent/agents/architect.md +1 -0
  4. package/.agent/agents/backend-specialist.md +11 -0
  5. package/.agent/agents/code-reviewer.md +1 -0
  6. package/.agent/agents/database-architect.md +11 -0
  7. package/.agent/agents/devops-engineer.md +11 -0
  8. package/.agent/agents/e2e-runner.md +1 -0
  9. package/.agent/agents/explorer-agent.md +11 -0
  10. package/.agent/agents/frontend-specialist.md +11 -0
  11. package/.agent/agents/mobile-developer.md +11 -0
  12. package/.agent/agents/performance-optimizer.md +11 -0
  13. package/.agent/agents/planner.md +1 -0
  14. package/.agent/agents/refactor-cleaner.md +1 -0
  15. package/.agent/agents/reliability-engineer.md +11 -0
  16. package/.agent/agents/security-reviewer.md +1 -0
  17. package/.agent/agents/sprint-orchestrator.md +10 -0
  18. package/.agent/agents/tdd-guide.md +1 -0
  19. package/.agent/commands/code-review.md +1 -0
  20. package/.agent/commands/debug.md +1 -0
  21. package/.agent/commands/deploy.md +1 -0
  22. package/.agent/commands/help.md +252 -31
  23. package/.agent/commands/plan.md +1 -0
  24. package/.agent/commands/status.md +1 -0
  25. package/.agent/commands/tdd.md +1 -0
  26. package/.agent/contexts/brainstorm.md +26 -0
  27. package/.agent/contexts/debug.md +28 -0
  28. package/.agent/contexts/implement.md +29 -0
  29. package/.agent/contexts/review.md +27 -0
  30. package/.agent/contexts/ship.md +28 -0
  31. package/.agent/engine/identity.json +13 -0
  32. package/.agent/engine/loading-rules.json +23 -1
  33. package/.agent/engine/marketplace-index.json +29 -0
  34. package/.agent/engine/reliability-config.json +14 -0
  35. package/.agent/engine/sdlc-map.json +44 -0
  36. package/.agent/engine/workflow-state.json +28 -2
  37. package/.agent/hooks/hooks.json +27 -25
  38. package/.agent/manifest.json +12 -4
  39. package/.agent/rules.md +2 -1
  40. package/.agent/skills/README.md +10 -5
  41. package/.agent/skills/i18n-localization/SKILL.md +191 -0
  42. package/.agent/skills/mcp-integration/SKILL.md +224 -0
  43. package/.agent/skills/parallel-agents/SKILL.md +1 -1
  44. package/.agent/skills/shell-conventions/SKILL.md +92 -0
  45. package/.agent/skills/ui-ux-pro-max/SKILL.md +557 -0
  46. package/.agent/skills/ui-ux-pro-max/data/charts.csv +26 -0
  47. package/.agent/skills/ui-ux-pro-max/data/colors.csv +97 -0
  48. package/.agent/skills/ui-ux-pro-max/data/icons.csv +101 -0
  49. package/.agent/skills/ui-ux-pro-max/data/landing.csv +31 -0
  50. package/.agent/skills/ui-ux-pro-max/data/products.csv +97 -0
  51. package/.agent/skills/ui-ux-pro-max/data/react-performance.csv +45 -0
  52. package/.agent/skills/ui-ux-pro-max/data/stacks/astro.csv +54 -0
  53. package/.agent/skills/ui-ux-pro-max/data/stacks/flutter.csv +53 -0
  54. package/.agent/skills/ui-ux-pro-max/data/stacks/html-tailwind.csv +56 -0
  55. package/.agent/skills/ui-ux-pro-max/data/stacks/jetpack-compose.csv +53 -0
  56. package/.agent/skills/ui-ux-pro-max/data/stacks/nextjs.csv +53 -0
  57. package/.agent/skills/ui-ux-pro-max/data/stacks/nuxt-ui.csv +51 -0
  58. package/.agent/skills/ui-ux-pro-max/data/stacks/nuxtjs.csv +59 -0
  59. package/.agent/skills/ui-ux-pro-max/data/stacks/react-native.csv +52 -0
  60. package/.agent/skills/ui-ux-pro-max/data/stacks/react.csv +54 -0
  61. package/.agent/skills/ui-ux-pro-max/data/stacks/shadcn.csv +61 -0
  62. package/.agent/skills/ui-ux-pro-max/data/stacks/svelte.csv +54 -0
  63. package/.agent/skills/ui-ux-pro-max/data/stacks/swiftui.csv +51 -0
  64. package/.agent/skills/ui-ux-pro-max/data/stacks/vue.csv +50 -0
  65. package/.agent/skills/ui-ux-pro-max/data/styles.csv +68 -0
  66. package/.agent/skills/ui-ux-pro-max/data/typography.csv +58 -0
  67. package/.agent/skills/ui-ux-pro-max/data/ui-reasoning.csv +101 -0
  68. package/.agent/skills/ui-ux-pro-max/data/ux-guidelines.csv +100 -0
  69. package/.agent/skills/ui-ux-pro-max/data/web-interface.csv +31 -0
  70. package/.agent/skills/ui-ux-pro-max/scripts/core.py +253 -0
  71. package/.agent/skills/ui-ux-pro-max/scripts/design_system.py +1067 -0
  72. package/.agent/skills/ui-ux-pro-max/scripts/search.py +114 -0
  73. package/.agent/templates/adr-template.md +32 -0
  74. package/.agent/templates/bug-report.md +37 -0
  75. package/.agent/templates/feature-request.md +32 -0
  76. package/.agent/workflows/README.md +92 -78
  77. package/.agent/workflows/brainstorm.md +154 -100
  78. package/.agent/workflows/create.md +142 -75
  79. package/.agent/workflows/debug.md +157 -98
  80. package/.agent/workflows/deploy.md +195 -144
  81. package/.agent/workflows/enhance.md +157 -65
  82. package/.agent/workflows/orchestrate.md +171 -114
  83. package/.agent/workflows/plan.md +147 -72
  84. package/.agent/workflows/preview.md +140 -83
  85. package/.agent/workflows/quality-gate.md +196 -0
  86. package/.agent/workflows/retrospective.md +197 -0
  87. package/.agent/workflows/review.md +188 -0
  88. package/.agent/workflows/status.md +142 -91
  89. package/.agent/workflows/test.md +168 -95
  90. package/.agent/workflows/ui-ux-pro-max.md +181 -127
  91. package/README.md +215 -78
  92. package/bin/ag-kit.js +344 -10
  93. package/lib/agent-registry.js +214 -0
  94. package/lib/agent-reputation.js +351 -0
  95. package/lib/cli-commands.js +235 -0
  96. package/lib/conflict-detector.js +245 -0
  97. package/lib/engineering-manager.js +354 -0
  98. package/lib/error-budget.js +294 -0
  99. package/lib/hook-system.js +252 -0
  100. package/lib/identity.js +245 -0
  101. package/lib/loading-engine.js +208 -0
  102. package/lib/marketplace.js +298 -0
  103. package/lib/plugin-system.js +604 -0
  104. package/lib/security-scanner.js +309 -0
  105. package/lib/self-healing.js +434 -0
  106. package/lib/session-manager.js +261 -0
  107. package/lib/skill-sandbox.js +244 -0
  108. package/lib/task-governance.js +523 -0
  109. package/lib/task-model.js +317 -0
  110. package/lib/updater.js +201 -0
  111. package/lib/verify.js +240 -0
  112. package/lib/workflow-engine.js +353 -0
  113. package/lib/workflow-persistence.js +160 -0
  114. package/package.json +7 -3
package/lib/verify.js ADDED
@@ -0,0 +1,240 @@
1
+ /**
2
+ * Antigravity AI Kit — Manifest Verification
3
+ *
4
+ * Validates the integrity of the .agent/ framework by checking
5
+ * manifest ↔ filesystem consistency, JSON validity, and
6
+ * cross-reference integrity.
7
+ *
8
+ * @module lib/verify
9
+ * @author Emre Dursun
10
+ * @since v3.0.0
11
+ */
12
+
13
+ 'use strict';
14
+
15
+ const fs = require('fs');
16
+ const path = require('path');
17
+
18
+ const AGENT_DIR = '.agent';
19
+
20
+ /**
21
+ * @typedef {object} CheckResult
22
+ * @property {string} name - Check name
23
+ * @property {'pass' | 'fail' | 'warn'} status - Result status
24
+ * @property {string} message - Human-readable result message
25
+ */
26
+
27
+ /**
28
+ * @typedef {object} VerificationReport
29
+ * @property {number} passed - Number of passed checks
30
+ * @property {number} failed - Number of failed checks
31
+ * @property {number} warnings - Number of warnings
32
+ * @property {CheckResult[]} results - Individual check results
33
+ */
34
+
35
+ /**
36
+ * Checks that a JSON file exists and is valid.
37
+ *
38
+ * @param {string} filePath - Absolute path to JSON file
39
+ * @param {string} checkName - Name for the check result
40
+ * @returns {CheckResult}
41
+ */
42
+ function checkJsonFile(filePath, checkName) {
43
+ if (!fs.existsSync(filePath)) {
44
+ return { name: checkName, status: 'fail', message: `File not found: ${filePath}` };
45
+ }
46
+
47
+ try {
48
+ const raw = fs.readFileSync(filePath, 'utf-8');
49
+ JSON.parse(raw);
50
+ return { name: checkName, status: 'pass', message: `Valid JSON: ${path.basename(filePath)}` };
51
+ } catch (parseError) {
52
+ return { name: checkName, status: 'fail', message: `Invalid JSON: ${parseError.message}` };
53
+ }
54
+ }
55
+
56
+ /**
57
+ * Runs all manifest integrity checks.
58
+ *
59
+ * @param {string} projectRoot - Root directory of the project
60
+ * @returns {VerificationReport}
61
+ */
62
+ function runAllChecks(projectRoot) {
63
+ const agentDir = path.join(projectRoot, AGENT_DIR);
64
+ /** @type {CheckResult[]} */
65
+ const results = [];
66
+
67
+ // --- Check 1: Manifest exists and is valid JSON ---
68
+ const manifestPath = path.join(agentDir, 'manifest.json');
69
+ results.push(checkJsonFile(manifestPath, 'manifest-exists'));
70
+
71
+ if (!fs.existsSync(manifestPath)) {
72
+ return buildReport(results);
73
+ }
74
+
75
+ /** @type {object} */
76
+ let manifest;
77
+ try {
78
+ manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf-8'));
79
+ } catch {
80
+ return buildReport(results);
81
+ }
82
+
83
+ // --- Check 2: Schema version is valid ---
84
+ const schemaVersion = manifest.schemaVersion || '';
85
+ const semverPattern = /^\d+\.\d+\.\d+$/;
86
+ results.push({
87
+ name: 'schema-version',
88
+ status: semverPattern.test(schemaVersion) ? 'pass' : 'fail',
89
+ message: semverPattern.test(schemaVersion)
90
+ ? `Schema version valid: ${schemaVersion}`
91
+ : `Invalid schema version: "${schemaVersion}"`,
92
+ });
93
+
94
+ // --- Check 3: Agent files exist ---
95
+ const agents = manifest.capabilities?.agents?.items || [];
96
+ for (const agent of agents) {
97
+ const agentPath = path.join(agentDir, agent.file);
98
+ const exists = fs.existsSync(agentPath);
99
+ results.push({
100
+ name: `agent-file:${agent.name}`,
101
+ status: exists ? 'pass' : 'fail',
102
+ message: exists ? `Agent exists: ${agent.name}` : `Missing agent file: ${agent.file}`,
103
+ });
104
+ }
105
+
106
+ // --- Check 4: Agent count matches ---
107
+ const agentCountManifest = manifest.capabilities?.agents?.count || 0;
108
+ const agentCountFS = fs.existsSync(path.join(agentDir, 'agents'))
109
+ ? fs.readdirSync(path.join(agentDir, 'agents')).filter((f) => f.endsWith('.md') && f !== 'README.md').length
110
+ : 0;
111
+ results.push({
112
+ name: 'agent-count',
113
+ status: agentCountManifest === agentCountFS ? 'pass' : 'fail',
114
+ message:
115
+ agentCountManifest === agentCountFS
116
+ ? `Agent count matches: ${agentCountFS}`
117
+ : `Agent count mismatch: manifest=${agentCountManifest}, filesystem=${agentCountFS}`,
118
+ });
119
+
120
+ // --- Check 5: Skill directories and SKILL.md exist ---
121
+ const skills = manifest.capabilities?.skills?.items || [];
122
+ for (const skill of skills) {
123
+ const skillPath = path.join(agentDir, skill.directory, 'SKILL.md');
124
+ const exists = fs.existsSync(skillPath);
125
+ results.push({
126
+ name: `skill-file:${skill.name}`,
127
+ status: exists ? 'pass' : 'fail',
128
+ message: exists ? `Skill exists: ${skill.name}` : `Missing SKILL.md: ${skill.directory}SKILL.md`,
129
+ });
130
+ }
131
+
132
+ // --- Check 6: Skill count matches ---
133
+ const skillCountManifest = manifest.capabilities?.skills?.count || 0;
134
+ const skillCountFS = fs.existsSync(path.join(agentDir, 'skills'))
135
+ ? fs.readdirSync(path.join(agentDir, 'skills'), { withFileTypes: true }).filter((d) => d.isDirectory()).length
136
+ : 0;
137
+ results.push({
138
+ name: 'skill-count',
139
+ status: skillCountManifest === skillCountFS ? 'pass' : 'fail',
140
+ message:
141
+ skillCountManifest === skillCountFS
142
+ ? `Skill count matches: ${skillCountFS}`
143
+ : `Skill count mismatch: manifest=${skillCountManifest}, filesystem=${skillCountFS}`,
144
+ });
145
+
146
+ // --- Check 7: Workflow files exist ---
147
+ const workflows = manifest.capabilities?.workflows?.items || [];
148
+ for (const workflow of workflows) {
149
+ const wfPath = path.join(agentDir, workflow.file);
150
+ const exists = fs.existsSync(wfPath);
151
+ results.push({
152
+ name: `workflow-file:${workflow.name}`,
153
+ status: exists ? 'pass' : 'fail',
154
+ message: exists ? `Workflow exists: ${workflow.name}` : `Missing workflow: ${workflow.file}`,
155
+ });
156
+ }
157
+
158
+ // --- Check 8: Workflow count matches ---
159
+ const wfCountManifest = manifest.capabilities?.workflows?.count || 0;
160
+ const wfCountFS = fs.existsSync(path.join(agentDir, 'workflows'))
161
+ ? fs.readdirSync(path.join(agentDir, 'workflows')).filter((f) => f.endsWith('.md') && f !== 'README.md').length
162
+ : 0;
163
+ results.push({
164
+ name: 'workflow-count',
165
+ status: wfCountManifest === wfCountFS ? 'pass' : 'fail',
166
+ message:
167
+ wfCountManifest === wfCountFS
168
+ ? `Workflow count matches: ${wfCountFS}`
169
+ : `Workflow count mismatch: manifest=${wfCountManifest}, filesystem=${wfCountFS}`,
170
+ });
171
+
172
+ // --- Check 9: Command count matches ---
173
+ const cmdCountManifest = manifest.capabilities?.commands?.count || 0;
174
+ const cmdCountFS = fs.existsSync(path.join(agentDir, 'commands'))
175
+ ? fs.readdirSync(path.join(agentDir, 'commands')).filter((f) => f.endsWith('.md') && f !== 'README.md').length
176
+ : 0;
177
+ results.push({
178
+ name: 'command-count',
179
+ status: cmdCountManifest === cmdCountFS ? 'pass' : 'fail',
180
+ message:
181
+ cmdCountManifest === cmdCountFS
182
+ ? `Command count matches: ${cmdCountFS}`
183
+ : `Command count mismatch: manifest=${cmdCountManifest}, filesystem=${cmdCountFS}`,
184
+ });
185
+
186
+ // --- Check 10: Engine JSON files valid ---
187
+ const engineFiles = ['workflow-state.json', 'loading-rules.json', 'sdlc-map.json', 'reliability-config.json'];
188
+ for (const engineFile of engineFiles) {
189
+ results.push(checkJsonFile(path.join(agentDir, 'engine', engineFile), `engine:${engineFile}`));
190
+ }
191
+
192
+ // --- Check 11: Hooks file valid ---
193
+ results.push(checkJsonFile(path.join(agentDir, 'hooks', 'hooks.json'), 'hooks-json'));
194
+
195
+ // --- Check 12: Cross-reference — loading-rules agents exist in manifest ---
196
+ const loadingRulesPath = path.join(agentDir, 'engine', 'loading-rules.json');
197
+ if (fs.existsSync(loadingRulesPath)) {
198
+ try {
199
+ const loadingRules = JSON.parse(fs.readFileSync(loadingRulesPath, 'utf-8'));
200
+ const manifestAgentNames = new Set(agents.map((agent) => agent.name));
201
+ const domainRules = loadingRules.domainRules || [];
202
+
203
+ for (const rule of domainRules) {
204
+ for (const agentName of (rule.loadAgents || [])) {
205
+ const exists = manifestAgentNames.has(agentName);
206
+ results.push({
207
+ name: `xref:loading-rules:${agentName}`,
208
+ status: exists ? 'pass' : 'warn',
209
+ message: exists
210
+ ? `Loading-rules agent "${agentName}" exists in manifest`
211
+ : `Loading-rules references agent "${agentName}" not in manifest`,
212
+ });
213
+ }
214
+ }
215
+ } catch {
216
+ results.push({ name: 'xref:loading-rules', status: 'warn', message: 'Could not parse loading-rules.json for cross-reference check' });
217
+ }
218
+ }
219
+
220
+ return buildReport(results);
221
+ }
222
+
223
+ /**
224
+ * Builds a summary report from individual check results.
225
+ *
226
+ * @param {CheckResult[]} results - Array of individual check results
227
+ * @returns {VerificationReport}
228
+ */
229
+ function buildReport(results) {
230
+ const passed = results.filter((result) => result.status === 'pass').length;
231
+ const failed = results.filter((result) => result.status === 'fail').length;
232
+ const warnings = results.filter((result) => result.status === 'warn').length;
233
+
234
+ return { passed, failed, warnings, results };
235
+ }
236
+
237
+ module.exports = {
238
+ runAllChecks,
239
+ checkJsonFile,
240
+ };
@@ -0,0 +1,353 @@
1
+ /**
2
+ * Antigravity AI Kit — Workflow Engine
3
+ *
4
+ * Runtime module that enforces workflow-state.json transitions.
5
+ * This is the first true runtime enforcement layer — transitions
6
+ * are validated against the defined state machine before being applied.
7
+ *
8
+ * @module lib/workflow-engine
9
+ * @author Emre Dursun
10
+ * @since v3.0.0
11
+ */
12
+
13
+ 'use strict';
14
+
15
+ const fs = require('fs');
16
+ const path = require('path');
17
+
18
+ /** @typedef {'IDLE' | 'EXPLORE' | 'PLAN' | 'IMPLEMENT' | 'VERIFY' | 'REVIEW' | 'DEPLOY' | 'MAINTAIN'} WorkflowPhase */
19
+
20
+ /**
21
+ * @typedef {object} TransitionResult
22
+ * @property {boolean} success - Whether the transition was applied
23
+ * @property {string} fromPhase - Phase before transition
24
+ * @property {string} toPhase - Target phase
25
+ * @property {string} trigger - What triggered the transition
26
+ * @property {string} guard - Guard condition for this transition
27
+ * @property {string} [timestamp] - ISO timestamp of when transition occurred
28
+ * @property {string} [error] - Error message if transition failed
29
+ */
30
+
31
+ /**
32
+ * @typedef {object} HistoryEntry
33
+ * @property {string} from - Source phase
34
+ * @property {string} to - Target phase
35
+ * @property {string} trigger - Transition trigger
36
+ * @property {string} timestamp - ISO timestamp
37
+ */
38
+
39
+ const WORKFLOW_STATE_FILENAME = 'workflow-state.json';
40
+ const ENGINE_DIR = 'engine';
41
+ const AGENT_DIR = '.agent';
42
+
43
+ /**
44
+ * Resolves the absolute path to workflow-state.json for a given project root.
45
+ *
46
+ * @param {string} projectRoot - Root directory of the project
47
+ * @returns {string} Absolute path to workflow-state.json
48
+ */
49
+ function resolveStatePath(projectRoot) {
50
+ return path.join(projectRoot, AGENT_DIR, ENGINE_DIR, WORKFLOW_STATE_FILENAME);
51
+ }
52
+
53
+ /**
54
+ * Loads and parses the workflow state from disk.
55
+ *
56
+ * @param {string} projectRoot - Root directory of the project
57
+ * @returns {{ state: object, filePath: string }}
58
+ * @throws {Error} If file does not exist or contains invalid JSON
59
+ */
60
+ function loadWorkflowState(projectRoot) {
61
+ const filePath = resolveStatePath(projectRoot);
62
+
63
+ if (!fs.existsSync(filePath)) {
64
+ throw new Error(`Workflow state file not found: ${filePath}`);
65
+ }
66
+
67
+ const raw = fs.readFileSync(filePath, 'utf-8');
68
+
69
+ try {
70
+ const state = JSON.parse(raw);
71
+ return { state, filePath };
72
+ } catch (parseError) {
73
+ throw new Error(`Invalid JSON in workflow state file: ${parseError.message}`);
74
+ }
75
+ }
76
+
77
+ /**
78
+ * Returns the current workflow phase for a project.
79
+ *
80
+ * @param {string} projectRoot - Root directory of the project
81
+ * @returns {string} Current phase name (e.g., 'IDLE', 'PLAN')
82
+ */
83
+ function getCurrentPhase(projectRoot) {
84
+ const { state } = loadWorkflowState(projectRoot);
85
+ return state.currentPhase;
86
+ }
87
+
88
+ /**
89
+ * Returns the full transition history.
90
+ *
91
+ * @param {string} projectRoot - Root directory of the project
92
+ * @returns {HistoryEntry[]} Array of historical transitions
93
+ */
94
+ function getTransitionHistory(projectRoot) {
95
+ const { state } = loadWorkflowState(projectRoot);
96
+ return state.history || [];
97
+ }
98
+
99
+ /**
100
+ * Finds a matching transition definition from the state machine.
101
+ *
102
+ * @param {object[]} transitions - Array of transition definitions
103
+ * @param {string} fromPhase - Source phase
104
+ * @param {string} toPhase - Target phase
105
+ * @returns {object | null} Matching transition or null
106
+ */
107
+ function findTransition(transitions, fromPhase, toPhase) {
108
+ return transitions.find(
109
+ (transition) => transition.from === fromPhase && transition.to === toPhase
110
+ ) || null;
111
+ }
112
+
113
+ /**
114
+ * Validates whether a transition from the current phase to the target phase
115
+ * is permitted by the state machine definition.
116
+ *
117
+ * Does NOT execute the transition — use executeTransition() for that.
118
+ *
119
+ * @param {string} projectRoot - Root directory of the project
120
+ * @param {string} toPhase - Target phase to transition to
121
+ * @returns {TransitionResult} Validation result (success does not mean executed)
122
+ */
123
+ function validateTransition(projectRoot, toPhase) {
124
+ const { state } = loadWorkflowState(projectRoot);
125
+ const currentPhase = state.currentPhase;
126
+ const transitions = state.transitions || [];
127
+
128
+ if (currentPhase === toPhase) {
129
+ return {
130
+ success: false,
131
+ fromPhase: currentPhase,
132
+ toPhase,
133
+ trigger: '',
134
+ guard: '',
135
+ error: `Already in phase ${toPhase} — no transition needed`,
136
+ };
137
+ }
138
+
139
+ const match = findTransition(transitions, currentPhase, toPhase);
140
+
141
+ if (!match) {
142
+ const validTargets = transitions
143
+ .filter((transition) => transition.from === currentPhase)
144
+ .map((transition) => transition.to);
145
+
146
+ return {
147
+ success: false,
148
+ fromPhase: currentPhase,
149
+ toPhase,
150
+ trigger: '',
151
+ guard: '',
152
+ error: `Invalid transition: ${currentPhase} → ${toPhase}. Valid targets from ${currentPhase}: [${validTargets.join(', ')}]`,
153
+ };
154
+ }
155
+
156
+ return {
157
+ success: true,
158
+ fromPhase: currentPhase,
159
+ toPhase,
160
+ trigger: match.trigger,
161
+ guard: match.guard,
162
+ };
163
+ }
164
+
165
+ /**
166
+ * Executes a workflow transition atomically.
167
+ *
168
+ * 1. Validates the transition is permitted
169
+ * 2. Updates the current phase
170
+ * 3. Records timestamps on phase records
171
+ * 4. Appends to history
172
+ * 5. Writes updated state to disk
173
+ *
174
+ * @param {string} projectRoot - Root directory of the project
175
+ * @param {string} toPhase - Target phase to transition to
176
+ * @param {string} [triggerOverride] - Optional override for the trigger description
177
+ * @returns {TransitionResult} Result of the transition attempt
178
+ */
179
+ function executeTransition(projectRoot, toPhase, triggerOverride) {
180
+ const { state, filePath } = loadWorkflowState(projectRoot);
181
+ const currentPhase = state.currentPhase;
182
+ const transitions = state.transitions || [];
183
+
184
+ if (currentPhase === toPhase) {
185
+ return {
186
+ success: false,
187
+ fromPhase: currentPhase,
188
+ toPhase,
189
+ trigger: '',
190
+ guard: '',
191
+ error: `Already in phase ${toPhase} — no transition needed`,
192
+ };
193
+ }
194
+
195
+ const match = findTransition(transitions, currentPhase, toPhase);
196
+
197
+ if (!match) {
198
+ const validTargets = transitions
199
+ .filter((transition) => transition.from === currentPhase)
200
+ .map((transition) => transition.to);
201
+
202
+ return {
203
+ success: false,
204
+ fromPhase: currentPhase,
205
+ toPhase,
206
+ trigger: '',
207
+ guard: '',
208
+ error: `Invalid transition: ${currentPhase} → ${toPhase}. Valid targets from ${currentPhase}: [${validTargets.join(', ')}]`,
209
+ };
210
+ }
211
+
212
+ const timestamp = new Date().toISOString();
213
+ const trigger = triggerOverride || match.trigger;
214
+
215
+ // Update previous phase completion timestamp
216
+ if (currentPhase !== 'IDLE' && state.phases[currentPhase]) {
217
+ state.phases[currentPhase].completedAt = timestamp;
218
+ state.phases[currentPhase].status = 'completed';
219
+ }
220
+
221
+ // 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 = [];
238
+ }
239
+
240
+ state.history.push({
241
+ from: currentPhase,
242
+ to: toPhase,
243
+ trigger,
244
+ timestamp,
245
+ });
246
+
247
+ // Atomic write: write to temp file, then rename
248
+ const tempPath = `${filePath}.tmp`;
249
+
250
+ try {
251
+ fs.writeFileSync(tempPath, JSON.stringify(state, null, 2) + '\n', 'utf-8');
252
+ fs.renameSync(tempPath, filePath);
253
+ } 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
+ }
262
+ return {
263
+ success: false,
264
+ fromPhase: currentPhase,
265
+ toPhase,
266
+ trigger,
267
+ guard: match.guard,
268
+ error: `Failed to write state: ${writeError.message}`,
269
+ };
270
+ }
271
+
272
+ return {
273
+ success: true,
274
+ fromPhase: currentPhase,
275
+ toPhase,
276
+ trigger,
277
+ guard: match.guard,
278
+ timestamp,
279
+ };
280
+ }
281
+
282
+ /**
283
+ * Resets the workflow to IDLE state with clean phase records.
284
+ *
285
+ * @param {string} projectRoot - Root directory of the project
286
+ * @param {boolean} [preserveHistory=true] - Whether to keep transition history
287
+ * @returns {{ success: boolean, previousPhase: string }}
288
+ */
289
+ 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
+ }
303
+
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
+ });
314
+ }
315
+
316
+ const tempPath = `${filePath}.tmp`;
317
+ fs.writeFileSync(tempPath, JSON.stringify(state, null, 2) + '\n', 'utf-8');
318
+ fs.renameSync(tempPath, filePath);
319
+
320
+ return { success: true, previousPhase };
321
+ }
322
+
323
+ /**
324
+ * Returns all valid transitions from the current phase.
325
+ *
326
+ * @param {string} projectRoot - Root directory of the project
327
+ * @returns {{ currentPhase: string, validTransitions: object[] }}
328
+ */
329
+ function getAvailableTransitions(projectRoot) {
330
+ const { state } = loadWorkflowState(projectRoot);
331
+ const currentPhase = state.currentPhase;
332
+ const transitions = state.transitions || [];
333
+
334
+ const validTransitions = transitions
335
+ .filter((transition) => transition.from === currentPhase)
336
+ .map((transition) => ({
337
+ to: transition.to,
338
+ trigger: transition.trigger,
339
+ guard: transition.guard,
340
+ }));
341
+
342
+ return { currentPhase, validTransitions };
343
+ }
344
+
345
+ module.exports = {
346
+ loadWorkflowState,
347
+ getCurrentPhase,
348
+ getTransitionHistory,
349
+ validateTransition,
350
+ executeTransition,
351
+ resetWorkflow,
352
+ getAvailableTransitions,
353
+ };