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
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Antigravity AI Kit — Agent Conflict Detection
|
|
3
|
+
*
|
|
4
|
+
* Tracks file ownership by agents and detects concurrent modifications.
|
|
5
|
+
* Uses JSON-based locks for cross-platform compatibility.
|
|
6
|
+
*
|
|
7
|
+
* @module lib/conflict-detector
|
|
8
|
+
* @author Emre Dursun
|
|
9
|
+
* @since v3.0.0
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
'use strict';
|
|
13
|
+
|
|
14
|
+
const fs = require('fs');
|
|
15
|
+
const path = require('path');
|
|
16
|
+
|
|
17
|
+
const AGENT_DIR = '.agent';
|
|
18
|
+
const ENGINE_DIR = 'engine';
|
|
19
|
+
const FILE_LOCKS_FILE = 'file-locks.json';
|
|
20
|
+
|
|
21
|
+
/** Default lock TTL in milliseconds (30 minutes) */
|
|
22
|
+
const DEFAULT_LOCK_TTL_MS = 30 * 60 * 1000;
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* @typedef {object} FileLock
|
|
26
|
+
* @property {string} filePath - Relative path to the claimed file
|
|
27
|
+
* @property {string} agent - Agent name holding the lock
|
|
28
|
+
* @property {string} claimedAt - ISO timestamp
|
|
29
|
+
* @property {number} ttlMs - Time-to-live in milliseconds
|
|
30
|
+
*/
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* @typedef {object} ConflictReport
|
|
34
|
+
* @property {string} filePath - Path with conflict
|
|
35
|
+
* @property {string[]} agents - Agents claiming this file
|
|
36
|
+
* @property {'warning' | 'blocking'} severity - Conflict severity
|
|
37
|
+
*/
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Resolves the file locks path.
|
|
41
|
+
*
|
|
42
|
+
* @param {string} projectRoot - Root directory of the project
|
|
43
|
+
* @returns {string}
|
|
44
|
+
*/
|
|
45
|
+
function resolveLocksPath(projectRoot) {
|
|
46
|
+
return path.join(projectRoot, AGENT_DIR, ENGINE_DIR, FILE_LOCKS_FILE);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Loads current file locks, filtering out stale ones.
|
|
51
|
+
*
|
|
52
|
+
* @param {string} projectRoot - Root directory of the project
|
|
53
|
+
* @returns {FileLock[]}
|
|
54
|
+
*/
|
|
55
|
+
function loadLocks(projectRoot) {
|
|
56
|
+
const locksPath = resolveLocksPath(projectRoot);
|
|
57
|
+
|
|
58
|
+
if (!fs.existsSync(locksPath)) {
|
|
59
|
+
return [];
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
try {
|
|
63
|
+
const data = JSON.parse(fs.readFileSync(locksPath, 'utf-8'));
|
|
64
|
+
const locks = data.locks || [];
|
|
65
|
+
const now = Date.now();
|
|
66
|
+
|
|
67
|
+
// Filter out expired locks
|
|
68
|
+
return locks.filter((lock) => {
|
|
69
|
+
const claimedTime = new Date(lock.claimedAt).getTime();
|
|
70
|
+
return (now - claimedTime) < (lock.ttlMs || DEFAULT_LOCK_TTL_MS);
|
|
71
|
+
});
|
|
72
|
+
} catch {
|
|
73
|
+
return [];
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Writes locks to disk atomically.
|
|
79
|
+
*
|
|
80
|
+
* @param {string} projectRoot - Root directory of the project
|
|
81
|
+
* @param {FileLock[]} locks - Current locks
|
|
82
|
+
* @returns {void}
|
|
83
|
+
*/
|
|
84
|
+
function writeLocks(projectRoot, locks) {
|
|
85
|
+
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
|
+
const data = {
|
|
94
|
+
schemaVersion: '1.0.0',
|
|
95
|
+
lastUpdated: new Date().toISOString(),
|
|
96
|
+
locks,
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
fs.writeFileSync(tempPath, JSON.stringify(data, null, 2) + '\n', 'utf-8');
|
|
100
|
+
fs.renameSync(tempPath, locksPath);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Claims a file for an agent.
|
|
105
|
+
*
|
|
106
|
+
* @param {string} projectRoot - Root directory of the project
|
|
107
|
+
* @param {string} filePath - Relative path to file
|
|
108
|
+
* @param {string} agent - Agent name
|
|
109
|
+
* @param {number} [ttlMs] - Lock TTL in milliseconds
|
|
110
|
+
* @returns {{ success: boolean, conflict?: ConflictReport }}
|
|
111
|
+
*/
|
|
112
|
+
function claimFile(projectRoot, filePath, agent, ttlMs) {
|
|
113
|
+
const locks = loadLocks(projectRoot);
|
|
114
|
+
const normalizedPath = filePath.replace(/\\/g, '/');
|
|
115
|
+
const lockTtl = ttlMs || DEFAULT_LOCK_TTL_MS;
|
|
116
|
+
|
|
117
|
+
// Check for existing claim by a different agent
|
|
118
|
+
const existingLock = locks.find((l) => l.filePath === normalizedPath && l.agent !== agent);
|
|
119
|
+
|
|
120
|
+
if (existingLock) {
|
|
121
|
+
return {
|
|
122
|
+
success: false,
|
|
123
|
+
conflict: {
|
|
124
|
+
filePath: normalizedPath,
|
|
125
|
+
agents: [existingLock.agent, agent],
|
|
126
|
+
severity: 'blocking',
|
|
127
|
+
},
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// Update or create lock
|
|
132
|
+
const existingIndex = locks.findIndex((l) => l.filePath === normalizedPath && l.agent === agent);
|
|
133
|
+
|
|
134
|
+
if (existingIndex !== -1) {
|
|
135
|
+
locks[existingIndex].claimedAt = new Date().toISOString();
|
|
136
|
+
locks[existingIndex].ttlMs = lockTtl;
|
|
137
|
+
} else {
|
|
138
|
+
locks.push({
|
|
139
|
+
filePath: normalizedPath,
|
|
140
|
+
agent,
|
|
141
|
+
claimedAt: new Date().toISOString(),
|
|
142
|
+
ttlMs: lockTtl,
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
writeLocks(projectRoot, locks);
|
|
147
|
+
return { success: true };
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Releases a file lock held by an agent.
|
|
152
|
+
*
|
|
153
|
+
* @param {string} projectRoot - Root directory of the project
|
|
154
|
+
* @param {string} filePath - Relative path to file
|
|
155
|
+
* @param {string} agent - Agent name
|
|
156
|
+
* @returns {{ success: boolean }}
|
|
157
|
+
*/
|
|
158
|
+
function releaseFile(projectRoot, filePath, agent) {
|
|
159
|
+
const locks = loadLocks(projectRoot);
|
|
160
|
+
const normalizedPath = filePath.replace(/\\/g, '/');
|
|
161
|
+
const filteredLocks = locks.filter((l) => !(l.filePath === normalizedPath && l.agent === agent));
|
|
162
|
+
|
|
163
|
+
if (filteredLocks.length === locks.length) {
|
|
164
|
+
return { success: false };
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
writeLocks(projectRoot, filteredLocks);
|
|
168
|
+
return { success: true };
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Detects all current file conflicts.
|
|
173
|
+
*
|
|
174
|
+
* @param {string} projectRoot - Root directory of the project
|
|
175
|
+
* @returns {ConflictReport[]}
|
|
176
|
+
*/
|
|
177
|
+
function detectConflicts(projectRoot) {
|
|
178
|
+
const locks = loadLocks(projectRoot);
|
|
179
|
+
/** @type {Map<string, string[]>} */
|
|
180
|
+
const fileAgents = new Map();
|
|
181
|
+
|
|
182
|
+
for (const lock of locks) {
|
|
183
|
+
const agents = fileAgents.get(lock.filePath) || [];
|
|
184
|
+
if (!agents.includes(lock.agent)) {
|
|
185
|
+
agents.push(lock.agent);
|
|
186
|
+
}
|
|
187
|
+
fileAgents.set(lock.filePath, agents);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/** @type {ConflictReport[]} */
|
|
191
|
+
const conflicts = [];
|
|
192
|
+
|
|
193
|
+
for (const [filePath, agents] of fileAgents.entries()) {
|
|
194
|
+
if (agents.length > 1) {
|
|
195
|
+
conflicts.push({
|
|
196
|
+
filePath,
|
|
197
|
+
agents,
|
|
198
|
+
severity: agents.length > 2 ? 'blocking' : 'warning',
|
|
199
|
+
});
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
return conflicts;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* Gets file ownership information.
|
|
208
|
+
*
|
|
209
|
+
* @param {string} projectRoot - Root directory of the project
|
|
210
|
+
* @returns {Array<{ filePath: string, agent: string, claimedAt: string }>}
|
|
211
|
+
*/
|
|
212
|
+
function getFileOwnership(projectRoot) {
|
|
213
|
+
const locks = loadLocks(projectRoot);
|
|
214
|
+
|
|
215
|
+
return locks.map((lock) => ({
|
|
216
|
+
filePath: lock.filePath,
|
|
217
|
+
agent: lock.agent,
|
|
218
|
+
claimedAt: lock.claimedAt,
|
|
219
|
+
}));
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* Generates a full conflict report with metrics.
|
|
224
|
+
*
|
|
225
|
+
* @param {string} projectRoot - Root directory of the project
|
|
226
|
+
* @returns {{ activeLocks: number, conflicts: ConflictReport[], hasBlockingConflict: boolean }}
|
|
227
|
+
*/
|
|
228
|
+
function reportConflicts(projectRoot) {
|
|
229
|
+
const locks = loadLocks(projectRoot);
|
|
230
|
+
const conflicts = detectConflicts(projectRoot);
|
|
231
|
+
|
|
232
|
+
return {
|
|
233
|
+
activeLocks: locks.length,
|
|
234
|
+
conflicts,
|
|
235
|
+
hasBlockingConflict: conflicts.some((c) => c.severity === 'blocking'),
|
|
236
|
+
};
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
module.exports = {
|
|
240
|
+
claimFile,
|
|
241
|
+
releaseFile,
|
|
242
|
+
detectConflicts,
|
|
243
|
+
getFileOwnership,
|
|
244
|
+
reportConflicts,
|
|
245
|
+
};
|
|
@@ -0,0 +1,354 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Antigravity AI Kit — Autonomous Engineering Manager
|
|
3
|
+
*
|
|
4
|
+
* Data engine for sprint planning, task auto-assignment,
|
|
5
|
+
* and velocity metrics. Powers the sprint-orchestrator agent.
|
|
6
|
+
*
|
|
7
|
+
* @module lib/engineering-manager
|
|
8
|
+
* @author Emre Dursun
|
|
9
|
+
* @since v3.0.0
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
'use strict';
|
|
13
|
+
|
|
14
|
+
const fs = require('fs');
|
|
15
|
+
const path = require('path');
|
|
16
|
+
const crypto = require('crypto');
|
|
17
|
+
const taskModel = require('./task-model');
|
|
18
|
+
const agentRegistry = require('./agent-registry');
|
|
19
|
+
const agentReputation = require('./agent-reputation');
|
|
20
|
+
|
|
21
|
+
const AGENT_DIR = '.agent';
|
|
22
|
+
const ENGINE_DIR = 'engine';
|
|
23
|
+
const SPRINT_FILE = 'sprint-plans.json';
|
|
24
|
+
|
|
25
|
+
/** Maximum tasks per sprint suggestion */
|
|
26
|
+
const MAX_SPRINT_SIZE = 20;
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* @typedef {object} SprintPlan
|
|
30
|
+
* @property {string} id - Sprint plan ID
|
|
31
|
+
* @property {string} name - Sprint name
|
|
32
|
+
* @property {string} createdAt - ISO timestamp
|
|
33
|
+
* @property {object[]} assignments - Task assignments
|
|
34
|
+
* @property {string} status - Plan status: 'draft' | 'active' | 'completed'
|
|
35
|
+
*/
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* @typedef {object} TaskAssignment
|
|
39
|
+
* @property {string} taskId - Task ID
|
|
40
|
+
* @property {string} taskTitle - Task title
|
|
41
|
+
* @property {string} suggestedAgent - Recommended agent
|
|
42
|
+
* @property {string} reason - Why this agent was chosen
|
|
43
|
+
* @property {string} priority - Task priority
|
|
44
|
+
*/
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Resolves the sprint plans file path.
|
|
48
|
+
*
|
|
49
|
+
* @param {string} projectRoot - Root directory
|
|
50
|
+
* @returns {string}
|
|
51
|
+
*/
|
|
52
|
+
function resolveSprintPath(projectRoot) {
|
|
53
|
+
return path.join(projectRoot, AGENT_DIR, ENGINE_DIR, SPRINT_FILE);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Loads sprint plans from disk.
|
|
58
|
+
*
|
|
59
|
+
* @param {string} projectRoot - Root directory
|
|
60
|
+
* @returns {{ plans: SprintPlan[] }}
|
|
61
|
+
*/
|
|
62
|
+
function loadSprintData(projectRoot) {
|
|
63
|
+
const filePath = resolveSprintPath(projectRoot);
|
|
64
|
+
|
|
65
|
+
if (!fs.existsSync(filePath)) {
|
|
66
|
+
return { plans: [] };
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
try {
|
|
70
|
+
return JSON.parse(fs.readFileSync(filePath, 'utf-8'));
|
|
71
|
+
} catch {
|
|
72
|
+
return { plans: [] };
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Writes sprint data atomically.
|
|
78
|
+
*
|
|
79
|
+
* @param {string} projectRoot - Root directory
|
|
80
|
+
* @param {{ plans: SprintPlan[] }} data
|
|
81
|
+
* @returns {void}
|
|
82
|
+
*/
|
|
83
|
+
function writeSprintData(projectRoot, data) {
|
|
84
|
+
const filePath = resolveSprintPath(projectRoot);
|
|
85
|
+
const dir = path.dirname(filePath);
|
|
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);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Gets the current workload (in-progress task count) for an agent.
|
|
98
|
+
*
|
|
99
|
+
* @param {string} projectRoot - Root directory
|
|
100
|
+
* @param {string} agentName - Agent name
|
|
101
|
+
* @returns {number}
|
|
102
|
+
*/
|
|
103
|
+
function getAgentWorkload(projectRoot, agentName) {
|
|
104
|
+
try {
|
|
105
|
+
const inProgressTasks = taskModel.listTasks(projectRoot, {
|
|
106
|
+
status: 'in-progress',
|
|
107
|
+
assignee: agentName,
|
|
108
|
+
});
|
|
109
|
+
return inProgressTasks.length;
|
|
110
|
+
} catch {
|
|
111
|
+
return 0;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Finds the best agent for a task based on domain, reputation, and workload.
|
|
117
|
+
*
|
|
118
|
+
* @param {string} projectRoot - Root directory
|
|
119
|
+
* @param {string} taskTitle - Task title for domain matching
|
|
120
|
+
* @param {string} taskPriority - Task priority
|
|
121
|
+
* @returns {{ agent: string, reason: string }}
|
|
122
|
+
*/
|
|
123
|
+
function findBestAgent(projectRoot, taskTitle, taskPriority) {
|
|
124
|
+
let agents = [];
|
|
125
|
+
|
|
126
|
+
try {
|
|
127
|
+
const registry = agentRegistry.loadRegistry(projectRoot);
|
|
128
|
+
agents = registry.agents;
|
|
129
|
+
} catch {
|
|
130
|
+
return { agent: 'unassigned', reason: 'No agent registry available' };
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
if (agents.length === 0) {
|
|
134
|
+
return { agent: 'unassigned', reason: 'No agents registered' };
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// Score each agent
|
|
138
|
+
const scored = agents.map((agent) => {
|
|
139
|
+
let score = 0;
|
|
140
|
+
let reasons = [];
|
|
141
|
+
|
|
142
|
+
// 1. Domain match (keyword overlap between task title and agent domain)
|
|
143
|
+
const titleWords = taskTitle.toLowerCase().split(/\W+/);
|
|
144
|
+
const domainWords = (agent.domain || '').toLowerCase().split(/\W+/);
|
|
145
|
+
const domainOverlap = titleWords.filter((word) =>
|
|
146
|
+
word.length > 2 && domainWords.some((dw) => dw.includes(word) || word.includes(dw))
|
|
147
|
+
).length;
|
|
148
|
+
|
|
149
|
+
if (domainOverlap > 0) {
|
|
150
|
+
score += domainOverlap * 30;
|
|
151
|
+
reasons.push(`domain match (${domainOverlap} keywords)`);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// 2. Reputation score
|
|
155
|
+
try {
|
|
156
|
+
const reputation = agentReputation.getReputation(projectRoot, agent.name);
|
|
157
|
+
score += reputation.score / 10; // Normalize to ~0-100 range
|
|
158
|
+
if (reputation.score > 0) {
|
|
159
|
+
reasons.push(`reputation ${reputation.score}`);
|
|
160
|
+
}
|
|
161
|
+
} catch {
|
|
162
|
+
// No reputation data — neutral
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// 3. Workload penalty (fewer in-progress tasks = better)
|
|
166
|
+
const workload = getAgentWorkload(projectRoot, agent.name);
|
|
167
|
+
score -= workload * 20;
|
|
168
|
+
if (workload > 0) {
|
|
169
|
+
reasons.push(`workload ${workload} tasks`);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
return {
|
|
173
|
+
agent: agent.name,
|
|
174
|
+
score,
|
|
175
|
+
reason: reasons.length > 0 ? reasons.join(', ') : 'general availability',
|
|
176
|
+
};
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
// Sort by score descending
|
|
180
|
+
scored.sort((a, b) => b.score - a.score);
|
|
181
|
+
|
|
182
|
+
return {
|
|
183
|
+
agent: scored[0].agent,
|
|
184
|
+
reason: scored[0].reason,
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Generates a sprint plan from open tasks.
|
|
190
|
+
* This is an advisory suggestion — never auto-executed.
|
|
191
|
+
*
|
|
192
|
+
* @param {string} projectRoot - Root directory
|
|
193
|
+
* @param {object} [options] - Sprint options
|
|
194
|
+
* @param {string} [options.name] - Sprint name
|
|
195
|
+
* @param {number} [options.maxTasks] - Max tasks to include
|
|
196
|
+
* @returns {SprintPlan}
|
|
197
|
+
*/
|
|
198
|
+
function generateSprintPlan(projectRoot, options = {}) {
|
|
199
|
+
const sprintName = options.name || `Sprint-${new Date().toISOString().slice(0, 10)}`;
|
|
200
|
+
const maxTasks = options.maxTasks || MAX_SPRINT_SIZE;
|
|
201
|
+
|
|
202
|
+
// Get all open/blocked tasks, prioritized
|
|
203
|
+
let tasks = [];
|
|
204
|
+
try {
|
|
205
|
+
const openTasks = taskModel.listTasks(projectRoot, { status: 'open' });
|
|
206
|
+
const blockedTasks = taskModel.listTasks(projectRoot, { status: 'blocked' });
|
|
207
|
+
tasks = [...openTasks, ...blockedTasks];
|
|
208
|
+
} catch {
|
|
209
|
+
tasks = [];
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// Priority sort: critical > high > medium > low
|
|
213
|
+
const priorityOrder = { critical: 0, high: 1, medium: 2, low: 3 };
|
|
214
|
+
tasks.sort((a, b) => {
|
|
215
|
+
const aPriority = priorityOrder[a.priority] ?? 2;
|
|
216
|
+
const bPriority = priorityOrder[b.priority] ?? 2;
|
|
217
|
+
return aPriority - bPriority;
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
// Take top N
|
|
221
|
+
const sprintTasks = tasks.slice(0, maxTasks);
|
|
222
|
+
|
|
223
|
+
// Auto-assign each
|
|
224
|
+
/** @type {TaskAssignment[]} */
|
|
225
|
+
const assignments = sprintTasks.map((task) => {
|
|
226
|
+
const best = findBestAgent(projectRoot, task.title, task.priority);
|
|
227
|
+
return {
|
|
228
|
+
taskId: task.id,
|
|
229
|
+
taskTitle: task.title,
|
|
230
|
+
suggestedAgent: best.agent,
|
|
231
|
+
reason: best.reason,
|
|
232
|
+
priority: task.priority,
|
|
233
|
+
};
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
/** @type {SprintPlan} */
|
|
237
|
+
const plan = {
|
|
238
|
+
id: `SPR-${crypto.randomUUID().slice(0, 8).toUpperCase()}`,
|
|
239
|
+
name: sprintName,
|
|
240
|
+
createdAt: new Date().toISOString(),
|
|
241
|
+
assignments,
|
|
242
|
+
status: 'draft',
|
|
243
|
+
};
|
|
244
|
+
|
|
245
|
+
// Persist
|
|
246
|
+
const data = loadSprintData(projectRoot);
|
|
247
|
+
data.plans.push(plan);
|
|
248
|
+
writeSprintData(projectRoot, data);
|
|
249
|
+
|
|
250
|
+
return plan;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/**
|
|
254
|
+
* Auto-assigns a single task to the best available agent.
|
|
255
|
+
*
|
|
256
|
+
* @param {string} projectRoot - Root directory
|
|
257
|
+
* @param {string} taskId - Task ID
|
|
258
|
+
* @returns {{ success: boolean, agent?: string, reason?: string, error?: string }}
|
|
259
|
+
*/
|
|
260
|
+
function autoAssignTask(projectRoot, taskId) {
|
|
261
|
+
const task = taskModel.getTask(projectRoot, taskId);
|
|
262
|
+
if (!task) {
|
|
263
|
+
return { success: false, error: `Task not found: ${taskId}` };
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
const best = findBestAgent(projectRoot, task.title, task.priority);
|
|
267
|
+
|
|
268
|
+
if (best.agent === 'unassigned') {
|
|
269
|
+
return { success: false, error: best.reason };
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
const result = taskModel.updateTask(projectRoot, taskId, { assignee: best.agent });
|
|
273
|
+
|
|
274
|
+
if (result.success) {
|
|
275
|
+
return { success: true, agent: best.agent, reason: best.reason };
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
return { success: false, error: 'Failed to update task' };
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
/**
|
|
282
|
+
* Suggests the next highest-priority unblocked task.
|
|
283
|
+
*
|
|
284
|
+
* @param {string} projectRoot - Root directory
|
|
285
|
+
* @returns {{ task: object | null, reason: string }}
|
|
286
|
+
*/
|
|
287
|
+
function suggestNextTask(projectRoot) {
|
|
288
|
+
let openTasks = [];
|
|
289
|
+
try {
|
|
290
|
+
openTasks = taskModel.listTasks(projectRoot, { status: 'open' });
|
|
291
|
+
} catch {
|
|
292
|
+
return { task: null, reason: 'No tasks available' };
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
if (openTasks.length === 0) {
|
|
296
|
+
return { task: null, reason: 'No open tasks remaining' };
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
// Priority sort
|
|
300
|
+
const priorityOrder = { critical: 0, high: 1, medium: 2, low: 3 };
|
|
301
|
+
openTasks.sort((a, b) => {
|
|
302
|
+
const aPriority = priorityOrder[a.priority] ?? 2;
|
|
303
|
+
const bPriority = priorityOrder[b.priority] ?? 2;
|
|
304
|
+
return aPriority - bPriority;
|
|
305
|
+
});
|
|
306
|
+
|
|
307
|
+
const topTask = openTasks[0];
|
|
308
|
+
return {
|
|
309
|
+
task: topTask,
|
|
310
|
+
reason: `Highest priority open task (${topTask.priority})`,
|
|
311
|
+
};
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
/**
|
|
315
|
+
* Returns sprint velocity and progress metrics.
|
|
316
|
+
*
|
|
317
|
+
* @param {string} projectRoot - Root directory
|
|
318
|
+
* @returns {{ totalSprints: number, activeSprint: SprintPlan | null, velocity: number, completionRate: number }}
|
|
319
|
+
*/
|
|
320
|
+
function getSprintMetrics(projectRoot) {
|
|
321
|
+
const data = loadSprintData(projectRoot);
|
|
322
|
+
const activeSprint = data.plans.find((p) => p.status === 'active') || null;
|
|
323
|
+
const completedSprints = data.plans.filter((p) => p.status === 'completed');
|
|
324
|
+
|
|
325
|
+
// Velocity: average assignments per completed sprint
|
|
326
|
+
const velocity = completedSprints.length > 0
|
|
327
|
+
? Math.round(
|
|
328
|
+
completedSprints.reduce((sum, s) => sum + s.assignments.length, 0) / completedSprints.length
|
|
329
|
+
)
|
|
330
|
+
: 0;
|
|
331
|
+
|
|
332
|
+
// Task metrics
|
|
333
|
+
let completionRate = 0;
|
|
334
|
+
try {
|
|
335
|
+
const metrics = taskModel.getTaskMetrics(projectRoot);
|
|
336
|
+
completionRate = metrics.completionRate;
|
|
337
|
+
} catch {
|
|
338
|
+
completionRate = 0;
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
return {
|
|
342
|
+
totalSprints: data.plans.length,
|
|
343
|
+
activeSprint,
|
|
344
|
+
velocity,
|
|
345
|
+
completionRate,
|
|
346
|
+
};
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
module.exports = {
|
|
350
|
+
generateSprintPlan,
|
|
351
|
+
autoAssignTask,
|
|
352
|
+
suggestNextTask,
|
|
353
|
+
getSprintMetrics,
|
|
354
|
+
};
|