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/loading-engine.js
CHANGED
|
@@ -2,7 +2,9 @@
|
|
|
2
2
|
* Antigravity AI Kit — Loading Rules Engine
|
|
3
3
|
*
|
|
4
4
|
* Runtime implementation of loading-rules.json keyword matching
|
|
5
|
-
* and context budget enforcement.
|
|
5
|
+
* and context budget enforcement. Includes enhanced planning
|
|
6
|
+
* resolution with mandatory cross-cutting concerns, implicit
|
|
7
|
+
* domain triggers, and protected budget items.
|
|
6
8
|
*
|
|
7
9
|
* @module lib/loading-engine
|
|
8
10
|
* @author Emre Dursun
|
|
@@ -14,10 +16,22 @@
|
|
|
14
16
|
const fs = require('fs');
|
|
15
17
|
const path = require('path');
|
|
16
18
|
|
|
17
|
-
const AGENT_DIR = '
|
|
18
|
-
const ENGINE_DIR = 'engine';
|
|
19
|
+
const { AGENT_DIR, ENGINE_DIR } = require('./constants');
|
|
19
20
|
const LOADING_RULES_FILE = 'loading-rules.json';
|
|
20
21
|
|
|
22
|
+
/** Default maximum agents per session */
|
|
23
|
+
const DEFAULT_MAX_AGENTS = 4;
|
|
24
|
+
/** Default maximum skills per session */
|
|
25
|
+
const DEFAULT_MAX_SKILLS = 8;
|
|
26
|
+
/** Default warning threshold percentage */
|
|
27
|
+
const DEFAULT_WARNING_THRESHOLD_PERCENT = 80;
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* @typedef {object} ProtectedItems
|
|
31
|
+
* @property {string[]} [agents] - Agents exempt from budget trimming
|
|
32
|
+
* @property {string[]} [skills] - Skills exempt from budget trimming
|
|
33
|
+
*/
|
|
34
|
+
|
|
21
35
|
/**
|
|
22
36
|
* @typedef {object} LoadPlan
|
|
23
37
|
* @property {string[]} agents - Agents to load
|
|
@@ -29,6 +43,7 @@ const LOADING_RULES_FILE = 'loading-rules.json';
|
|
|
29
43
|
* @property {number} budgetUsage.skillsUsed - Number of skills selected
|
|
30
44
|
* @property {number} budgetUsage.skillsMax - Maximum allowed
|
|
31
45
|
* @property {string[]} matchedDomains - Which domains matched
|
|
46
|
+
* @property {string[]} [mandatoryRules] - Rule files that must be consulted (planning only)
|
|
32
47
|
*/
|
|
33
48
|
|
|
34
49
|
/**
|
|
@@ -47,6 +62,55 @@ function loadRules(projectRoot) {
|
|
|
47
62
|
return JSON.parse(fs.readFileSync(rulesPath, 'utf-8'));
|
|
48
63
|
}
|
|
49
64
|
|
|
65
|
+
/**
|
|
66
|
+
* Trims an array of items respecting a protected set.
|
|
67
|
+
* Protected items survive trimming; non-protected items are dropped from end.
|
|
68
|
+
*
|
|
69
|
+
* @param {string[]} items - Items to potentially trim
|
|
70
|
+
* @param {Set<string>} protectedSet - Items exempt from trimming
|
|
71
|
+
* @param {number} max - Maximum allowed count
|
|
72
|
+
* @returns {{ result: string[], trimmed: boolean, warnings: string[] }}
|
|
73
|
+
*/
|
|
74
|
+
function trimWithProtection(items, protectedSet, max, label) {
|
|
75
|
+
if (items.length <= max) {
|
|
76
|
+
return { result: items, trimmed: false, warnings: [] };
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const protectedArr = items.filter((i) => protectedSet.has(i));
|
|
80
|
+
const nonProtected = items.filter((i) => !protectedSet.has(i));
|
|
81
|
+
const slotsForNonProtected = Math.max(0, max - protectedArr.length);
|
|
82
|
+
const result = [...protectedArr, ...nonProtected.slice(0, slotsForNonProtected)];
|
|
83
|
+
|
|
84
|
+
const warnings = [
|
|
85
|
+
`${label} budget exceeded: ${items.length}/${max} — trimmed to ${result.length}`,
|
|
86
|
+
];
|
|
87
|
+
|
|
88
|
+
if (protectedArr.length > max) {
|
|
89
|
+
warnings.push(`Protected ${label.toLowerCase()} (${protectedArr.length}) exceed budget (${max}) — budget override in effect`);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
return { result, trimmed: true, warnings };
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Builds a pre-compiled word-boundary regex from an array of trigger strings.
|
|
97
|
+
* Combines all triggers into a single alternation pattern for efficient matching.
|
|
98
|
+
*
|
|
99
|
+
* @param {string[]} triggers - Trigger strings to compile
|
|
100
|
+
* @returns {RegExp | null} Compiled regex, or null if no triggers
|
|
101
|
+
*/
|
|
102
|
+
function buildImplicitTriggerRegex(triggers) {
|
|
103
|
+
if (!triggers || triggers.length === 0) {
|
|
104
|
+
return null;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const pattern = triggers
|
|
108
|
+
.map((t) => t.toLowerCase().replace(/[.*+?^${}()|[\]\\]/g, '\\$&'))
|
|
109
|
+
.join('|');
|
|
110
|
+
|
|
111
|
+
return new RegExp(`\\b(?:${pattern})\\b`);
|
|
112
|
+
}
|
|
113
|
+
|
|
50
114
|
/**
|
|
51
115
|
* Matches a task description against domain keywords.
|
|
52
116
|
*
|
|
@@ -55,7 +119,22 @@ function loadRules(projectRoot) {
|
|
|
55
119
|
* @returns {{ matchedDomains: string[], agents: string[], skills: string[] }}
|
|
56
120
|
*/
|
|
57
121
|
function resolveForTask(taskDescription, projectRoot) {
|
|
122
|
+
if (typeof taskDescription !== 'string') {
|
|
123
|
+
return { matchedDomains: [], agents: [], skills: [] };
|
|
124
|
+
}
|
|
125
|
+
|
|
58
126
|
const rules = loadRules(projectRoot);
|
|
127
|
+
return resolveForTaskWithRules(taskDescription, rules);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Internal: Matches a task description against domain keywords using pre-loaded rules.
|
|
132
|
+
*
|
|
133
|
+
* @param {string} taskDescription - Human-readable task text
|
|
134
|
+
* @param {object} rules - Pre-loaded rules object
|
|
135
|
+
* @returns {{ matchedDomains: string[], agents: string[], skills: string[] }}
|
|
136
|
+
*/
|
|
137
|
+
function resolveForTaskWithRules(taskDescription, rules) {
|
|
59
138
|
const domainRules = rules.domainRules || [];
|
|
60
139
|
const lowerTask = taskDescription.toLowerCase();
|
|
61
140
|
|
|
@@ -88,6 +167,83 @@ function resolveForTask(taskDescription, projectRoot) {
|
|
|
88
167
|
};
|
|
89
168
|
}
|
|
90
169
|
|
|
170
|
+
/**
|
|
171
|
+
* Enhanced task resolution for planning workflows.
|
|
172
|
+
* Extends standard keyword matching with implicit trigger detection
|
|
173
|
+
* and mandatory planning resources from planningMandates config.
|
|
174
|
+
*
|
|
175
|
+
* @param {string} taskDescription - Human-readable task text
|
|
176
|
+
* @param {string} projectRoot - Root directory of the project
|
|
177
|
+
* @returns {{ matchedDomains: string[], agents: string[], skills: string[], mandatoryRules: string[] }}
|
|
178
|
+
*/
|
|
179
|
+
function resolveForPlanning(taskDescription, projectRoot) {
|
|
180
|
+
if (typeof taskDescription !== 'string') {
|
|
181
|
+
return { matchedDomains: [], agents: [], skills: [], mandatoryRules: [] };
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
const rules = loadRules(projectRoot);
|
|
185
|
+
return resolveForPlanningWithRules(taskDescription, rules);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Internal: Enhanced planning resolution using pre-loaded rules.
|
|
190
|
+
*
|
|
191
|
+
* @param {string} taskDescription - Human-readable task text
|
|
192
|
+
* @param {object} rules - Pre-loaded rules object
|
|
193
|
+
* @returns {{ matchedDomains: string[], agents: string[], skills: string[], mandatoryRules: string[] }}
|
|
194
|
+
*/
|
|
195
|
+
function resolveForPlanningWithRules(taskDescription, rules) {
|
|
196
|
+
const domainRules = rules.domainRules || [];
|
|
197
|
+
const planningMandates = rules.planningMandates || {};
|
|
198
|
+
const lowerTask = taskDescription.toLowerCase();
|
|
199
|
+
|
|
200
|
+
/** @type {Set<string>} */
|
|
201
|
+
const agents = new Set();
|
|
202
|
+
/** @type {Set<string>} */
|
|
203
|
+
const skills = new Set();
|
|
204
|
+
/** @type {string[]} */
|
|
205
|
+
const matchedDomains = [];
|
|
206
|
+
|
|
207
|
+
// Step 1: Standard keyword matching + implicit trigger detection
|
|
208
|
+
for (const rule of domainRules) {
|
|
209
|
+
const hasKeywordMatch = (rule.keywords || []).some(
|
|
210
|
+
(keyword) => lowerTask.includes(keyword.toLowerCase())
|
|
211
|
+
);
|
|
212
|
+
|
|
213
|
+
// Pre-compile a single regex for all implicit triggers per domain rule
|
|
214
|
+
const triggerRegex = buildImplicitTriggerRegex(rule.implicitTriggers);
|
|
215
|
+
const hasImplicitMatch = triggerRegex !== null && triggerRegex.test(lowerTask);
|
|
216
|
+
|
|
217
|
+
if (hasKeywordMatch || hasImplicitMatch) {
|
|
218
|
+
matchedDomains.push(rule.domain);
|
|
219
|
+
|
|
220
|
+
for (const agent of (rule.loadAgents || [])) {
|
|
221
|
+
agents.add(agent);
|
|
222
|
+
}
|
|
223
|
+
for (const skill of (rule.loadSkills || [])) {
|
|
224
|
+
skills.add(skill);
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// Step 2: Merge mandatory planning skills
|
|
230
|
+
for (const skill of (planningMandates.alwaysLoadSkills || [])) {
|
|
231
|
+
skills.add(skill);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// Step 3: Build mandatory rules list (file paths for planner to reference)
|
|
235
|
+
const mandatoryRules = (planningMandates.alwaysLoadRules || []).map(
|
|
236
|
+
(ruleName) => path.join(AGENT_DIR, 'rules', `${ruleName}.md`)
|
|
237
|
+
);
|
|
238
|
+
|
|
239
|
+
return {
|
|
240
|
+
matchedDomains,
|
|
241
|
+
agents: [...agents],
|
|
242
|
+
skills: [...skills],
|
|
243
|
+
mandatoryRules,
|
|
244
|
+
};
|
|
245
|
+
}
|
|
246
|
+
|
|
91
247
|
/**
|
|
92
248
|
* Resolves agents and skills for a named workflow using workflow bindings.
|
|
93
249
|
*
|
|
@@ -97,6 +253,17 @@ function resolveForTask(taskDescription, projectRoot) {
|
|
|
97
253
|
*/
|
|
98
254
|
function resolveForWorkflow(workflowName, projectRoot) {
|
|
99
255
|
const rules = loadRules(projectRoot);
|
|
256
|
+
return resolveForWorkflowWithRules(workflowName, rules);
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
/**
|
|
260
|
+
* Internal: Resolves workflow bindings using pre-loaded rules.
|
|
261
|
+
*
|
|
262
|
+
* @param {string} workflowName - Name of the workflow
|
|
263
|
+
* @param {object} rules - Pre-loaded rules object
|
|
264
|
+
* @returns {{ agents: string[], skills: string[], bindingType: string }}
|
|
265
|
+
*/
|
|
266
|
+
function resolveForWorkflowWithRules(workflowName, rules) {
|
|
100
267
|
const bindings = rules.workflowBindings || [];
|
|
101
268
|
const match = bindings.find((b) => b.workflow === workflowName);
|
|
102
269
|
|
|
@@ -113,47 +280,62 @@ function resolveForWorkflow(workflowName, projectRoot) {
|
|
|
113
280
|
|
|
114
281
|
/**
|
|
115
282
|
* Enforces context budget limits by trimming agents and skills.
|
|
283
|
+
* Protected items (from planning mandates) are exempt from trimming.
|
|
116
284
|
*
|
|
117
285
|
* @param {string[]} agents - Candidate agents
|
|
118
286
|
* @param {string[]} skills - Candidate skills
|
|
119
287
|
* @param {string} projectRoot - Root directory of the project
|
|
288
|
+
* @param {ProtectedItems} [protectedItems] - Items exempt from budget trimming
|
|
120
289
|
* @returns {{ agents: string[], skills: string[], trimmed: boolean, warnings: string[] }}
|
|
121
290
|
*/
|
|
122
|
-
function enforceContextBudget(agents, skills, projectRoot) {
|
|
291
|
+
function enforceContextBudget(agents, skills, projectRoot, protectedItems) {
|
|
123
292
|
const rules = loadRules(projectRoot);
|
|
293
|
+
return enforceContextBudgetWithRules(agents, skills, rules, protectedItems);
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
/**
|
|
297
|
+
* Internal: Enforces context budget using pre-loaded rules.
|
|
298
|
+
*
|
|
299
|
+
* @param {string[]} agents - Candidate agents
|
|
300
|
+
* @param {string[]} skills - Candidate skills
|
|
301
|
+
* @param {object} rules - Pre-loaded rules object
|
|
302
|
+
* @param {ProtectedItems} [protectedItems] - Items exempt from budget trimming
|
|
303
|
+
* @returns {{ agents: string[], skills: string[], trimmed: boolean, warnings: string[] }}
|
|
304
|
+
*/
|
|
305
|
+
function enforceContextBudgetWithRules(agents, skills, rules, protectedItems) {
|
|
124
306
|
const budget = rules.contextBudget || {};
|
|
125
|
-
const maxAgents = budget.maxAgentsPerSession ||
|
|
126
|
-
const maxSkills = budget.maxSkillsPerSession ||
|
|
127
|
-
const warningThreshold = (budget.warningThresholdPercent ||
|
|
307
|
+
const maxAgents = budget.maxAgentsPerSession || DEFAULT_MAX_AGENTS;
|
|
308
|
+
const maxSkills = budget.maxSkillsPerSession || DEFAULT_MAX_SKILLS;
|
|
309
|
+
const warningThreshold = (budget.warningThresholdPercent || DEFAULT_WARNING_THRESHOLD_PERCENT) / 100;
|
|
128
310
|
|
|
129
|
-
|
|
130
|
-
const
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
if (finalAgents.length > maxAgents) {
|
|
137
|
-
warnings.push(`Agent budget exceeded: ${finalAgents.length}/${maxAgents} — trimmed to ${maxAgents}`);
|
|
138
|
-
finalAgents = finalAgents.slice(0, maxAgents);
|
|
139
|
-
trimmed = true;
|
|
140
|
-
} else if (finalAgents.length >= maxAgents * warningThreshold) {
|
|
141
|
-
warnings.push(`Agent budget near limit: ${finalAgents.length}/${maxAgents} (${Math.round(finalAgents.length / maxAgents * 100)}%)`);
|
|
142
|
-
}
|
|
311
|
+
const protectedAgentSet = new Set((protectedItems && protectedItems.agents) || []);
|
|
312
|
+
const protectedSkillSet = new Set((protectedItems && protectedItems.skills) || []);
|
|
313
|
+
|
|
314
|
+
const agentTrim = trimWithProtection([...agents], protectedAgentSet, maxAgents, 'Agent');
|
|
315
|
+
const skillTrim = trimWithProtection([...skills], protectedSkillSet, maxSkills, 'Skill');
|
|
316
|
+
|
|
317
|
+
const warnings = [...agentTrim.warnings, ...skillTrim.warnings];
|
|
143
318
|
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
warnings.push(`Skill budget near limit: ${
|
|
319
|
+
// Add near-limit warnings for non-trimmed cases
|
|
320
|
+
if (!agentTrim.trimmed && agentTrim.result.length >= maxAgents * warningThreshold) {
|
|
321
|
+
warnings.push(`Agent budget near limit: ${agentTrim.result.length}/${maxAgents} (${Math.round(agentTrim.result.length / maxAgents * 100)}%)`);
|
|
322
|
+
}
|
|
323
|
+
if (!skillTrim.trimmed && skillTrim.result.length >= maxSkills * warningThreshold) {
|
|
324
|
+
warnings.push(`Skill budget near limit: ${skillTrim.result.length}/${maxSkills} (${Math.round(skillTrim.result.length / maxSkills * 100)}%)`);
|
|
150
325
|
}
|
|
151
326
|
|
|
152
|
-
return {
|
|
327
|
+
return {
|
|
328
|
+
agents: agentTrim.result,
|
|
329
|
+
skills: skillTrim.result,
|
|
330
|
+
trimmed: agentTrim.trimmed || skillTrim.trimmed,
|
|
331
|
+
warnings,
|
|
332
|
+
};
|
|
153
333
|
}
|
|
154
334
|
|
|
155
335
|
/**
|
|
156
336
|
* Full resolution: combines domain matching, workflow binding, and budget enforcement.
|
|
337
|
+
* Uses enhanced planning resolution when workflow is 'plan'.
|
|
338
|
+
* Loads rules once and passes through to all internal functions.
|
|
157
339
|
*
|
|
158
340
|
* @param {string} taskDescription - Task text for domain matching
|
|
159
341
|
* @param {string} [workflowName] - Optional workflow name for binding resolution
|
|
@@ -161,11 +343,28 @@ function enforceContextBudget(agents, skills, projectRoot) {
|
|
|
161
343
|
* @returns {LoadPlan}
|
|
162
344
|
*/
|
|
163
345
|
function getLoadPlan(taskDescription, workflowName, projectRoot) {
|
|
346
|
+
if (typeof taskDescription !== 'string') {
|
|
347
|
+
return {
|
|
348
|
+
agents: [],
|
|
349
|
+
skills: [],
|
|
350
|
+
warnings: ['Invalid task description'],
|
|
351
|
+
budgetUsage: { agentsUsed: 0, agentsMax: DEFAULT_MAX_AGENTS, skillsUsed: 0, skillsMax: DEFAULT_MAX_SKILLS },
|
|
352
|
+
matchedDomains: [],
|
|
353
|
+
};
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
// Load rules ONCE and pass through to all helpers (H-1 fix)
|
|
164
357
|
const rules = loadRules(projectRoot);
|
|
165
358
|
const budget = rules.contextBudget || {};
|
|
359
|
+
const planningMandates = rules.planningMandates || {};
|
|
360
|
+
const isPlanWorkflow = workflowName === 'plan';
|
|
361
|
+
const maxAgents = budget.maxAgentsPerSession || DEFAULT_MAX_AGENTS;
|
|
362
|
+
const maxSkills = budget.maxSkillsPerSession || DEFAULT_MAX_SKILLS;
|
|
166
363
|
|
|
167
|
-
// Step 1: Domain
|
|
168
|
-
const taskResolution =
|
|
364
|
+
// Step 1: Domain matching (enhanced for planning workflows)
|
|
365
|
+
const taskResolution = isPlanWorkflow
|
|
366
|
+
? resolveForPlanningWithRules(taskDescription, rules)
|
|
367
|
+
: resolveForTaskWithRules(taskDescription, rules);
|
|
169
368
|
|
|
170
369
|
// Step 2: Workflow bindings (if workflow specified)
|
|
171
370
|
/** @type {Set<string>} */
|
|
@@ -174,7 +373,7 @@ function getLoadPlan(taskDescription, workflowName, projectRoot) {
|
|
|
174
373
|
const allSkills = new Set(taskResolution.skills);
|
|
175
374
|
|
|
176
375
|
if (workflowName) {
|
|
177
|
-
const wfResolution =
|
|
376
|
+
const wfResolution = resolveForWorkflowWithRules(workflowName, rules);
|
|
178
377
|
for (const agent of wfResolution.agents) {
|
|
179
378
|
allAgents.add(agent);
|
|
180
379
|
}
|
|
@@ -183,25 +382,39 @@ function getLoadPlan(taskDescription, workflowName, projectRoot) {
|
|
|
183
382
|
}
|
|
184
383
|
}
|
|
185
384
|
|
|
186
|
-
// Step 3: Budget enforcement
|
|
187
|
-
const
|
|
385
|
+
// Step 3: Budget enforcement (with protected items for planning)
|
|
386
|
+
const protectedItems = isPlanWorkflow
|
|
387
|
+
? { agents: [], skills: planningMandates.alwaysLoadSkills || [] }
|
|
388
|
+
: undefined;
|
|
389
|
+
|
|
390
|
+
const budgetResult = enforceContextBudgetWithRules(
|
|
391
|
+
[...allAgents],
|
|
392
|
+
[...allSkills],
|
|
393
|
+
rules,
|
|
394
|
+
protectedItems
|
|
395
|
+
);
|
|
188
396
|
|
|
397
|
+
// Construct complete object in one expression (H-3 immutability fix)
|
|
189
398
|
return {
|
|
190
399
|
agents: budgetResult.agents,
|
|
191
400
|
skills: budgetResult.skills,
|
|
192
401
|
warnings: budgetResult.warnings,
|
|
193
402
|
budgetUsage: {
|
|
194
403
|
agentsUsed: budgetResult.agents.length,
|
|
195
|
-
agentsMax:
|
|
404
|
+
agentsMax: maxAgents,
|
|
196
405
|
skillsUsed: budgetResult.skills.length,
|
|
197
|
-
skillsMax:
|
|
406
|
+
skillsMax: maxSkills,
|
|
198
407
|
},
|
|
199
408
|
matchedDomains: taskResolution.matchedDomains,
|
|
409
|
+
...(isPlanWorkflow && taskResolution.mandatoryRules
|
|
410
|
+
? { mandatoryRules: taskResolution.mandatoryRules }
|
|
411
|
+
: {}),
|
|
200
412
|
};
|
|
201
413
|
}
|
|
202
414
|
|
|
203
415
|
module.exports = {
|
|
204
416
|
resolveForTask,
|
|
417
|
+
resolveForPlanning,
|
|
205
418
|
resolveForWorkflow,
|
|
206
419
|
enforceContextBudget,
|
|
207
420
|
getLoadPlan,
|
package/lib/logger.js
ADDED
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Antigravity AI Kit — Structured Logger
|
|
3
|
+
*
|
|
4
|
+
* Provides structured JSON logging with log levels, correlation IDs,
|
|
5
|
+
* and module context. Replaces raw console.log across all runtime modules.
|
|
6
|
+
*
|
|
7
|
+
* @module lib/logger
|
|
8
|
+
* @author Emre Dursun
|
|
9
|
+
* @since v3.2.0
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
'use strict';
|
|
13
|
+
|
|
14
|
+
const crypto = require('crypto');
|
|
15
|
+
|
|
16
|
+
/** @typedef {'debug' | 'info' | 'warn' | 'error'} LogLevel */
|
|
17
|
+
|
|
18
|
+
const LOG_LEVELS = { debug: 0, info: 1, warn: 2, error: 3 };
|
|
19
|
+
|
|
20
|
+
/** Default minimum log level */
|
|
21
|
+
let minLevel = LOG_LEVELS[process.env.AG_LOG_LEVEL] ?? LOG_LEVELS.info;
|
|
22
|
+
|
|
23
|
+
/** Output mode: 'json' for structured, 'text' for human-readable */
|
|
24
|
+
let outputMode = process.env.AG_LOG_FORMAT === 'json' ? 'json' : 'text';
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Configures the logger.
|
|
28
|
+
*
|
|
29
|
+
* @param {object} options - Logger configuration
|
|
30
|
+
* @param {LogLevel} [options.level] - Minimum log level
|
|
31
|
+
* @param {'json' | 'text'} [options.format] - Output format
|
|
32
|
+
* @returns {void}
|
|
33
|
+
*/
|
|
34
|
+
function configure(options = {}) {
|
|
35
|
+
if (options.level && LOG_LEVELS[options.level] !== undefined) {
|
|
36
|
+
minLevel = LOG_LEVELS[options.level];
|
|
37
|
+
}
|
|
38
|
+
if (options.format === 'json' || options.format === 'text') {
|
|
39
|
+
outputMode = options.format;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Generates a short correlation ID for request tracing.
|
|
45
|
+
*
|
|
46
|
+
* @returns {string} 8-character hex correlation ID
|
|
47
|
+
*/
|
|
48
|
+
function correlationId() {
|
|
49
|
+
return crypto.randomBytes(4).toString('hex');
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Creates a child logger scoped to a specific module.
|
|
54
|
+
*
|
|
55
|
+
* @param {string} moduleName - Module name for context
|
|
56
|
+
* @param {string} [corrId] - Optional correlation ID (auto-generated if omitted)
|
|
57
|
+
* @returns {{ debug: Function, info: Function, warn: Function, error: Function, child: Function }}
|
|
58
|
+
*/
|
|
59
|
+
function createLogger(moduleName, corrId) {
|
|
60
|
+
const cid = corrId || correlationId();
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Emits a structured log entry.
|
|
64
|
+
*
|
|
65
|
+
* @param {LogLevel} level - Log level
|
|
66
|
+
* @param {string} message - Log message
|
|
67
|
+
* @param {object} [data] - Additional structured data
|
|
68
|
+
* @returns {void}
|
|
69
|
+
*/
|
|
70
|
+
function emit(level, message, data) {
|
|
71
|
+
if (LOG_LEVELS[level] < minLevel) {
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const entry = {
|
|
76
|
+
timestamp: new Date().toISOString(),
|
|
77
|
+
level,
|
|
78
|
+
module: moduleName,
|
|
79
|
+
correlationId: cid,
|
|
80
|
+
message,
|
|
81
|
+
...(data && Object.keys(data).length > 0 ? { data } : {}),
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
if (outputMode === 'json') {
|
|
85
|
+
const stream = level === 'error' ? process.stderr : process.stdout;
|
|
86
|
+
stream.write(JSON.stringify(entry) + '\n');
|
|
87
|
+
} else {
|
|
88
|
+
const prefix = `[${entry.timestamp.slice(11, 19)}] [${level.toUpperCase().padEnd(5)}] [${moduleName}]`;
|
|
89
|
+
const suffix = data && Object.keys(data).length > 0
|
|
90
|
+
? ` ${JSON.stringify(data)}`
|
|
91
|
+
: '';
|
|
92
|
+
const stream = level === 'error' ? console.error : console.log;
|
|
93
|
+
stream(`${prefix} ${message}${suffix}`);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
return {
|
|
98
|
+
debug: (message, data) => emit('debug', message, data),
|
|
99
|
+
info: (message, data) => emit('info', message, data),
|
|
100
|
+
warn: (message, data) => emit('warn', message, data),
|
|
101
|
+
error: (message, data) => emit('error', message, data),
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Creates a child logger inheriting the correlation ID.
|
|
105
|
+
*
|
|
106
|
+
* @param {string} childModule - Child module name
|
|
107
|
+
* @returns {ReturnType<typeof createLogger>}
|
|
108
|
+
*/
|
|
109
|
+
child: (childModule) => createLogger(childModule, cid),
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
module.exports = {
|
|
114
|
+
createLogger,
|
|
115
|
+
correlationId,
|
|
116
|
+
configure,
|
|
117
|
+
LOG_LEVELS,
|
|
118
|
+
};
|
package/lib/marketplace.js
CHANGED
|
@@ -13,10 +13,14 @@
|
|
|
13
13
|
|
|
14
14
|
const fs = require('fs');
|
|
15
15
|
const path = require('path');
|
|
16
|
-
const {
|
|
17
|
-
|
|
18
|
-
const
|
|
19
|
-
|
|
16
|
+
const { execFileSync } = require('child_process');
|
|
17
|
+
const { createCircuitBreaker } = require('./circuit-breaker');
|
|
18
|
+
const { createRateLimiter } = require('./rate-limiter');
|
|
19
|
+
|
|
20
|
+
const { AGENT_DIR, ENGINE_DIR } = require('./constants');
|
|
21
|
+
const { writeJsonAtomic } = require('./io');
|
|
22
|
+
const { createLogger } = require('./logger');
|
|
23
|
+
const log = createLogger('marketplace');
|
|
20
24
|
const INDEX_FILE = 'marketplace-index.json';
|
|
21
25
|
|
|
22
26
|
/** Registry index TTL in milliseconds (24 hours) */
|
|
@@ -25,6 +29,18 @@ const INDEX_TTL_MS = 24 * 60 * 60 * 1000;
|
|
|
25
29
|
/** Git clone timeout in milliseconds */
|
|
26
30
|
const GIT_CLONE_TIMEOUT_MS = 30000;
|
|
27
31
|
|
|
32
|
+
/** Circuit breaker for git clone operations */
|
|
33
|
+
const gitCloneBreaker = createCircuitBreaker('marketplace-git-clone', {
|
|
34
|
+
failureThreshold: 3,
|
|
35
|
+
resetTimeoutMs: 120000,
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
/** Rate limiter for marketplace install operations (5 per minute) */
|
|
39
|
+
const installLimiter = createRateLimiter('marketplace-install', {
|
|
40
|
+
maxTokens: 5,
|
|
41
|
+
refillRateMs: 60000,
|
|
42
|
+
});
|
|
43
|
+
|
|
28
44
|
/**
|
|
29
45
|
* @typedef {object} MarketEntry
|
|
30
46
|
* @property {string} name - Plugin name
|
|
@@ -74,15 +90,7 @@ function loadIndex(projectRoot) {
|
|
|
74
90
|
*/
|
|
75
91
|
function writeIndex(projectRoot, data) {
|
|
76
92
|
const filePath = resolveIndexPath(projectRoot);
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
if (!fs.existsSync(dir)) {
|
|
80
|
-
fs.mkdirSync(dir, { recursive: true });
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
const tempPath = `${filePath}.tmp`;
|
|
84
|
-
fs.writeFileSync(tempPath, JSON.stringify(data, null, 2) + '\n', 'utf-8');
|
|
85
|
-
fs.renameSync(tempPath, filePath);
|
|
93
|
+
writeJsonAtomic(filePath, data);
|
|
86
94
|
}
|
|
87
95
|
|
|
88
96
|
/**
|
|
@@ -209,18 +217,30 @@ function installFromMarket(projectRoot, pluginName) {
|
|
|
209
217
|
return { success: false, message: `Invalid repository URL: ${entry.repository}` };
|
|
210
218
|
}
|
|
211
219
|
|
|
220
|
+
// Rate limit check
|
|
221
|
+
const rateCheck = installLimiter.tryAcquire();
|
|
222
|
+
if (!rateCheck.allowed) {
|
|
223
|
+
return {
|
|
224
|
+
success: false,
|
|
225
|
+
message: `Rate limit exceeded — retry after ${Math.ceil(rateCheck.retryAfterMs / 1000)}s`,
|
|
226
|
+
};
|
|
227
|
+
}
|
|
228
|
+
|
|
212
229
|
// Create temp directory for clone
|
|
213
230
|
const tempDir = path.join(projectRoot, AGENT_DIR, ENGINE_DIR, `_temp_${Date.now()}`);
|
|
214
231
|
|
|
215
232
|
try {
|
|
216
233
|
fs.mkdirSync(tempDir, { recursive: true });
|
|
217
234
|
|
|
218
|
-
// Shallow clone with timeout (
|
|
235
|
+
// Shallow clone with circuit breaker and timeout (uses execFileSync to prevent shell injection)
|
|
219
236
|
try {
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
237
|
+
gitCloneBreaker.execute(() => {
|
|
238
|
+
execFileSync(
|
|
239
|
+
'git',
|
|
240
|
+
['clone', '--depth', '1', '--single-branch', entry.repository, tempDir],
|
|
241
|
+
{ timeout: GIT_CLONE_TIMEOUT_MS, stdio: 'pipe' }
|
|
242
|
+
);
|
|
243
|
+
});
|
|
224
244
|
} catch (gitError) {
|
|
225
245
|
return { success: false, message: `Git clone failed: ${gitError.message || 'timeout or network error'}` };
|
|
226
246
|
}
|
|
@@ -250,8 +270,11 @@ function installFromMarket(projectRoot, pluginName) {
|
|
|
250
270
|
// Delegate to plugin system for actual installation
|
|
251
271
|
try {
|
|
252
272
|
const pluginSystem = require('./plugin-system');
|
|
253
|
-
const result = pluginSystem.installPlugin(
|
|
254
|
-
|
|
273
|
+
const result = pluginSystem.installPlugin(tempDir, projectRoot);
|
|
274
|
+
const message = result.errors?.length > 0
|
|
275
|
+
? result.errors.join('; ')
|
|
276
|
+
: 'Plugin installed successfully';
|
|
277
|
+
return { success: result.success, message };
|
|
255
278
|
} catch (installError) {
|
|
256
279
|
return { success: false, message: `Plugin installation failed: ${installError.message}` };
|
|
257
280
|
}
|