antigravity-ai-kit 2.1.0 → 3.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (114) hide show
  1. package/.agent/README.md +4 -4
  2. package/.agent/agents/README.md +16 -12
  3. package/.agent/agents/architect.md +1 -0
  4. package/.agent/agents/backend-specialist.md +11 -0
  5. package/.agent/agents/code-reviewer.md +1 -0
  6. package/.agent/agents/database-architect.md +11 -0
  7. package/.agent/agents/devops-engineer.md +11 -0
  8. package/.agent/agents/e2e-runner.md +1 -0
  9. package/.agent/agents/explorer-agent.md +11 -0
  10. package/.agent/agents/frontend-specialist.md +11 -0
  11. package/.agent/agents/mobile-developer.md +11 -0
  12. package/.agent/agents/performance-optimizer.md +11 -0
  13. package/.agent/agents/planner.md +1 -0
  14. package/.agent/agents/refactor-cleaner.md +1 -0
  15. package/.agent/agents/reliability-engineer.md +11 -0
  16. package/.agent/agents/security-reviewer.md +1 -0
  17. package/.agent/agents/sprint-orchestrator.md +10 -0
  18. package/.agent/agents/tdd-guide.md +1 -0
  19. package/.agent/commands/code-review.md +1 -0
  20. package/.agent/commands/debug.md +1 -0
  21. package/.agent/commands/deploy.md +1 -0
  22. package/.agent/commands/help.md +252 -31
  23. package/.agent/commands/plan.md +1 -0
  24. package/.agent/commands/status.md +1 -0
  25. package/.agent/commands/tdd.md +1 -0
  26. package/.agent/contexts/brainstorm.md +26 -0
  27. package/.agent/contexts/debug.md +28 -0
  28. package/.agent/contexts/implement.md +29 -0
  29. package/.agent/contexts/review.md +27 -0
  30. package/.agent/contexts/ship.md +28 -0
  31. package/.agent/engine/identity.json +13 -0
  32. package/.agent/engine/loading-rules.json +23 -1
  33. package/.agent/engine/marketplace-index.json +29 -0
  34. package/.agent/engine/reliability-config.json +14 -0
  35. package/.agent/engine/sdlc-map.json +44 -0
  36. package/.agent/engine/workflow-state.json +28 -2
  37. package/.agent/hooks/hooks.json +27 -25
  38. package/.agent/manifest.json +12 -4
  39. package/.agent/rules.md +2 -1
  40. package/.agent/skills/README.md +10 -5
  41. package/.agent/skills/i18n-localization/SKILL.md +191 -0
  42. package/.agent/skills/mcp-integration/SKILL.md +224 -0
  43. package/.agent/skills/parallel-agents/SKILL.md +1 -1
  44. package/.agent/skills/shell-conventions/SKILL.md +92 -0
  45. package/.agent/skills/ui-ux-pro-max/SKILL.md +557 -0
  46. package/.agent/skills/ui-ux-pro-max/data/charts.csv +26 -0
  47. package/.agent/skills/ui-ux-pro-max/data/colors.csv +97 -0
  48. package/.agent/skills/ui-ux-pro-max/data/icons.csv +101 -0
  49. package/.agent/skills/ui-ux-pro-max/data/landing.csv +31 -0
  50. package/.agent/skills/ui-ux-pro-max/data/products.csv +97 -0
  51. package/.agent/skills/ui-ux-pro-max/data/react-performance.csv +45 -0
  52. package/.agent/skills/ui-ux-pro-max/data/stacks/astro.csv +54 -0
  53. package/.agent/skills/ui-ux-pro-max/data/stacks/flutter.csv +53 -0
  54. package/.agent/skills/ui-ux-pro-max/data/stacks/html-tailwind.csv +56 -0
  55. package/.agent/skills/ui-ux-pro-max/data/stacks/jetpack-compose.csv +53 -0
  56. package/.agent/skills/ui-ux-pro-max/data/stacks/nextjs.csv +53 -0
  57. package/.agent/skills/ui-ux-pro-max/data/stacks/nuxt-ui.csv +51 -0
  58. package/.agent/skills/ui-ux-pro-max/data/stacks/nuxtjs.csv +59 -0
  59. package/.agent/skills/ui-ux-pro-max/data/stacks/react-native.csv +52 -0
  60. package/.agent/skills/ui-ux-pro-max/data/stacks/react.csv +54 -0
  61. package/.agent/skills/ui-ux-pro-max/data/stacks/shadcn.csv +61 -0
  62. package/.agent/skills/ui-ux-pro-max/data/stacks/svelte.csv +54 -0
  63. package/.agent/skills/ui-ux-pro-max/data/stacks/swiftui.csv +51 -0
  64. package/.agent/skills/ui-ux-pro-max/data/stacks/vue.csv +50 -0
  65. package/.agent/skills/ui-ux-pro-max/data/styles.csv +68 -0
  66. package/.agent/skills/ui-ux-pro-max/data/typography.csv +58 -0
  67. package/.agent/skills/ui-ux-pro-max/data/ui-reasoning.csv +101 -0
  68. package/.agent/skills/ui-ux-pro-max/data/ux-guidelines.csv +100 -0
  69. package/.agent/skills/ui-ux-pro-max/data/web-interface.csv +31 -0
  70. package/.agent/skills/ui-ux-pro-max/scripts/core.py +253 -0
  71. package/.agent/skills/ui-ux-pro-max/scripts/design_system.py +1067 -0
  72. package/.agent/skills/ui-ux-pro-max/scripts/search.py +114 -0
  73. package/.agent/templates/adr-template.md +32 -0
  74. package/.agent/templates/bug-report.md +37 -0
  75. package/.agent/templates/feature-request.md +32 -0
  76. package/.agent/workflows/README.md +92 -78
  77. package/.agent/workflows/brainstorm.md +154 -100
  78. package/.agent/workflows/create.md +142 -75
  79. package/.agent/workflows/debug.md +157 -98
  80. package/.agent/workflows/deploy.md +195 -144
  81. package/.agent/workflows/enhance.md +157 -65
  82. package/.agent/workflows/orchestrate.md +171 -114
  83. package/.agent/workflows/plan.md +147 -72
  84. package/.agent/workflows/preview.md +140 -83
  85. package/.agent/workflows/quality-gate.md +196 -0
  86. package/.agent/workflows/retrospective.md +197 -0
  87. package/.agent/workflows/review.md +188 -0
  88. package/.agent/workflows/status.md +142 -91
  89. package/.agent/workflows/test.md +168 -95
  90. package/.agent/workflows/ui-ux-pro-max.md +181 -127
  91. package/README.md +215 -78
  92. package/bin/ag-kit.js +344 -10
  93. package/lib/agent-registry.js +214 -0
  94. package/lib/agent-reputation.js +351 -0
  95. package/lib/cli-commands.js +235 -0
  96. package/lib/conflict-detector.js +245 -0
  97. package/lib/engineering-manager.js +354 -0
  98. package/lib/error-budget.js +294 -0
  99. package/lib/hook-system.js +252 -0
  100. package/lib/identity.js +245 -0
  101. package/lib/loading-engine.js +208 -0
  102. package/lib/marketplace.js +298 -0
  103. package/lib/plugin-system.js +604 -0
  104. package/lib/security-scanner.js +309 -0
  105. package/lib/self-healing.js +434 -0
  106. package/lib/session-manager.js +261 -0
  107. package/lib/skill-sandbox.js +244 -0
  108. package/lib/task-governance.js +523 -0
  109. package/lib/task-model.js +317 -0
  110. package/lib/updater.js +201 -0
  111. package/lib/verify.js +240 -0
  112. package/lib/workflow-engine.js +353 -0
  113. package/lib/workflow-persistence.js +160 -0
  114. package/package.json +7 -3
@@ -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
+ };