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/cli-commands.js
CHANGED
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
|
|
14
14
|
const fs = require('fs');
|
|
15
15
|
const path = require('path');
|
|
16
|
+
const { AGENT_DIR, ENGINE_DIR } = require('./constants');
|
|
16
17
|
|
|
17
18
|
// ANSI colors (shared with ag-kit.js)
|
|
18
19
|
const colors = {
|
|
@@ -133,7 +134,7 @@ function healCommand(projectRoot, options = {}) {
|
|
|
133
134
|
ciOutput = fs.readFileSync(filePath, 'utf-8');
|
|
134
135
|
} else {
|
|
135
136
|
// Try last-saved CI output
|
|
136
|
-
const lastCiPath = path.join(projectRoot,
|
|
137
|
+
const lastCiPath = path.join(projectRoot, AGENT_DIR, ENGINE_DIR, 'last-ci-output.txt');
|
|
137
138
|
if (fs.existsSync(lastCiPath)) {
|
|
138
139
|
ciOutput = fs.readFileSync(lastCiPath, 'utf-8');
|
|
139
140
|
} else {
|
|
@@ -228,8 +229,94 @@ function renderDashboardSections(projectRoot) {
|
|
|
228
229
|
}
|
|
229
230
|
}
|
|
230
231
|
|
|
232
|
+
/**
|
|
233
|
+
* Health check CLI handler.
|
|
234
|
+
* Aggregates error budget, plugin integrity, config validation, and healing status.
|
|
235
|
+
*
|
|
236
|
+
* @param {string} projectRoot - Root directory
|
|
237
|
+
* @returns {{ healthy: boolean, checks: object[] }}
|
|
238
|
+
*/
|
|
239
|
+
function healthCommand(projectRoot) {
|
|
240
|
+
const checks = [];
|
|
241
|
+
|
|
242
|
+
// Error budget health
|
|
243
|
+
try {
|
|
244
|
+
const errorBudget = require('./error-budget');
|
|
245
|
+
const report = errorBudget.getBudgetReport(projectRoot);
|
|
246
|
+
checks.push({
|
|
247
|
+
name: 'Error Budget',
|
|
248
|
+
status: report.status,
|
|
249
|
+
healthy: report.status !== 'EXHAUSTED',
|
|
250
|
+
detail: report.violations.length > 0 ? `Violations: ${report.violations.join(', ')}` : 'All rates within thresholds',
|
|
251
|
+
});
|
|
252
|
+
} catch {
|
|
253
|
+
checks.push({ name: 'Error Budget', status: 'SKIPPED', healthy: true, detail: 'No reliability config found' });
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
// Plugin integrity
|
|
257
|
+
try {
|
|
258
|
+
const pluginVerifier = require('./plugin-verifier');
|
|
259
|
+
const result = pluginVerifier.verifyAllPlugins(projectRoot);
|
|
260
|
+
const pluginHealthy = result.invalid.length === 0;
|
|
261
|
+
checks.push({
|
|
262
|
+
name: 'Plugin Integrity',
|
|
263
|
+
status: pluginHealthy ? 'PASS' : 'FAIL',
|
|
264
|
+
healthy: pluginHealthy,
|
|
265
|
+
detail: `${result.valid} valid, ${result.invalid.length} invalid, ${result.unverified.length} unverified`,
|
|
266
|
+
});
|
|
267
|
+
} catch {
|
|
268
|
+
checks.push({ name: 'Plugin Integrity', status: 'SKIPPED', healthy: true, detail: 'Verifier not available' });
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
// Config validation
|
|
272
|
+
try {
|
|
273
|
+
const configValidator = require('./config-validator');
|
|
274
|
+
const result = configValidator.validateAllConfigs(projectRoot);
|
|
275
|
+
const invalidCount = result.totalConfigs - result.validConfigs;
|
|
276
|
+
const configHealthy = invalidCount === 0;
|
|
277
|
+
checks.push({
|
|
278
|
+
name: 'Config Validation',
|
|
279
|
+
status: configHealthy ? 'PASS' : 'FAIL',
|
|
280
|
+
healthy: configHealthy,
|
|
281
|
+
detail: `${result.validConfigs} valid, ${invalidCount} invalid of ${result.totalConfigs} configs`,
|
|
282
|
+
});
|
|
283
|
+
} catch {
|
|
284
|
+
checks.push({ name: 'Config Validation', status: 'SKIPPED', healthy: true, detail: 'Validator not available' });
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
// Self-healing status
|
|
288
|
+
try {
|
|
289
|
+
const selfHealing = require('./self-healing');
|
|
290
|
+
const report = selfHealing.getHealingReport(projectRoot);
|
|
291
|
+
checks.push({
|
|
292
|
+
name: 'Self-Healing',
|
|
293
|
+
status: report.pendingPatches > 0 ? 'WARNING' : 'PASS',
|
|
294
|
+
healthy: true,
|
|
295
|
+
detail: `${report.totalHeals} heals, ${report.successRate}% success, ${report.pendingPatches} pending`,
|
|
296
|
+
});
|
|
297
|
+
} catch {
|
|
298
|
+
checks.push({ name: 'Self-Healing', status: 'SKIPPED', healthy: true, detail: 'Healer not available' });
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
const healthy = checks.every((c) => c.healthy);
|
|
302
|
+
|
|
303
|
+
// Render output
|
|
304
|
+
console.log(`\n${colors.bright}${colors.blue}═══ Health Check ═══${colors.reset}\n`);
|
|
305
|
+
for (const check of checks) {
|
|
306
|
+
const icon = check.status === 'PASS' || check.status === 'HEALTHY' ? '✓' : check.status === 'FAIL' || check.status === 'EXHAUSTED' ? '✗' : '⚠';
|
|
307
|
+
const color = check.healthy ? 'green' : 'red';
|
|
308
|
+
console.log(` ${colors[color]}${icon} ${check.name}: ${check.status}${colors.reset}`);
|
|
309
|
+
console.log(` ${check.detail}`);
|
|
310
|
+
}
|
|
311
|
+
console.log('');
|
|
312
|
+
console.log(` ${healthy ? `${colors.green}✅ All health checks passed` : `${colors.red}❌ Some health checks failed`}${colors.reset}\n`);
|
|
313
|
+
|
|
314
|
+
return { healthy, checks };
|
|
315
|
+
}
|
|
316
|
+
|
|
231
317
|
module.exports = {
|
|
232
318
|
marketCommand,
|
|
233
319
|
healCommand,
|
|
320
|
+
healthCommand,
|
|
234
321
|
renderDashboardSections,
|
|
235
322
|
};
|
|
@@ -0,0 +1,274 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Antigravity AI Kit — Configuration Validator
|
|
3
|
+
*
|
|
4
|
+
* Runtime JSON schema validation for engine configuration files.
|
|
5
|
+
* Catches configuration corruption and drift before they cause
|
|
6
|
+
* runtime failures.
|
|
7
|
+
*
|
|
8
|
+
* @module lib/config-validator
|
|
9
|
+
* @author Emre Dursun
|
|
10
|
+
* @since v3.2.0
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
'use strict';
|
|
14
|
+
|
|
15
|
+
const fs = require('fs');
|
|
16
|
+
const path = require('path');
|
|
17
|
+
const { AGENT_DIR, ENGINE_DIR, HOOKS_DIR } = require('./constants');
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* @typedef {object} ValidationResult
|
|
21
|
+
* @property {boolean} valid - Whether the config is valid
|
|
22
|
+
* @property {string[]} errors - Validation error messages
|
|
23
|
+
* @property {string[]} warnings - Non-critical warnings
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Schema definitions for engine configuration files.
|
|
28
|
+
* Each schema defines required fields, their types, and constraints.
|
|
29
|
+
*/
|
|
30
|
+
const SCHEMAS = {
|
|
31
|
+
'manifest.json': {
|
|
32
|
+
required: ['schemaVersion', 'kitVersion', 'capabilities'],
|
|
33
|
+
types: {
|
|
34
|
+
schemaVersion: 'string',
|
|
35
|
+
kitVersion: 'string',
|
|
36
|
+
capabilities: 'object',
|
|
37
|
+
},
|
|
38
|
+
nested: {
|
|
39
|
+
capabilities: {
|
|
40
|
+
required: ['agents', 'commands', 'skills', 'workflows'],
|
|
41
|
+
types: {
|
|
42
|
+
agents: 'object',
|
|
43
|
+
commands: 'object',
|
|
44
|
+
skills: 'object',
|
|
45
|
+
workflows: 'object',
|
|
46
|
+
},
|
|
47
|
+
},
|
|
48
|
+
},
|
|
49
|
+
},
|
|
50
|
+
|
|
51
|
+
'workflow-state.json': {
|
|
52
|
+
required: ['currentPhase', 'phases', 'transitions'],
|
|
53
|
+
types: {
|
|
54
|
+
currentPhase: 'string',
|
|
55
|
+
phases: 'object',
|
|
56
|
+
transitions: 'array',
|
|
57
|
+
},
|
|
58
|
+
validators: {
|
|
59
|
+
currentPhase: (value) => {
|
|
60
|
+
const validPhases = ['IDLE', 'EXPLORE', 'PLAN', 'IMPLEMENT', 'VERIFY', 'REVIEW', 'DEPLOY', 'MAINTAIN'];
|
|
61
|
+
return validPhases.includes(value) ? null : `Invalid phase: ${value}. Valid: ${validPhases.join(', ')}`;
|
|
62
|
+
},
|
|
63
|
+
},
|
|
64
|
+
},
|
|
65
|
+
|
|
66
|
+
'loading-rules.json': {
|
|
67
|
+
required: ['domainRules', 'contextBudget', 'planningMandates'],
|
|
68
|
+
types: {
|
|
69
|
+
domainRules: 'array',
|
|
70
|
+
contextBudget: 'object',
|
|
71
|
+
planningMandates: 'object',
|
|
72
|
+
},
|
|
73
|
+
nested: {
|
|
74
|
+
contextBudget: {
|
|
75
|
+
required: ['maxAgentsPerSession', 'maxSkillsPerSession'],
|
|
76
|
+
types: {
|
|
77
|
+
maxAgentsPerSession: 'number',
|
|
78
|
+
maxSkillsPerSession: 'number',
|
|
79
|
+
},
|
|
80
|
+
},
|
|
81
|
+
planningMandates: {
|
|
82
|
+
required: ['alwaysLoadRules', 'alwaysLoadSkills', 'crossCuttingSections'],
|
|
83
|
+
types: {
|
|
84
|
+
alwaysLoadRules: 'array',
|
|
85
|
+
alwaysLoadSkills: 'array',
|
|
86
|
+
crossCuttingSections: 'array',
|
|
87
|
+
specialistContributors: 'object',
|
|
88
|
+
},
|
|
89
|
+
},
|
|
90
|
+
},
|
|
91
|
+
arrayItemSchema: {
|
|
92
|
+
domainRules: {
|
|
93
|
+
required: ['domain', 'keywords'],
|
|
94
|
+
types: {
|
|
95
|
+
domain: 'string',
|
|
96
|
+
keywords: 'array',
|
|
97
|
+
loadAgents: 'array',
|
|
98
|
+
loadSkills: 'array',
|
|
99
|
+
},
|
|
100
|
+
},
|
|
101
|
+
},
|
|
102
|
+
},
|
|
103
|
+
|
|
104
|
+
'reliability-config.json': {
|
|
105
|
+
required: ['errorBudget'],
|
|
106
|
+
types: {
|
|
107
|
+
errorBudget: 'object',
|
|
108
|
+
},
|
|
109
|
+
nested: {
|
|
110
|
+
errorBudget: {
|
|
111
|
+
required: ['thresholds', 'resetCadence'],
|
|
112
|
+
types: {
|
|
113
|
+
thresholds: 'object',
|
|
114
|
+
resetCadence: 'string',
|
|
115
|
+
},
|
|
116
|
+
},
|
|
117
|
+
},
|
|
118
|
+
},
|
|
119
|
+
|
|
120
|
+
'hooks.json': {
|
|
121
|
+
required: ['hooks'],
|
|
122
|
+
types: {
|
|
123
|
+
hooks: 'array',
|
|
124
|
+
},
|
|
125
|
+
arrayItemSchema: {
|
|
126
|
+
hooks: {
|
|
127
|
+
required: ['event'],
|
|
128
|
+
types: {
|
|
129
|
+
event: 'string',
|
|
130
|
+
},
|
|
131
|
+
},
|
|
132
|
+
},
|
|
133
|
+
},
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Validates a value's type.
|
|
138
|
+
*
|
|
139
|
+
* @param {*} value - Value to check
|
|
140
|
+
* @param {string} expectedType - Expected type string
|
|
141
|
+
* @returns {boolean}
|
|
142
|
+
*/
|
|
143
|
+
function checkType(value, expectedType) {
|
|
144
|
+
if (expectedType === 'array') {
|
|
145
|
+
return Array.isArray(value);
|
|
146
|
+
}
|
|
147
|
+
return typeof value === expectedType;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Validates a configuration object against its schema.
|
|
152
|
+
*
|
|
153
|
+
* @param {object} config - Parsed configuration object
|
|
154
|
+
* @param {object} schema - Schema definition
|
|
155
|
+
* @param {string} [prefix=''] - Field path prefix for nested errors
|
|
156
|
+
* @returns {ValidationResult}
|
|
157
|
+
*/
|
|
158
|
+
function validateAgainstSchema(config, schema, prefix = '') {
|
|
159
|
+
const errors = [];
|
|
160
|
+
const warnings = [];
|
|
161
|
+
|
|
162
|
+
// Check required fields
|
|
163
|
+
for (const field of (schema.required || [])) {
|
|
164
|
+
const fieldPath = prefix ? `${prefix}.${field}` : field;
|
|
165
|
+
if (config[field] === undefined || config[field] === null) {
|
|
166
|
+
errors.push(`Missing required field: ${fieldPath}`);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// Check types
|
|
171
|
+
for (const [field, expectedType] of Object.entries(schema.types || {})) {
|
|
172
|
+
const fieldPath = prefix ? `${prefix}.${field}` : field;
|
|
173
|
+
if (config[field] !== undefined && config[field] !== null) {
|
|
174
|
+
if (!checkType(config[field], expectedType)) {
|
|
175
|
+
errors.push(`Invalid type for ${fieldPath}: expected ${expectedType}, got ${Array.isArray(config[field]) ? 'array' : typeof config[field]}`);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// Run custom validators
|
|
181
|
+
for (const [field, validator] of Object.entries(schema.validators || {})) {
|
|
182
|
+
if (config[field] !== undefined) {
|
|
183
|
+
const error = validator(config[field]);
|
|
184
|
+
if (error) {
|
|
185
|
+
errors.push(error);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// Validate nested schemas
|
|
191
|
+
for (const [field, nestedSchema] of Object.entries(schema.nested || {})) {
|
|
192
|
+
if (config[field] && typeof config[field] === 'object' && !Array.isArray(config[field])) {
|
|
193
|
+
const nestedResult = validateAgainstSchema(config[field], nestedSchema, prefix ? `${prefix}.${field}` : field);
|
|
194
|
+
errors.push(...nestedResult.errors);
|
|
195
|
+
warnings.push(...nestedResult.warnings);
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// Validate array items
|
|
200
|
+
for (const [field, itemSchema] of Object.entries(schema.arrayItemSchema || {})) {
|
|
201
|
+
if (Array.isArray(config[field])) {
|
|
202
|
+
config[field].forEach((item, index) => {
|
|
203
|
+
if (typeof item === 'object' && item !== null) {
|
|
204
|
+
const itemResult = validateAgainstSchema(item, itemSchema, `${field}[${index}]`);
|
|
205
|
+
errors.push(...itemResult.errors);
|
|
206
|
+
warnings.push(...itemResult.warnings);
|
|
207
|
+
}
|
|
208
|
+
});
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
return { valid: errors.length === 0, errors, warnings };
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* Validates a specific engine configuration file.
|
|
217
|
+
*
|
|
218
|
+
* @param {string} projectRoot - Root directory of the project
|
|
219
|
+
* @param {string} configName - Configuration file name (e.g., 'manifest.json')
|
|
220
|
+
* @returns {ValidationResult}
|
|
221
|
+
*/
|
|
222
|
+
function validateConfig(projectRoot, configName) {
|
|
223
|
+
const schema = SCHEMAS[configName];
|
|
224
|
+
if (!schema) {
|
|
225
|
+
return { valid: false, errors: [`No schema defined for: ${configName}`], warnings: [] };
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
const configDir = configName === 'manifest.json'
|
|
229
|
+
? path.join(projectRoot, AGENT_DIR)
|
|
230
|
+
: configName === 'hooks.json'
|
|
231
|
+
? path.join(projectRoot, AGENT_DIR, HOOKS_DIR)
|
|
232
|
+
: path.join(projectRoot, AGENT_DIR, ENGINE_DIR);
|
|
233
|
+
|
|
234
|
+
const configPath = path.join(configDir, configName);
|
|
235
|
+
|
|
236
|
+
if (!fs.existsSync(configPath)) {
|
|
237
|
+
return { valid: false, errors: [`Config file not found: ${configPath}`], warnings: [] };
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
let config;
|
|
241
|
+
try {
|
|
242
|
+
config = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
|
|
243
|
+
} catch (parseError) {
|
|
244
|
+
return { valid: false, errors: [`Invalid JSON: ${parseError.message}`], warnings: [] };
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
return validateAgainstSchema(config, schema);
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/**
|
|
251
|
+
* Validates all known engine configuration files.
|
|
252
|
+
*
|
|
253
|
+
* @param {string} projectRoot - Root directory of the project
|
|
254
|
+
* @returns {{ totalConfigs: number, validConfigs: number, results: Object.<string, ValidationResult> }}
|
|
255
|
+
*/
|
|
256
|
+
function validateAllConfigs(projectRoot) {
|
|
257
|
+
const entries = Object.keys(SCHEMAS).map(
|
|
258
|
+
(configName) => [configName, validateConfig(projectRoot, configName)]
|
|
259
|
+
);
|
|
260
|
+
const results = Object.fromEntries(entries);
|
|
261
|
+
const validCount = entries.filter(([, result]) => result.valid).length;
|
|
262
|
+
|
|
263
|
+
return {
|
|
264
|
+
totalConfigs: Object.keys(SCHEMAS).length,
|
|
265
|
+
validConfigs: validCount,
|
|
266
|
+
results,
|
|
267
|
+
};
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
module.exports = {
|
|
271
|
+
validateConfig,
|
|
272
|
+
validateAllConfigs,
|
|
273
|
+
SCHEMAS,
|
|
274
|
+
};
|
package/lib/conflict-detector.js
CHANGED
|
@@ -14,8 +14,8 @@
|
|
|
14
14
|
const fs = require('fs');
|
|
15
15
|
const path = require('path');
|
|
16
16
|
|
|
17
|
-
const AGENT_DIR = '
|
|
18
|
-
const
|
|
17
|
+
const { AGENT_DIR, ENGINE_DIR } = require('./constants');
|
|
18
|
+
const { writeJsonAtomic } = require('./io');
|
|
19
19
|
const FILE_LOCKS_FILE = 'file-locks.json';
|
|
20
20
|
|
|
21
21
|
/** Default lock TTL in milliseconds (30 minutes) */
|
|
@@ -65,10 +65,17 @@ function loadLocks(projectRoot) {
|
|
|
65
65
|
const now = Date.now();
|
|
66
66
|
|
|
67
67
|
// Filter out expired locks
|
|
68
|
-
|
|
68
|
+
const activeLocks = locks.filter((lock) => {
|
|
69
69
|
const claimedTime = new Date(lock.claimedAt).getTime();
|
|
70
70
|
return (now - claimedTime) < (lock.ttlMs || DEFAULT_LOCK_TTL_MS);
|
|
71
71
|
});
|
|
72
|
+
|
|
73
|
+
// Persist pruned list if stale locks were removed
|
|
74
|
+
if (activeLocks.length < locks.length) {
|
|
75
|
+
writeLocks(projectRoot, activeLocks);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
return activeLocks;
|
|
72
79
|
} catch {
|
|
73
80
|
return [];
|
|
74
81
|
}
|
|
@@ -83,21 +90,13 @@ function loadLocks(projectRoot) {
|
|
|
83
90
|
*/
|
|
84
91
|
function writeLocks(projectRoot, locks) {
|
|
85
92
|
const locksPath = resolveLocksPath(projectRoot);
|
|
86
|
-
const tempPath = `${locksPath}.tmp`;
|
|
87
|
-
const dir = path.dirname(locksPath);
|
|
88
|
-
|
|
89
|
-
if (!fs.existsSync(dir)) {
|
|
90
|
-
fs.mkdirSync(dir, { recursive: true });
|
|
91
|
-
}
|
|
92
|
-
|
|
93
93
|
const data = {
|
|
94
94
|
schemaVersion: '1.0.0',
|
|
95
95
|
lastUpdated: new Date().toISOString(),
|
|
96
96
|
locks,
|
|
97
97
|
};
|
|
98
98
|
|
|
99
|
-
|
|
100
|
-
fs.renameSync(tempPath, locksPath);
|
|
99
|
+
writeJsonAtomic(locksPath, data);
|
|
101
100
|
}
|
|
102
101
|
|
|
103
102
|
/**
|
|
@@ -128,22 +127,30 @@ function claimFile(projectRoot, filePath, agent, ttlMs) {
|
|
|
128
127
|
};
|
|
129
128
|
}
|
|
130
129
|
|
|
131
|
-
// Update or create lock
|
|
130
|
+
// Update or create lock immutably
|
|
132
131
|
const existingIndex = locks.findIndex((l) => l.filePath === normalizedPath && l.agent === agent);
|
|
132
|
+
const now = new Date().toISOString();
|
|
133
133
|
|
|
134
|
+
let updatedLocks;
|
|
134
135
|
if (existingIndex !== -1) {
|
|
135
|
-
|
|
136
|
-
|
|
136
|
+
updatedLocks = locks.map((l, i) =>
|
|
137
|
+
i === existingIndex
|
|
138
|
+
? { ...l, claimedAt: now, ttlMs: lockTtl }
|
|
139
|
+
: l
|
|
140
|
+
);
|
|
137
141
|
} else {
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
142
|
+
updatedLocks = [
|
|
143
|
+
...locks,
|
|
144
|
+
{
|
|
145
|
+
filePath: normalizedPath,
|
|
146
|
+
agent,
|
|
147
|
+
claimedAt: now,
|
|
148
|
+
ttlMs: lockTtl,
|
|
149
|
+
},
|
|
150
|
+
];
|
|
144
151
|
}
|
|
145
152
|
|
|
146
|
-
writeLocks(projectRoot,
|
|
153
|
+
writeLocks(projectRoot, updatedLocks);
|
|
147
154
|
return { success: true };
|
|
148
155
|
}
|
|
149
156
|
|
package/lib/constants.js
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Antigravity AI Kit — Shared Constants
|
|
3
|
+
*
|
|
4
|
+
* Central definition of directory names and paths used across
|
|
5
|
+
* all runtime modules. Prevents drift from duplicated strings.
|
|
6
|
+
* Frozen to prevent accidental mutation at runtime.
|
|
7
|
+
*
|
|
8
|
+
* @module lib/constants
|
|
9
|
+
* @since v3.2.0
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
'use strict';
|
|
13
|
+
|
|
14
|
+
/** Root agent configuration directory name */
|
|
15
|
+
const AGENT_DIR = '.agent';
|
|
16
|
+
|
|
17
|
+
/** Engine subdirectory within .agent */
|
|
18
|
+
const ENGINE_DIR = 'engine';
|
|
19
|
+
|
|
20
|
+
/** Hooks subdirectory within .agent */
|
|
21
|
+
const HOOKS_DIR = 'hooks';
|
|
22
|
+
|
|
23
|
+
/** Skills subdirectory within .agent */
|
|
24
|
+
const SKILLS_DIR = 'skills';
|
|
25
|
+
|
|
26
|
+
/** Plugins subdirectory within .agent */
|
|
27
|
+
const PLUGINS_DIR = 'plugins';
|
|
28
|
+
|
|
29
|
+
module.exports = Object.freeze({
|
|
30
|
+
AGENT_DIR,
|
|
31
|
+
ENGINE_DIR,
|
|
32
|
+
HOOKS_DIR,
|
|
33
|
+
SKILLS_DIR,
|
|
34
|
+
PLUGINS_DIR,
|
|
35
|
+
});
|
|
@@ -18,8 +18,8 @@ const taskModel = require('./task-model');
|
|
|
18
18
|
const agentRegistry = require('./agent-registry');
|
|
19
19
|
const agentReputation = require('./agent-reputation');
|
|
20
20
|
|
|
21
|
-
const AGENT_DIR = '
|
|
22
|
-
const
|
|
21
|
+
const { AGENT_DIR, ENGINE_DIR } = require('./constants');
|
|
22
|
+
const { writeJsonAtomic } = require('./io');
|
|
23
23
|
const SPRINT_FILE = 'sprint-plans.json';
|
|
24
24
|
|
|
25
25
|
/** Maximum tasks per sprint suggestion */
|
|
@@ -82,15 +82,7 @@ function loadSprintData(projectRoot) {
|
|
|
82
82
|
*/
|
|
83
83
|
function writeSprintData(projectRoot, data) {
|
|
84
84
|
const filePath = resolveSprintPath(projectRoot);
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
if (!fs.existsSync(dir)) {
|
|
88
|
-
fs.mkdirSync(dir, { recursive: true });
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
const tempPath = `${filePath}.tmp`;
|
|
92
|
-
fs.writeFileSync(tempPath, JSON.stringify(data, null, 2) + '\n', 'utf-8');
|
|
93
|
-
fs.renameSync(tempPath, filePath);
|
|
85
|
+
writeJsonAtomic(filePath, data);
|
|
94
86
|
}
|
|
95
87
|
|
|
96
88
|
/**
|
|
@@ -209,16 +201,11 @@ function generateSprintPlan(projectRoot, options = {}) {
|
|
|
209
201
|
tasks = [];
|
|
210
202
|
}
|
|
211
203
|
|
|
212
|
-
// Priority sort
|
|
213
|
-
const
|
|
214
|
-
tasks.sort((a, b) => {
|
|
215
|
-
const aPriority = priorityOrder[a.priority] ?? 2;
|
|
216
|
-
const bPriority = priorityOrder[b.priority] ?? 2;
|
|
217
|
-
return aPriority - bPriority;
|
|
218
|
-
});
|
|
204
|
+
// Priority sort using shared utility
|
|
205
|
+
const sortedTasks = taskModel.sortByPriority(tasks);
|
|
219
206
|
|
|
220
207
|
// Take top N
|
|
221
|
-
const sprintTasks =
|
|
208
|
+
const sprintTasks = sortedTasks.slice(0, maxTasks);
|
|
222
209
|
|
|
223
210
|
// Auto-assign each
|
|
224
211
|
/** @type {TaskAssignment[]} */
|
|
@@ -296,15 +283,10 @@ function suggestNextTask(projectRoot) {
|
|
|
296
283
|
return { task: null, reason: 'No open tasks remaining' };
|
|
297
284
|
}
|
|
298
285
|
|
|
299
|
-
// Priority sort
|
|
300
|
-
const
|
|
301
|
-
openTasks.sort((a, b) => {
|
|
302
|
-
const aPriority = priorityOrder[a.priority] ?? 2;
|
|
303
|
-
const bPriority = priorityOrder[b.priority] ?? 2;
|
|
304
|
-
return aPriority - bPriority;
|
|
305
|
-
});
|
|
286
|
+
// Priority sort using shared utility
|
|
287
|
+
const sorted = taskModel.sortByPriority(openTasks);
|
|
306
288
|
|
|
307
|
-
const topTask =
|
|
289
|
+
const topTask = sorted[0];
|
|
308
290
|
return {
|
|
309
291
|
task: topTask,
|
|
310
292
|
reason: `Highest priority open task (${topTask.priority})`,
|