@devran-ai/kit 4.1.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.
Files changed (231) hide show
  1. package/.agent/CheatSheet.md +350 -0
  2. package/.agent/README.md +76 -0
  3. package/.agent/agents/README.md +155 -0
  4. package/.agent/agents/architect.md +185 -0
  5. package/.agent/agents/backend-specialist.md +276 -0
  6. package/.agent/agents/build-error-resolver.md +207 -0
  7. package/.agent/agents/code-reviewer.md +162 -0
  8. package/.agent/agents/database-architect.md +138 -0
  9. package/.agent/agents/devops-engineer.md +144 -0
  10. package/.agent/agents/doc-updater.md +229 -0
  11. package/.agent/agents/e2e-runner.md +145 -0
  12. package/.agent/agents/explorer-agent.md +143 -0
  13. package/.agent/agents/frontend-specialist.md +144 -0
  14. package/.agent/agents/go-reviewer.md +128 -0
  15. package/.agent/agents/knowledge-agent.md +197 -0
  16. package/.agent/agents/mobile-developer.md +150 -0
  17. package/.agent/agents/performance-optimizer.md +175 -0
  18. package/.agent/agents/planner.md +133 -0
  19. package/.agent/agents/pr-reviewer.md +148 -0
  20. package/.agent/agents/python-reviewer.md +123 -0
  21. package/.agent/agents/refactor-cleaner.md +201 -0
  22. package/.agent/agents/reliability-engineer.md +156 -0
  23. package/.agent/agents/security-reviewer.md +141 -0
  24. package/.agent/agents/sprint-orchestrator.md +124 -0
  25. package/.agent/agents/tdd-guide.md +179 -0
  26. package/.agent/agents/typescript-reviewer.md +110 -0
  27. package/.agent/checklists/README.md +102 -0
  28. package/.agent/checklists/pre-commit.md +93 -0
  29. package/.agent/checklists/session-end.md +99 -0
  30. package/.agent/checklists/session-start.md +102 -0
  31. package/.agent/checklists/task-complete.md +81 -0
  32. package/.agent/commands/README.md +130 -0
  33. package/.agent/commands/adr.md +29 -0
  34. package/.agent/commands/ask.md +28 -0
  35. package/.agent/commands/build.md +30 -0
  36. package/.agent/commands/changelog.md +40 -0
  37. package/.agent/commands/checkpoint.md +28 -0
  38. package/.agent/commands/code-review.md +65 -0
  39. package/.agent/commands/compact.md +28 -0
  40. package/.agent/commands/cook.md +30 -0
  41. package/.agent/commands/db.md +30 -0
  42. package/.agent/commands/debug.md +31 -0
  43. package/.agent/commands/deploy.md +37 -0
  44. package/.agent/commands/design.md +29 -0
  45. package/.agent/commands/doc.md +30 -0
  46. package/.agent/commands/eval.md +30 -0
  47. package/.agent/commands/fix.md +32 -0
  48. package/.agent/commands/git.md +32 -0
  49. package/.agent/commands/help.md +273 -0
  50. package/.agent/commands/implement.md +30 -0
  51. package/.agent/commands/integrate.md +32 -0
  52. package/.agent/commands/learn.md +29 -0
  53. package/.agent/commands/perf.md +31 -0
  54. package/.agent/commands/plan.md +56 -0
  55. package/.agent/commands/pr-describe.md +65 -0
  56. package/.agent/commands/pr-fix.md +45 -0
  57. package/.agent/commands/pr-merge.md +45 -0
  58. package/.agent/commands/pr-review.md +50 -0
  59. package/.agent/commands/pr-split.md +54 -0
  60. package/.agent/commands/pr-status.md +56 -0
  61. package/.agent/commands/pr.md +58 -0
  62. package/.agent/commands/refactor.md +32 -0
  63. package/.agent/commands/research.md +28 -0
  64. package/.agent/commands/scout.md +30 -0
  65. package/.agent/commands/security-scan.md +33 -0
  66. package/.agent/commands/setup.md +31 -0
  67. package/.agent/commands/status.md +59 -0
  68. package/.agent/commands/tdd.md +73 -0
  69. package/.agent/commands/verify.md +58 -0
  70. package/.agent/contexts/brainstorm.md +26 -0
  71. package/.agent/contexts/debug.md +28 -0
  72. package/.agent/contexts/implement.md +29 -0
  73. package/.agent/contexts/plan-quality-log.md +30 -0
  74. package/.agent/contexts/review.md +27 -0
  75. package/.agent/contexts/ship.md +28 -0
  76. package/.agent/decisions/001-trust-grade-governance.md +46 -0
  77. package/.agent/decisions/002-cross-ide-generation.md +15 -0
  78. package/.agent/engine/identity.json +4 -0
  79. package/.agent/engine/loading-rules.json +193 -0
  80. package/.agent/engine/marketplace-index.json +29 -0
  81. package/.agent/engine/mcp-servers/filesystem.json +9 -0
  82. package/.agent/engine/mcp-servers/github.json +11 -0
  83. package/.agent/engine/mcp-servers/postgres.json +11 -0
  84. package/.agent/engine/mcp-servers/supabase.json +11 -0
  85. package/.agent/engine/mcp-servers/vercel.json +11 -0
  86. package/.agent/engine/reliability-config.json +14 -0
  87. package/.agent/engine/sdlc-map.json +50 -0
  88. package/.agent/engine/workflow-state.json +167 -0
  89. package/.agent/hooks/README.md +101 -0
  90. package/.agent/hooks/hooks.json +104 -0
  91. package/.agent/hooks/templates/session-end.md +110 -0
  92. package/.agent/hooks/templates/session-start.md +95 -0
  93. package/.agent/manifest.json +466 -0
  94. package/.agent/rules/agent-upgrade-policy.md +56 -0
  95. package/.agent/rules/architecture.md +111 -0
  96. package/.agent/rules/coding-style.md +75 -0
  97. package/.agent/rules/documentation.md +74 -0
  98. package/.agent/rules/git-workflow.md +140 -0
  99. package/.agent/rules/quality-gate.md +117 -0
  100. package/.agent/rules/security.md +67 -0
  101. package/.agent/rules/sprint-tracking.md +103 -0
  102. package/.agent/rules/testing.md +80 -0
  103. package/.agent/rules/workflow-standards.md +30 -0
  104. package/.agent/rules.md +293 -0
  105. package/.agent/session-context.md +69 -0
  106. package/.agent/session-state.json +27 -0
  107. package/.agent/skills/README.md +135 -0
  108. package/.agent/skills/api-patterns/SKILL.md +117 -0
  109. package/.agent/skills/app-builder/SKILL.md +202 -0
  110. package/.agent/skills/architecture/SKILL.md +101 -0
  111. package/.agent/skills/behavioral-modes/SKILL.md +295 -0
  112. package/.agent/skills/brainstorming/SKILL.md +156 -0
  113. package/.agent/skills/clean-code/SKILL.md +142 -0
  114. package/.agent/skills/context-budget/SKILL.md +78 -0
  115. package/.agent/skills/continuous-learning/SKILL.md +145 -0
  116. package/.agent/skills/database-design/SKILL.md +303 -0
  117. package/.agent/skills/debugging-strategies/SKILL.md +158 -0
  118. package/.agent/skills/deployment-procedures/SKILL.md +191 -0
  119. package/.agent/skills/docker-patterns/SKILL.md +161 -0
  120. package/.agent/skills/eval-harness/SKILL.md +89 -0
  121. package/.agent/skills/frontend-patterns/SKILL.md +141 -0
  122. package/.agent/skills/git-workflow/SKILL.md +159 -0
  123. package/.agent/skills/i18n-localization/SKILL.md +191 -0
  124. package/.agent/skills/intelligent-routing/SKILL.md +180 -0
  125. package/.agent/skills/mcp-integration/SKILL.md +240 -0
  126. package/.agent/skills/mobile-design/SKILL.md +191 -0
  127. package/.agent/skills/nodejs-patterns/SKILL.md +164 -0
  128. package/.agent/skills/parallel-agents/SKILL.md +200 -0
  129. package/.agent/skills/performance-profiling/SKILL.md +134 -0
  130. package/.agent/skills/plan-validation/SKILL.md +192 -0
  131. package/.agent/skills/plan-writing/SKILL.md +183 -0
  132. package/.agent/skills/plan-writing/domain-enhancers.md +184 -0
  133. package/.agent/skills/plan-writing/plan-retrospective.md +116 -0
  134. package/.agent/skills/plan-writing/plan-schema.md +119 -0
  135. package/.agent/skills/pr-toolkit/SKILL.md +174 -0
  136. package/.agent/skills/production-readiness/SKILL.md +126 -0
  137. package/.agent/skills/security-practices/SKILL.md +109 -0
  138. package/.agent/skills/shell-conventions/SKILL.md +92 -0
  139. package/.agent/skills/strategic-compact/SKILL.md +62 -0
  140. package/.agent/skills/testing-patterns/SKILL.md +141 -0
  141. package/.agent/skills/typescript-expert/SKILL.md +160 -0
  142. package/.agent/skills/ui-ux-pro-max/SKILL.md +137 -0
  143. package/.agent/skills/ui-ux-pro-max/data/charts.csv +26 -0
  144. package/.agent/skills/ui-ux-pro-max/data/colors.csv +97 -0
  145. package/.agent/skills/ui-ux-pro-max/data/icons.csv +101 -0
  146. package/.agent/skills/ui-ux-pro-max/data/landing.csv +31 -0
  147. package/.agent/skills/ui-ux-pro-max/data/products.csv +97 -0
  148. package/.agent/skills/ui-ux-pro-max/data/react-performance.csv +45 -0
  149. package/.agent/skills/ui-ux-pro-max/data/stacks/astro.csv +54 -0
  150. package/.agent/skills/ui-ux-pro-max/data/stacks/flutter.csv +53 -0
  151. package/.agent/skills/ui-ux-pro-max/data/stacks/html-tailwind.csv +56 -0
  152. package/.agent/skills/ui-ux-pro-max/data/stacks/jetpack-compose.csv +53 -0
  153. package/.agent/skills/ui-ux-pro-max/data/stacks/nextjs.csv +53 -0
  154. package/.agent/skills/ui-ux-pro-max/data/stacks/nuxt-ui.csv +51 -0
  155. package/.agent/skills/ui-ux-pro-max/data/stacks/nuxtjs.csv +59 -0
  156. package/.agent/skills/ui-ux-pro-max/data/stacks/react-native.csv +52 -0
  157. package/.agent/skills/ui-ux-pro-max/data/stacks/react.csv +54 -0
  158. package/.agent/skills/ui-ux-pro-max/data/stacks/shadcn.csv +61 -0
  159. package/.agent/skills/ui-ux-pro-max/data/stacks/svelte.csv +54 -0
  160. package/.agent/skills/ui-ux-pro-max/data/stacks/swiftui.csv +51 -0
  161. package/.agent/skills/ui-ux-pro-max/data/stacks/vue.csv +50 -0
  162. package/.agent/skills/ui-ux-pro-max/data/styles.csv +68 -0
  163. package/.agent/skills/ui-ux-pro-max/data/typography.csv +58 -0
  164. package/.agent/skills/ui-ux-pro-max/data/ui-reasoning.csv +101 -0
  165. package/.agent/skills/ui-ux-pro-max/data/ux-guidelines.csv +100 -0
  166. package/.agent/skills/ui-ux-pro-max/data/web-interface.csv +31 -0
  167. package/.agent/skills/ui-ux-pro-max/scripts/core.py +253 -0
  168. package/.agent/skills/ui-ux-pro-max/scripts/design_system.py +1067 -0
  169. package/.agent/skills/ui-ux-pro-max/scripts/search.py +114 -0
  170. package/.agent/skills/verification-loop/SKILL.md +89 -0
  171. package/.agent/skills/webapp-testing/SKILL.md +175 -0
  172. package/.agent/templates/adr-template.md +32 -0
  173. package/.agent/templates/bug-report.md +37 -0
  174. package/.agent/templates/feature-request.md +32 -0
  175. package/.agent/workflows/README.md +101 -0
  176. package/.agent/workflows/brainstorm.md +86 -0
  177. package/.agent/workflows/create.md +85 -0
  178. package/.agent/workflows/debug.md +83 -0
  179. package/.agent/workflows/deploy.md +114 -0
  180. package/.agent/workflows/enhance.md +85 -0
  181. package/.agent/workflows/orchestrate.md +106 -0
  182. package/.agent/workflows/plan.md +105 -0
  183. package/.agent/workflows/pr-fix.md +163 -0
  184. package/.agent/workflows/pr-merge.md +117 -0
  185. package/.agent/workflows/pr-review.md +178 -0
  186. package/.agent/workflows/pr-split.md +118 -0
  187. package/.agent/workflows/pr.md +184 -0
  188. package/.agent/workflows/preflight.md +107 -0
  189. package/.agent/workflows/preview.md +95 -0
  190. package/.agent/workflows/quality-gate.md +103 -0
  191. package/.agent/workflows/retrospective.md +100 -0
  192. package/.agent/workflows/review.md +104 -0
  193. package/.agent/workflows/status.md +89 -0
  194. package/.agent/workflows/test.md +98 -0
  195. package/.agent/workflows/ui-ux-pro-max.md +93 -0
  196. package/.agent/workflows/upgrade.md +97 -0
  197. package/LICENSE +21 -0
  198. package/README.md +218 -0
  199. package/bin/kit.js +773 -0
  200. package/lib/agent-registry.js +228 -0
  201. package/lib/agent-reputation.js +343 -0
  202. package/lib/circuit-breaker.js +195 -0
  203. package/lib/cli-commands.js +322 -0
  204. package/lib/config-validator.js +274 -0
  205. package/lib/conflict-detector.js +252 -0
  206. package/lib/constants.js +47 -0
  207. package/lib/engineering-manager.js +336 -0
  208. package/lib/error-budget.js +370 -0
  209. package/lib/hook-system.js +256 -0
  210. package/lib/ide-generator.js +434 -0
  211. package/lib/identity.js +240 -0
  212. package/lib/io.js +146 -0
  213. package/lib/learning-engine.js +163 -0
  214. package/lib/loading-engine.js +421 -0
  215. package/lib/logger.js +118 -0
  216. package/lib/marketplace.js +321 -0
  217. package/lib/plugin-system.js +604 -0
  218. package/lib/plugin-verifier.js +197 -0
  219. package/lib/rate-limiter.js +113 -0
  220. package/lib/security-scanner.js +312 -0
  221. package/lib/self-healing.js +468 -0
  222. package/lib/session-manager.js +264 -0
  223. package/lib/skill-sandbox.js +244 -0
  224. package/lib/task-governance.js +522 -0
  225. package/lib/task-model.js +332 -0
  226. package/lib/updater.js +240 -0
  227. package/lib/verify.js +279 -0
  228. package/lib/workflow-engine.js +373 -0
  229. package/lib/workflow-events.js +166 -0
  230. package/lib/workflow-persistence.js +160 -0
  231. package/package.json +57 -0
@@ -0,0 +1,336 @@
1
+ /**
2
+ * Devran 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, ENGINE_DIR } = require('./constants');
22
+ const { writeJsonAtomic } = require('./io');
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
+ writeJsonAtomic(filePath, data);
86
+ }
87
+
88
+ /**
89
+ * Gets the current workload (in-progress task count) for an agent.
90
+ *
91
+ * @param {string} projectRoot - Root directory
92
+ * @param {string} agentName - Agent name
93
+ * @returns {number}
94
+ */
95
+ function getAgentWorkload(projectRoot, agentName) {
96
+ try {
97
+ const inProgressTasks = taskModel.listTasks(projectRoot, {
98
+ status: 'in-progress',
99
+ assignee: agentName,
100
+ });
101
+ return inProgressTasks.length;
102
+ } catch {
103
+ return 0;
104
+ }
105
+ }
106
+
107
+ /**
108
+ * Finds the best agent for a task based on domain, reputation, and workload.
109
+ *
110
+ * @param {string} projectRoot - Root directory
111
+ * @param {string} taskTitle - Task title for domain matching
112
+ * @param {string} taskPriority - Task priority
113
+ * @returns {{ agent: string, reason: string }}
114
+ */
115
+ function findBestAgent(projectRoot, taskTitle, taskPriority) {
116
+ let agents = [];
117
+
118
+ try {
119
+ const registry = agentRegistry.loadRegistry(projectRoot);
120
+ agents = registry.agents;
121
+ } catch {
122
+ return { agent: 'unassigned', reason: 'No agent registry available' };
123
+ }
124
+
125
+ if (agents.length === 0) {
126
+ return { agent: 'unassigned', reason: 'No agents registered' };
127
+ }
128
+
129
+ // Score each agent
130
+ const scored = agents.map((agent) => {
131
+ let score = 0;
132
+ let reasons = [];
133
+
134
+ // 1. Domain match (keyword overlap between task title and agent domain)
135
+ const titleWords = taskTitle.toLowerCase().split(/\W+/);
136
+ const domainWords = (agent.domain || '').toLowerCase().split(/\W+/);
137
+ const domainOverlap = titleWords.filter((word) =>
138
+ word.length > 2 && domainWords.some((dw) => dw.includes(word) || word.includes(dw))
139
+ ).length;
140
+
141
+ if (domainOverlap > 0) {
142
+ score += domainOverlap * 30;
143
+ reasons.push(`domain match (${domainOverlap} keywords)`);
144
+ }
145
+
146
+ // 2. Reputation score
147
+ try {
148
+ const reputation = agentReputation.getReputation(projectRoot, agent.name);
149
+ score += reputation.score / 10; // Normalize to ~0-100 range
150
+ if (reputation.score > 0) {
151
+ reasons.push(`reputation ${reputation.score}`);
152
+ }
153
+ } catch {
154
+ // No reputation data — neutral
155
+ }
156
+
157
+ // 3. Workload penalty (fewer in-progress tasks = better)
158
+ const workload = getAgentWorkload(projectRoot, agent.name);
159
+ score -= workload * 20;
160
+ if (workload > 0) {
161
+ reasons.push(`workload ${workload} tasks`);
162
+ }
163
+
164
+ return {
165
+ agent: agent.name,
166
+ score,
167
+ reason: reasons.length > 0 ? reasons.join(', ') : 'general availability',
168
+ };
169
+ });
170
+
171
+ // Sort by score descending
172
+ scored.sort((a, b) => b.score - a.score);
173
+
174
+ return {
175
+ agent: scored[0].agent,
176
+ reason: scored[0].reason,
177
+ };
178
+ }
179
+
180
+ /**
181
+ * Generates a sprint plan from open tasks.
182
+ * This is an advisory suggestion — never auto-executed.
183
+ *
184
+ * @param {string} projectRoot - Root directory
185
+ * @param {object} [options] - Sprint options
186
+ * @param {string} [options.name] - Sprint name
187
+ * @param {number} [options.maxTasks] - Max tasks to include
188
+ * @returns {SprintPlan}
189
+ */
190
+ function generateSprintPlan(projectRoot, options = {}) {
191
+ const sprintName = options.name || `Sprint-${new Date().toISOString().slice(0, 10)}`;
192
+ const maxTasks = options.maxTasks || MAX_SPRINT_SIZE;
193
+
194
+ // Get all open/blocked tasks, prioritized
195
+ let tasks = [];
196
+ try {
197
+ const openTasks = taskModel.listTasks(projectRoot, { status: 'open' });
198
+ const blockedTasks = taskModel.listTasks(projectRoot, { status: 'blocked' });
199
+ tasks = [...openTasks, ...blockedTasks];
200
+ } catch {
201
+ tasks = [];
202
+ }
203
+
204
+ // Priority sort using shared utility
205
+ const sortedTasks = taskModel.sortByPriority(tasks);
206
+
207
+ // Take top N
208
+ const sprintTasks = sortedTasks.slice(0, maxTasks);
209
+
210
+ // Auto-assign each
211
+ /** @type {TaskAssignment[]} */
212
+ const assignments = sprintTasks.map((task) => {
213
+ const best = findBestAgent(projectRoot, task.title, task.priority);
214
+ return {
215
+ taskId: task.id,
216
+ taskTitle: task.title,
217
+ suggestedAgent: best.agent,
218
+ reason: best.reason,
219
+ priority: task.priority,
220
+ };
221
+ });
222
+
223
+ /** @type {SprintPlan} */
224
+ const plan = {
225
+ id: `SPR-${crypto.randomUUID().slice(0, 8).toUpperCase()}`,
226
+ name: sprintName,
227
+ createdAt: new Date().toISOString(),
228
+ assignments,
229
+ status: 'draft',
230
+ };
231
+
232
+ // Persist
233
+ const data = loadSprintData(projectRoot);
234
+ data.plans.push(plan);
235
+ writeSprintData(projectRoot, data);
236
+
237
+ return plan;
238
+ }
239
+
240
+ /**
241
+ * Auto-assigns a single task to the best available agent.
242
+ *
243
+ * @param {string} projectRoot - Root directory
244
+ * @param {string} taskId - Task ID
245
+ * @returns {{ success: boolean, agent?: string, reason?: string, error?: string }}
246
+ */
247
+ function autoAssignTask(projectRoot, taskId) {
248
+ const task = taskModel.getTask(projectRoot, taskId);
249
+ if (!task) {
250
+ return { success: false, error: `Task not found: ${taskId}` };
251
+ }
252
+
253
+ const best = findBestAgent(projectRoot, task.title, task.priority);
254
+
255
+ if (best.agent === 'unassigned') {
256
+ return { success: false, error: best.reason };
257
+ }
258
+
259
+ const result = taskModel.updateTask(projectRoot, taskId, { assignee: best.agent });
260
+
261
+ if (result.success) {
262
+ return { success: true, agent: best.agent, reason: best.reason };
263
+ }
264
+
265
+ return { success: false, error: 'Failed to update task' };
266
+ }
267
+
268
+ /**
269
+ * Suggests the next highest-priority unblocked task.
270
+ *
271
+ * @param {string} projectRoot - Root directory
272
+ * @returns {{ task: object | null, reason: string }}
273
+ */
274
+ function suggestNextTask(projectRoot) {
275
+ let openTasks = [];
276
+ try {
277
+ openTasks = taskModel.listTasks(projectRoot, { status: 'open' });
278
+ } catch {
279
+ return { task: null, reason: 'No tasks available' };
280
+ }
281
+
282
+ if (openTasks.length === 0) {
283
+ return { task: null, reason: 'No open tasks remaining' };
284
+ }
285
+
286
+ // Priority sort using shared utility
287
+ const sorted = taskModel.sortByPriority(openTasks);
288
+
289
+ const topTask = sorted[0];
290
+ return {
291
+ task: topTask,
292
+ reason: `Highest priority open task (${topTask.priority})`,
293
+ };
294
+ }
295
+
296
+ /**
297
+ * Returns sprint velocity and progress metrics.
298
+ *
299
+ * @param {string} projectRoot - Root directory
300
+ * @returns {{ totalSprints: number, activeSprint: SprintPlan | null, velocity: number, completionRate: number }}
301
+ */
302
+ function getSprintMetrics(projectRoot) {
303
+ const data = loadSprintData(projectRoot);
304
+ const activeSprint = data.plans.find((p) => p.status === 'active') || null;
305
+ const completedSprints = data.plans.filter((p) => p.status === 'completed');
306
+
307
+ // Velocity: average assignments per completed sprint
308
+ const velocity = completedSprints.length > 0
309
+ ? Math.round(
310
+ completedSprints.reduce((sum, s) => sum + s.assignments.length, 0) / completedSprints.length
311
+ )
312
+ : 0;
313
+
314
+ // Task metrics
315
+ let completionRate = 0;
316
+ try {
317
+ const metrics = taskModel.getTaskMetrics(projectRoot);
318
+ completionRate = metrics.completionRate;
319
+ } catch {
320
+ completionRate = 0;
321
+ }
322
+
323
+ return {
324
+ totalSprints: data.plans.length,
325
+ activeSprint,
326
+ velocity,
327
+ completionRate,
328
+ };
329
+ }
330
+
331
+ module.exports = {
332
+ generateSprintPlan,
333
+ autoAssignTask,
334
+ suggestNextTask,
335
+ getSprintMetrics,
336
+ };
@@ -0,0 +1,370 @@
1
+ /**
2
+ * Devran AI Kit — Error Budget Tracker
3
+ *
4
+ * Enables reliability-config.json error budget tracking with
5
+ * basic metrics collection. Records test, build, and deployment
6
+ * results and calculates budget health.
7
+ *
8
+ * @module lib/error-budget
9
+ * @author Emre Dursun
10
+ * @since v3.0.0
11
+ */
12
+
13
+ 'use strict';
14
+
15
+ const fs = require('fs');
16
+ const path = require('path');
17
+
18
+ const { AGENT_DIR, ENGINE_DIR } = require('./constants');
19
+ const { writeJsonAtomic } = require('./io');
20
+ const { createLogger } = require('./logger');
21
+ const log = createLogger('error-budget');
22
+ const RELIABILITY_CONFIG = 'reliability-config.json';
23
+ const METRICS_FILE = 'metrics.json';
24
+
25
+ /**
26
+ * @typedef {'HEALTHY' | 'WARNING' | 'EXHAUSTED'} BudgetStatus
27
+ */
28
+
29
+ /**
30
+ * @typedef {object} MetricsData
31
+ * @property {number} testsPassed - Total tests passed
32
+ * @property {number} testsFailed - Total tests failed
33
+ * @property {number} buildsSucceeded - Total successful builds
34
+ * @property {number} buildsFailed - Total failed builds
35
+ * @property {number} deploysSucceeded - Total successful deploys
36
+ * @property {number} deploysRolledBack - Total rolled-back deploys
37
+ * @property {string} periodStart - ISO timestamp of period start
38
+ * @property {string} lastUpdated - ISO timestamp of last update
39
+ */
40
+
41
+ /**
42
+ * @typedef {object} BudgetReport
43
+ * @property {BudgetStatus} status - Overall budget health
44
+ * @property {object} rates - Current failure rates
45
+ * @property {number} rates.testFailureRate - Test failure rate (%)
46
+ * @property {number} rates.buildFailureRate - Build failure rate (%)
47
+ * @property {number} rates.deployRollbackRate - Deploy rollback rate (%)
48
+ * @property {object} thresholds - Configured thresholds
49
+ * @property {number} thresholds.testFailureRatePercent - Max test failure rate
50
+ * @property {number} thresholds.buildFailureRatePercent - Max build failure rate
51
+ * @property {number} thresholds.deployRollbackRatePercent - Max deploy rollback rate
52
+ * @property {string[]} violations - Which metrics exceeded thresholds
53
+ */
54
+
55
+ /**
56
+ * Resolves the path to the metrics file.
57
+ *
58
+ * @param {string} projectRoot - Root directory of the project
59
+ * @returns {string} Absolute path to metrics.json
60
+ */
61
+ function resolveMetricsPath(projectRoot) {
62
+ return path.join(projectRoot, AGENT_DIR, ENGINE_DIR, METRICS_FILE);
63
+ }
64
+
65
+ /**
66
+ * Resolves the path to the reliability config.
67
+ *
68
+ * @param {string} projectRoot - Root directory of the project
69
+ * @returns {string} Absolute path to reliability-config.json
70
+ */
71
+ function resolveConfigPath(projectRoot) {
72
+ return path.join(projectRoot, AGENT_DIR, ENGINE_DIR, RELIABILITY_CONFIG);
73
+ }
74
+
75
+ /**
76
+ * Loads the reliability configuration.
77
+ *
78
+ * @param {string} projectRoot - Root directory of the project
79
+ * @returns {object} Parsed reliability config
80
+ */
81
+ function loadConfig(projectRoot) {
82
+ const configPath = resolveConfigPath(projectRoot);
83
+
84
+ if (!fs.existsSync(configPath)) {
85
+ throw new Error(`Reliability config not found: ${configPath}`);
86
+ }
87
+
88
+ return JSON.parse(fs.readFileSync(configPath, 'utf-8'));
89
+ }
90
+
91
+ /**
92
+ * Loads metrics data from disk, or returns defaults if file doesn't exist.
93
+ *
94
+ * @param {string} projectRoot - Root directory of the project
95
+ * @returns {MetricsData}
96
+ */
97
+ function loadMetrics(projectRoot) {
98
+ const metricsPath = resolveMetricsPath(projectRoot);
99
+
100
+ if (!fs.existsSync(metricsPath)) {
101
+ return createEmptyMetrics();
102
+ }
103
+
104
+ try {
105
+ return JSON.parse(fs.readFileSync(metricsPath, 'utf-8'));
106
+ } catch {
107
+ return createEmptyMetrics();
108
+ }
109
+ }
110
+
111
+ /**
112
+ * Creates an empty metrics object.
113
+ *
114
+ * @returns {MetricsData}
115
+ */
116
+ function createEmptyMetrics() {
117
+ return {
118
+ testsPassed: 0,
119
+ testsFailed: 0,
120
+ buildsSucceeded: 0,
121
+ buildsFailed: 0,
122
+ deploysSucceeded: 0,
123
+ deploysRolledBack: 0,
124
+ periodStart: new Date().toISOString(),
125
+ lastUpdated: new Date().toISOString(),
126
+ };
127
+ }
128
+
129
+ /**
130
+ * Writes metrics data to disk atomically.
131
+ *
132
+ * @param {string} projectRoot - Root directory of the project
133
+ * @param {MetricsData} metrics - Metrics data to write
134
+ * @returns {void}
135
+ */
136
+ function writeMetrics(projectRoot, metrics) {
137
+ const metricsPath = resolveMetricsPath(projectRoot);
138
+
139
+ const updatedMetrics = {
140
+ ...metrics,
141
+ lastUpdated: new Date().toISOString(),
142
+ };
143
+
144
+ writeJsonAtomic(metricsPath, updatedMetrics);
145
+ }
146
+
147
+ /**
148
+ * Records the result of a test run.
149
+ *
150
+ * @param {string} projectRoot - Root directory of the project
151
+ * @param {number} passed - Number of tests passed
152
+ * @param {number} failed - Number of tests failed
153
+ * @returns {void}
154
+ */
155
+ function recordTestResult(projectRoot, passed, failed) {
156
+ const metrics = loadMetrics(projectRoot);
157
+ writeMetrics(projectRoot, {
158
+ ...metrics,
159
+ testsPassed: metrics.testsPassed + passed,
160
+ testsFailed: metrics.testsFailed + failed,
161
+ });
162
+ }
163
+
164
+ /**
165
+ * Records the result of a build.
166
+ *
167
+ * @param {string} projectRoot - Root directory of the project
168
+ * @param {boolean} success - Whether the build succeeded
169
+ * @returns {void}
170
+ */
171
+ function recordBuildResult(projectRoot, success) {
172
+ const metrics = loadMetrics(projectRoot);
173
+ writeMetrics(projectRoot, {
174
+ ...metrics,
175
+ buildsSucceeded: metrics.buildsSucceeded + (success ? 1 : 0),
176
+ buildsFailed: metrics.buildsFailed + (success ? 0 : 1),
177
+ });
178
+ }
179
+
180
+ /**
181
+ * Records the result of a deployment.
182
+ *
183
+ * @param {string} projectRoot - Root directory of the project
184
+ * @param {boolean} success - Whether deploy succeeded
185
+ * @param {boolean} [rolledBack=false] - Whether it was rolled back
186
+ * @returns {void}
187
+ */
188
+ function recordDeployResult(projectRoot, success, rolledBack = false) {
189
+ const metrics = loadMetrics(projectRoot);
190
+ writeMetrics(projectRoot, {
191
+ ...metrics,
192
+ deploysSucceeded: metrics.deploysSucceeded + (success && !rolledBack ? 1 : 0),
193
+ deploysRolledBack: metrics.deploysRolledBack + (rolledBack ? 1 : 0),
194
+ });
195
+ }
196
+
197
+ /**
198
+ * Calculates a failure rate percentage safely.
199
+ *
200
+ * @param {number} failed - Number of failures
201
+ * @param {number} total - Total attempts
202
+ * @returns {number} Failure rate as percentage (0-100)
203
+ */
204
+ function calculateRate(failed, total) {
205
+ if (total === 0) {
206
+ return 0;
207
+ }
208
+ return Number(((failed / total) * 100).toFixed(2));
209
+ }
210
+
211
+ /**
212
+ * Generates a budget health report.
213
+ *
214
+ * @param {string} projectRoot - Root directory of the project
215
+ * @returns {BudgetReport}
216
+ */
217
+ function getBudgetReport(projectRoot) {
218
+ const config = loadConfig(projectRoot);
219
+ const metrics = loadMetrics(projectRoot);
220
+ const thresholds = config.errorBudget?.thresholds || {};
221
+
222
+ const testTotal = metrics.testsPassed + metrics.testsFailed;
223
+ const buildTotal = metrics.buildsSucceeded + metrics.buildsFailed;
224
+ const deployTotal = metrics.deploysSucceeded + metrics.deploysRolledBack;
225
+
226
+ const rates = {
227
+ testFailureRate: calculateRate(metrics.testsFailed, testTotal),
228
+ buildFailureRate: calculateRate(metrics.buildsFailed, buildTotal),
229
+ deployRollbackRate: calculateRate(metrics.deploysRolledBack, deployTotal),
230
+ };
231
+
232
+ /** @type {string[]} */
233
+ const violations = [];
234
+
235
+ if (rates.testFailureRate > (thresholds.testFailureRatePercent || 5)) {
236
+ violations.push('testFailureRate');
237
+ }
238
+ if (rates.buildFailureRate > (thresholds.buildFailureRatePercent || 2)) {
239
+ violations.push('buildFailureRate');
240
+ }
241
+ if (rates.deployRollbackRate > (thresholds.deployRollbackRatePercent || 10)) {
242
+ violations.push('deployRollbackRate');
243
+ }
244
+
245
+ /** @type {BudgetStatus} */
246
+ let status = 'HEALTHY';
247
+
248
+ if (violations.length > 0) {
249
+ status = 'EXHAUSTED';
250
+ } else {
251
+ // Warning if any rate is above 80% of threshold
252
+ const testWarning = rates.testFailureRate > (thresholds.testFailureRatePercent || 5) * 0.8;
253
+ const buildWarning = rates.buildFailureRate > (thresholds.buildFailureRatePercent || 2) * 0.8;
254
+ const deployWarning = rates.deployRollbackRate > (thresholds.deployRollbackRatePercent || 10) * 0.8;
255
+
256
+ if (testWarning || buildWarning || deployWarning) {
257
+ status = 'WARNING';
258
+ }
259
+ }
260
+
261
+ return {
262
+ status,
263
+ rates,
264
+ thresholds: {
265
+ testFailureRatePercent: thresholds.testFailureRatePercent || 5,
266
+ buildFailureRatePercent: thresholds.buildFailureRatePercent || 2,
267
+ deployRollbackRatePercent: thresholds.deployRollbackRatePercent || 10,
268
+ },
269
+ violations,
270
+ };
271
+ }
272
+
273
+ /**
274
+ * Archives current metrics before resetting.
275
+ * Preserves historical data for trend analysis.
276
+ *
277
+ * @param {string} projectRoot - Root directory of the project
278
+ * @returns {{ archived: boolean, archivePath: string | null }}
279
+ */
280
+ function archiveMetrics(projectRoot) {
281
+ const metrics = loadMetrics(projectRoot);
282
+ const hasData = metrics.testsPassed + metrics.testsFailed +
283
+ metrics.buildsSucceeded + metrics.buildsFailed +
284
+ metrics.deploysSucceeded + metrics.deploysRolledBack > 0;
285
+
286
+ if (!hasData) {
287
+ return { archived: false, archivePath: null };
288
+ }
289
+
290
+ const archiveDir = path.join(projectRoot, AGENT_DIR, ENGINE_DIR, 'metrics-archive');
291
+ if (!fs.existsSync(archiveDir)) {
292
+ fs.mkdirSync(archiveDir, { recursive: true });
293
+ }
294
+
295
+ const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
296
+ const archivePath = path.join(archiveDir, `metrics-${timestamp}.json`);
297
+
298
+ const report = getBudgetReport(projectRoot);
299
+ const archiveEntry = {
300
+ ...metrics,
301
+ archivedAt: new Date().toISOString(),
302
+ budgetStatus: report.status,
303
+ rates: report.rates,
304
+ };
305
+
306
+ fs.writeFileSync(archivePath, JSON.stringify(archiveEntry, null, 2) + '\n', 'utf-8');
307
+
308
+ return { archived: true, archivePath };
309
+ }
310
+
311
+ /**
312
+ * Resets metrics for a new tracking period.
313
+ * Automatically archives previous period's data before resetting.
314
+ *
315
+ * @param {string} projectRoot - Root directory of the project
316
+ * @param {object} [options] - Reset options
317
+ * @param {boolean} [options.skipArchive=false] - Skip archival
318
+ * @returns {{ archived: boolean, archivePath: string | null }}
319
+ */
320
+ function resetMetrics(projectRoot, options = {}) {
321
+ let archiveResult = { archived: false, archivePath: null };
322
+
323
+ if (!options.skipArchive) {
324
+ archiveResult = archiveMetrics(projectRoot);
325
+ }
326
+
327
+ writeMetrics(projectRoot, createEmptyMetrics());
328
+ return archiveResult;
329
+ }
330
+
331
+ /**
332
+ * Returns historical metrics from the archive.
333
+ *
334
+ * @param {string} projectRoot - Root directory of the project
335
+ * @param {number} [limit=10] - Maximum entries to return
336
+ * @returns {object[]}
337
+ */
338
+ function getMetricsHistory(projectRoot, limit = 10) {
339
+ const archiveDir = path.join(projectRoot, AGENT_DIR, ENGINE_DIR, 'metrics-archive');
340
+
341
+ if (!fs.existsSync(archiveDir)) {
342
+ return [];
343
+ }
344
+
345
+ const files = fs.readdirSync(archiveDir)
346
+ .filter((f) => f.startsWith('metrics-') && f.endsWith('.json'))
347
+ .sort()
348
+ .reverse()
349
+ .slice(0, limit);
350
+
351
+ return files.map((file) => {
352
+ try {
353
+ return JSON.parse(fs.readFileSync(path.join(archiveDir, file), 'utf-8'));
354
+ } catch {
355
+ return null;
356
+ }
357
+ }).filter(Boolean);
358
+ }
359
+
360
+ module.exports = {
361
+ loadConfig,
362
+ loadMetrics,
363
+ recordTestResult,
364
+ recordBuildResult,
365
+ recordDeployResult,
366
+ getBudgetReport,
367
+ resetMetrics,
368
+ archiveMetrics,
369
+ getMetricsHistory,
370
+ };