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.
- package/.agent/README.md +4 -4
- package/.agent/agents/README.md +16 -12
- package/.agent/agents/architect.md +1 -0
- package/.agent/agents/backend-specialist.md +11 -0
- package/.agent/agents/code-reviewer.md +1 -0
- package/.agent/agents/database-architect.md +11 -0
- package/.agent/agents/devops-engineer.md +11 -0
- package/.agent/agents/e2e-runner.md +1 -0
- package/.agent/agents/explorer-agent.md +11 -0
- package/.agent/agents/frontend-specialist.md +11 -0
- package/.agent/agents/mobile-developer.md +11 -0
- package/.agent/agents/performance-optimizer.md +11 -0
- package/.agent/agents/planner.md +1 -0
- package/.agent/agents/refactor-cleaner.md +1 -0
- package/.agent/agents/reliability-engineer.md +11 -0
- package/.agent/agents/security-reviewer.md +1 -0
- package/.agent/agents/sprint-orchestrator.md +10 -0
- package/.agent/agents/tdd-guide.md +1 -0
- package/.agent/commands/code-review.md +1 -0
- package/.agent/commands/debug.md +1 -0
- package/.agent/commands/deploy.md +1 -0
- package/.agent/commands/help.md +252 -31
- package/.agent/commands/plan.md +1 -0
- package/.agent/commands/status.md +1 -0
- package/.agent/commands/tdd.md +1 -0
- package/.agent/contexts/brainstorm.md +26 -0
- package/.agent/contexts/debug.md +28 -0
- package/.agent/contexts/implement.md +29 -0
- package/.agent/contexts/review.md +27 -0
- package/.agent/contexts/ship.md +28 -0
- package/.agent/engine/identity.json +13 -0
- package/.agent/engine/loading-rules.json +23 -1
- package/.agent/engine/marketplace-index.json +29 -0
- package/.agent/engine/reliability-config.json +14 -0
- package/.agent/engine/sdlc-map.json +44 -0
- package/.agent/engine/workflow-state.json +28 -2
- package/.agent/hooks/hooks.json +27 -25
- package/.agent/manifest.json +12 -4
- package/.agent/rules.md +2 -1
- package/.agent/skills/README.md +10 -5
- package/.agent/skills/i18n-localization/SKILL.md +191 -0
- package/.agent/skills/mcp-integration/SKILL.md +224 -0
- package/.agent/skills/parallel-agents/SKILL.md +1 -1
- package/.agent/skills/shell-conventions/SKILL.md +92 -0
- package/.agent/skills/ui-ux-pro-max/SKILL.md +557 -0
- package/.agent/skills/ui-ux-pro-max/data/charts.csv +26 -0
- package/.agent/skills/ui-ux-pro-max/data/colors.csv +97 -0
- package/.agent/skills/ui-ux-pro-max/data/icons.csv +101 -0
- package/.agent/skills/ui-ux-pro-max/data/landing.csv +31 -0
- package/.agent/skills/ui-ux-pro-max/data/products.csv +97 -0
- package/.agent/skills/ui-ux-pro-max/data/react-performance.csv +45 -0
- package/.agent/skills/ui-ux-pro-max/data/stacks/astro.csv +54 -0
- package/.agent/skills/ui-ux-pro-max/data/stacks/flutter.csv +53 -0
- package/.agent/skills/ui-ux-pro-max/data/stacks/html-tailwind.csv +56 -0
- package/.agent/skills/ui-ux-pro-max/data/stacks/jetpack-compose.csv +53 -0
- package/.agent/skills/ui-ux-pro-max/data/stacks/nextjs.csv +53 -0
- package/.agent/skills/ui-ux-pro-max/data/stacks/nuxt-ui.csv +51 -0
- package/.agent/skills/ui-ux-pro-max/data/stacks/nuxtjs.csv +59 -0
- package/.agent/skills/ui-ux-pro-max/data/stacks/react-native.csv +52 -0
- package/.agent/skills/ui-ux-pro-max/data/stacks/react.csv +54 -0
- package/.agent/skills/ui-ux-pro-max/data/stacks/shadcn.csv +61 -0
- package/.agent/skills/ui-ux-pro-max/data/stacks/svelte.csv +54 -0
- package/.agent/skills/ui-ux-pro-max/data/stacks/swiftui.csv +51 -0
- package/.agent/skills/ui-ux-pro-max/data/stacks/vue.csv +50 -0
- package/.agent/skills/ui-ux-pro-max/data/styles.csv +68 -0
- package/.agent/skills/ui-ux-pro-max/data/typography.csv +58 -0
- package/.agent/skills/ui-ux-pro-max/data/ui-reasoning.csv +101 -0
- package/.agent/skills/ui-ux-pro-max/data/ux-guidelines.csv +100 -0
- package/.agent/skills/ui-ux-pro-max/data/web-interface.csv +31 -0
- package/.agent/skills/ui-ux-pro-max/scripts/core.py +253 -0
- package/.agent/skills/ui-ux-pro-max/scripts/design_system.py +1067 -0
- package/.agent/skills/ui-ux-pro-max/scripts/search.py +114 -0
- package/.agent/templates/adr-template.md +32 -0
- package/.agent/templates/bug-report.md +37 -0
- package/.agent/templates/feature-request.md +32 -0
- package/.agent/workflows/README.md +92 -78
- package/.agent/workflows/brainstorm.md +154 -100
- package/.agent/workflows/create.md +142 -75
- package/.agent/workflows/debug.md +157 -98
- package/.agent/workflows/deploy.md +195 -144
- package/.agent/workflows/enhance.md +157 -65
- package/.agent/workflows/orchestrate.md +171 -114
- package/.agent/workflows/plan.md +147 -72
- package/.agent/workflows/preview.md +140 -83
- package/.agent/workflows/quality-gate.md +196 -0
- package/.agent/workflows/retrospective.md +197 -0
- package/.agent/workflows/review.md +188 -0
- package/.agent/workflows/status.md +142 -91
- package/.agent/workflows/test.md +168 -95
- package/.agent/workflows/ui-ux-pro-max.md +181 -127
- package/README.md +215 -78
- package/bin/ag-kit.js +344 -10
- package/lib/agent-registry.js +214 -0
- package/lib/agent-reputation.js +351 -0
- package/lib/cli-commands.js +235 -0
- package/lib/conflict-detector.js +245 -0
- package/lib/engineering-manager.js +354 -0
- package/lib/error-budget.js +294 -0
- package/lib/hook-system.js +252 -0
- package/lib/identity.js +245 -0
- package/lib/loading-engine.js +208 -0
- package/lib/marketplace.js +298 -0
- package/lib/plugin-system.js +604 -0
- package/lib/security-scanner.js +309 -0
- package/lib/self-healing.js +434 -0
- package/lib/session-manager.js +261 -0
- package/lib/skill-sandbox.js +244 -0
- package/lib/task-governance.js +523 -0
- package/lib/task-model.js +317 -0
- package/lib/updater.js +201 -0
- package/lib/verify.js +240 -0
- package/lib/workflow-engine.js +353 -0
- package/lib/workflow-persistence.js +160 -0
- 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
|
+
};
|