@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,522 @@
1
+ /**
2
+ * Devran AI Kit — Task Governance Engine
3
+ *
4
+ * Extends task-model.js with locking, assignment enforcement,
5
+ * and audit trail for multi-developer task governance.
6
+ *
7
+ * @module lib/task-governance
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 taskModel = require('./task-model');
17
+
18
+ const { AGENT_DIR, ENGINE_DIR } = require('./constants');
19
+ const { writeJsonAtomic } = require('./io');
20
+ const LOCKS_DIR = 'locks';
21
+ const AUDIT_FILE = 'audit-log.json';
22
+
23
+ /**
24
+ * @typedef {object} TaskLock
25
+ * @property {string} taskId - Locked task ID
26
+ * @property {string} lockedBy - Identity ID of the lock holder
27
+ * @property {string} lockedAt - ISO timestamp
28
+ * @property {string} reason - Reason for locking
29
+ */
30
+
31
+ /**
32
+ * @typedef {object} AuditEntry
33
+ * @property {string} taskId - Task ID
34
+ * @property {string} action - Action performed
35
+ * @property {string} performedBy - Identity ID of the performer
36
+ * @property {string} timestamp - ISO timestamp
37
+ * @property {object} [details] - Additional action details
38
+ */
39
+
40
+ /**
41
+ * Resolves the locks directory path.
42
+ *
43
+ * @param {string} projectRoot - Root directory of the project
44
+ * @returns {string}
45
+ */
46
+ function resolveLocksDir(projectRoot) {
47
+ return path.join(projectRoot, AGENT_DIR, ENGINE_DIR, LOCKS_DIR);
48
+ }
49
+
50
+ /**
51
+ * Resolves the audit log file path.
52
+ *
53
+ * @param {string} projectRoot - Root directory of the project
54
+ * @returns {string}
55
+ */
56
+ function resolveAuditPath(projectRoot) {
57
+ return path.join(projectRoot, AGENT_DIR, ENGINE_DIR, AUDIT_FILE);
58
+ }
59
+
60
+ /**
61
+ * Locks a task for exclusive modification.
62
+ *
63
+ * @param {string} projectRoot - Root directory of the project
64
+ * @param {string} taskId - Task ID to lock
65
+ * @param {string} identityId - Identity ID of the lock requester
66
+ * @param {string} [reason] - Optional reason for locking
67
+ * @returns {{ success: boolean, error?: string }}
68
+ */
69
+ function lockTask(projectRoot, taskId, identityId, reason) {
70
+ // Validate taskId format to prevent path traversal (M-12)
71
+ if (typeof taskId !== 'string' || !/^[a-zA-Z0-9_-]+$/.test(taskId)) {
72
+ return { success: false, error: `Invalid task ID format: ${taskId}` };
73
+ }
74
+
75
+ const task = taskModel.getTask(projectRoot, taskId);
76
+ if (!task) {
77
+ return { success: false, error: `Task not found: ${taskId}` };
78
+ }
79
+
80
+ const locksDir = resolveLocksDir(projectRoot);
81
+ if (!fs.existsSync(locksDir)) {
82
+ fs.mkdirSync(locksDir, { recursive: true });
83
+ }
84
+
85
+ const lockFile = path.join(locksDir, `${taskId}.lock.json`);
86
+
87
+ // Check for existing lock
88
+ if (fs.existsSync(lockFile)) {
89
+ const existingLock = JSON.parse(fs.readFileSync(lockFile, 'utf-8'));
90
+ if (existingLock.lockedBy !== identityId) {
91
+ return {
92
+ success: false,
93
+ error: `Task already locked by ${existingLock.lockedBy} since ${existingLock.lockedAt}`,
94
+ };
95
+ }
96
+ // Same identity re-locking — refresh timestamp
97
+ }
98
+
99
+ /** @type {TaskLock} */
100
+ const lock = {
101
+ taskId,
102
+ lockedBy: identityId,
103
+ lockedAt: new Date().toISOString(),
104
+ reason: reason || 'Working on task',
105
+ };
106
+
107
+ writeJsonAtomic(lockFile, lock);
108
+
109
+ appendAudit(projectRoot, {
110
+ taskId,
111
+ action: 'lock',
112
+ performedBy: identityId,
113
+ timestamp: lock.lockedAt,
114
+ details: { reason: lock.reason },
115
+ });
116
+
117
+ return { success: true };
118
+ }
119
+
120
+ /**
121
+ * Unlocks a task. Only the lock holder or an owner can unlock.
122
+ *
123
+ * @param {string} projectRoot - Root directory of the project
124
+ * @param {string} taskId - Task ID to unlock
125
+ * @param {string} identityId - Identity ID of the unlock requester
126
+ * @returns {{ success: boolean, error?: string }}
127
+ */
128
+ function unlockTask(projectRoot, taskId, identityId) {
129
+ const lockFile = path.join(resolveLocksDir(projectRoot), `${taskId}.lock.json`);
130
+
131
+ if (!fs.existsSync(lockFile)) {
132
+ return { success: false, error: `Task is not locked: ${taskId}` };
133
+ }
134
+
135
+ const lock = JSON.parse(fs.readFileSync(lockFile, 'utf-8'));
136
+
137
+ if (lock.lockedBy !== identityId) {
138
+ // Allow owner override — check identity registry
139
+ try {
140
+ const identity = require('./identity');
141
+ const registry = identity.listIdentities(projectRoot);
142
+ const requester = registry.developers.find((d) => d.id === identityId);
143
+
144
+ if (!requester || requester.role !== 'owner') {
145
+ return { success: false, error: `Only lock holder (${lock.lockedBy}) or owner can unlock` };
146
+ }
147
+ } catch {
148
+ return { success: false, error: `Only lock holder (${lock.lockedBy}) can unlock` };
149
+ }
150
+ }
151
+
152
+ fs.unlinkSync(lockFile);
153
+
154
+ appendAudit(projectRoot, {
155
+ taskId,
156
+ action: 'unlock',
157
+ performedBy: identityId,
158
+ timestamp: new Date().toISOString(),
159
+ });
160
+
161
+ return { success: true };
162
+ }
163
+
164
+ /**
165
+ * Checks if a task is currently locked.
166
+ *
167
+ * @param {string} projectRoot - Root directory of the project
168
+ * @param {string} taskId - Task ID to check
169
+ * @returns {{ locked: boolean, lock?: TaskLock }}
170
+ */
171
+ function isTaskLocked(projectRoot, taskId) {
172
+ const lockFile = path.join(resolveLocksDir(projectRoot), `${taskId}.lock.json`);
173
+
174
+ if (!fs.existsSync(lockFile)) {
175
+ return { locked: false };
176
+ }
177
+
178
+ const lock = JSON.parse(fs.readFileSync(lockFile, 'utf-8'));
179
+ return { locked: true, lock };
180
+ }
181
+
182
+ /**
183
+ * Assigns a task to a specific identity with governance checks.
184
+ *
185
+ * @param {string} projectRoot - Root directory of the project
186
+ * @param {string} taskId - Task ID
187
+ * @param {string} assigneeId - Identity ID to assign to
188
+ * @param {string} performedBy - Identity ID performing the assignment
189
+ * @returns {{ success: boolean, error?: string }}
190
+ */
191
+ function assignTask(projectRoot, taskId, assigneeId, performedBy) {
192
+ const task = taskModel.getTask(projectRoot, taskId);
193
+ if (!task) {
194
+ return { success: false, error: `Task not found: ${taskId}` };
195
+ }
196
+
197
+ // Check lock — only lock holder can reassign
198
+ const lockStatus = isTaskLocked(projectRoot, taskId);
199
+ if (lockStatus.locked && lockStatus.lock.lockedBy !== performedBy) {
200
+ return { success: false, error: `Task is locked by ${lockStatus.lock.lockedBy} — cannot reassign` };
201
+ }
202
+
203
+ const result = taskModel.updateTask(projectRoot, taskId, { assignee: assigneeId });
204
+
205
+ if (result.success) {
206
+ appendAudit(projectRoot, {
207
+ taskId,
208
+ action: 'assign',
209
+ performedBy,
210
+ timestamp: new Date().toISOString(),
211
+ details: { assigneeId },
212
+ });
213
+ }
214
+
215
+ return result;
216
+ }
217
+
218
+ /**
219
+ * Appends an entry to the audit log.
220
+ *
221
+ * @param {string} projectRoot - Root directory of the project
222
+ * @param {AuditEntry} entry - Audit entry to append
223
+ * @returns {void}
224
+ */
225
+ function appendAudit(projectRoot, entry) {
226
+ const auditPath = resolveAuditPath(projectRoot);
227
+ const dir = path.dirname(auditPath);
228
+
229
+ if (!fs.existsSync(dir)) {
230
+ fs.mkdirSync(dir, { recursive: true });
231
+ }
232
+
233
+ let auditLog = { entries: [] };
234
+
235
+ if (fs.existsSync(auditPath)) {
236
+ try {
237
+ auditLog = JSON.parse(fs.readFileSync(auditPath, 'utf-8'));
238
+ } catch {
239
+ auditLog = { entries: [] };
240
+ }
241
+ }
242
+
243
+ auditLog.entries.push(entry);
244
+
245
+ writeJsonAtomic(auditPath, auditLog);
246
+ }
247
+
248
+ /**
249
+ * Gets the audit trail for a specific task or all tasks.
250
+ *
251
+ * @param {string} projectRoot - Root directory of the project
252
+ * @param {string} [taskId] - Optional filter by task ID
253
+ * @returns {AuditEntry[]}
254
+ */
255
+ function getAuditTrail(projectRoot, taskId) {
256
+ const auditPath = resolveAuditPath(projectRoot);
257
+
258
+ if (!fs.existsSync(auditPath)) {
259
+ return [];
260
+ }
261
+
262
+ try {
263
+ const auditLog = JSON.parse(fs.readFileSync(auditPath, 'utf-8'));
264
+ const entries = auditLog.entries || [];
265
+
266
+ if (taskId) {
267
+ return entries.filter((e) => e.taskId === taskId);
268
+ }
269
+
270
+ return entries;
271
+ } catch {
272
+ return [];
273
+ }
274
+ }
275
+
276
+ // ═══════════════════════════════════════════════════════════
277
+ // Decision Timeline Extension (Phase 4 — Deliverable 4.2)
278
+ // ═══════════════════════════════════════════════════════════
279
+
280
+ /** Maximum entries before rotation */
281
+ const MAX_AUDIT_ENTRIES = 500;
282
+
283
+ /**
284
+ * @typedef {object} DecisionEntry
285
+ * @property {string} actor - Name of the actor (agent or developer)
286
+ * @property {'agent' | 'developer'} actorType - Type of actor
287
+ * @property {string} action - Action performed
288
+ * @property {string[]} files - Files affected
289
+ * @property {string} outcome - Result of the decision
290
+ * @property {object} [metadata] - Additional context
291
+ * @property {string} timestamp - ISO timestamp
292
+ */
293
+
294
+ /**
295
+ * Normalizes a legacy audit entry into decision-compatible format.
296
+ * Legacy entries (from Phase 3) may lack actor/actorType/files/outcome fields.
297
+ *
298
+ * @param {object} entry - Raw audit entry
299
+ * @returns {object} Normalized entry with decision fields
300
+ */
301
+ function normalizeEntry(entry) {
302
+ return {
303
+ ...entry,
304
+ actor: entry.actor || entry.performedBy || 'unknown',
305
+ actorType: entry.actorType || 'developer',
306
+ files: entry.files || [],
307
+ outcome: entry.outcome || 'unknown',
308
+ };
309
+ }
310
+
311
+ /**
312
+ * Rotates the audit log when it exceeds MAX_AUDIT_ENTRIES.
313
+ * Archives the current log to `audit-log-{date}.json` and starts fresh.
314
+ *
315
+ * @param {string} projectRoot - Root directory
316
+ * @param {object} auditLog - Current audit log data
317
+ * @returns {object} Potentially trimmed audit log
318
+ */
319
+ function rotateIfNeeded(projectRoot, auditLog) {
320
+ if (!auditLog.entries || auditLog.entries.length < MAX_AUDIT_ENTRIES) {
321
+ return auditLog;
322
+ }
323
+
324
+ const auditPath = resolveAuditPath(projectRoot);
325
+ const dir = path.dirname(auditPath);
326
+ const dateStamp = new Date().toISOString().slice(0, 10);
327
+ const archiveName = `audit-log-${dateStamp}.json`;
328
+ const archivePath = path.join(dir, archiveName);
329
+
330
+ // Write archive atomically
331
+ const archiveTmp = `${archivePath}.tmp`;
332
+ fs.writeFileSync(archiveTmp, JSON.stringify(auditLog, null, 2) + '\n', 'utf-8');
333
+ fs.renameSync(archiveTmp, archivePath);
334
+
335
+ // Return fresh log
336
+ return { entries: [] };
337
+ }
338
+
339
+ /**
340
+ * Records an enriched decision in the audit trail.
341
+ *
342
+ * @param {string} projectRoot - Root directory
343
+ * @param {object} params - Decision parameters
344
+ * @param {string} params.actor - Who made the decision
345
+ * @param {'agent' | 'developer'} [params.actorType] - Actor type (default: 'developer')
346
+ * @param {string} params.action - What was decided
347
+ * @param {string[]} [params.files] - Affected files
348
+ * @param {string} [params.outcome] - Decision outcome
349
+ * @param {object} [params.metadata] - Additional context
350
+ * @returns {DecisionEntry}
351
+ */
352
+ function recordDecision(projectRoot, { actor, actorType, action, files, outcome, metadata }) {
353
+ if (!actor || typeof actor !== 'string') {
354
+ throw new Error('Actor name is required');
355
+ }
356
+ if (!action || typeof action !== 'string') {
357
+ throw new Error('Action is required');
358
+ }
359
+
360
+ const validTypes = ['agent', 'developer'];
361
+ const resolvedType = validTypes.includes(actorType) ? actorType : 'developer';
362
+
363
+ /** @type {DecisionEntry} */
364
+ const entry = {
365
+ actor,
366
+ actorType: resolvedType,
367
+ action,
368
+ files: files || [],
369
+ outcome: outcome || 'pending',
370
+ metadata: metadata || {},
371
+ timestamp: new Date().toISOString(),
372
+ // Also include legacy fields for backward compatibility
373
+ performedBy: actor,
374
+ taskId: (metadata && metadata.taskId) || 'decision',
375
+ };
376
+
377
+ const auditPath = resolveAuditPath(projectRoot);
378
+ const dir = path.dirname(auditPath);
379
+
380
+ if (!fs.existsSync(dir)) {
381
+ fs.mkdirSync(dir, { recursive: true });
382
+ }
383
+
384
+ let auditLog = { entries: [] };
385
+
386
+ if (fs.existsSync(auditPath)) {
387
+ try {
388
+ auditLog = JSON.parse(fs.readFileSync(auditPath, 'utf-8'));
389
+ } catch {
390
+ auditLog = { entries: [] };
391
+ }
392
+ }
393
+
394
+ auditLog.entries.push(entry);
395
+
396
+ // Rotate if needed
397
+ auditLog = rotateIfNeeded(projectRoot, auditLog);
398
+
399
+ writeJsonAtomic(auditPath, auditLog);
400
+
401
+ return entry;
402
+ }
403
+
404
+ /**
405
+ * Returns the decision timeline with optional filters.
406
+ *
407
+ * @param {string} projectRoot - Root directory
408
+ * @param {object} [filters] - Filter options
409
+ * @param {string} [filters.actor] - Filter by actor name
410
+ * @param {string} [filters.actorType] - Filter by actor type
411
+ * @param {string} [filters.action] - Filter by action type
412
+ * @param {string} [filters.since] - ISO date — only entries after this
413
+ * @param {string} [filters.until] - ISO date — only entries before this
414
+ * @returns {object[]} Normalized and filtered entries
415
+ */
416
+ function getTimeline(projectRoot, filters = {}) {
417
+ const entries = getAuditTrail(projectRoot).map(normalizeEntry);
418
+
419
+ let filtered = entries;
420
+
421
+ if (filters.actor) {
422
+ filtered = filtered.filter((e) => e.actor === filters.actor);
423
+ }
424
+ if (filters.actorType) {
425
+ filtered = filtered.filter((e) => e.actorType === filters.actorType);
426
+ }
427
+ if (filters.action) {
428
+ filtered = filtered.filter((e) => e.action === filters.action);
429
+ }
430
+ if (filters.since) {
431
+ const sinceTime = new Date(filters.since).getTime();
432
+ filtered = filtered.filter((e) => new Date(e.timestamp).getTime() >= sinceTime);
433
+ }
434
+ if (filters.until) {
435
+ const untilTime = new Date(filters.until).getTime();
436
+ filtered = filtered.filter((e) => new Date(e.timestamp).getTime() <= untilTime);
437
+ }
438
+
439
+ // Chronological order (oldest first)
440
+ return filtered.sort(
441
+ (a, b) => new Date(a.timestamp).getTime() - new Date(b.timestamp).getTime()
442
+ );
443
+ }
444
+
445
+ /**
446
+ * Returns decisions filtered by a specific actor.
447
+ *
448
+ * @param {string} projectRoot - Root directory
449
+ * @param {string} actorName - Actor name
450
+ * @param {'agent' | 'developer'} [actorType] - Optional actor type filter
451
+ * @returns {object[]} Matching entries
452
+ */
453
+ function getDecisionsByActor(projectRoot, actorName, actorType) {
454
+ const filters = { actor: actorName };
455
+ if (actorType) {
456
+ filters.actorType = actorType;
457
+ }
458
+ return getTimeline(projectRoot, filters);
459
+ }
460
+
461
+ /**
462
+ * Returns a summary of decision activity.
463
+ *
464
+ * @param {string} projectRoot - Root directory
465
+ * @returns {{ totalDecisions: number, actorCounts: object, mostActive: string | null, decisionFrequency: string }}
466
+ */
467
+ function getDecisionSummary(projectRoot) {
468
+ const entries = getAuditTrail(projectRoot).map(normalizeEntry);
469
+
470
+ /** @type {Record<string, number>} */
471
+ const actorCounts = {};
472
+
473
+ for (const entry of entries) {
474
+ const key = `${entry.actor} (${entry.actorType})`;
475
+ actorCounts[key] = (actorCounts[key] || 0) + 1;
476
+ }
477
+
478
+ // Most active
479
+ let mostActive = null;
480
+ let maxCount = 0;
481
+ for (const [actor, count] of Object.entries(actorCounts)) {
482
+ if (count > maxCount) {
483
+ mostActive = actor;
484
+ maxCount = count;
485
+ }
486
+ }
487
+
488
+ // Frequency: decisions per day based on time span
489
+ let decisionFrequency = '0/day';
490
+ if (entries.length >= 2) {
491
+ const sorted = [...entries].sort(
492
+ (a, b) => new Date(a.timestamp).getTime() - new Date(b.timestamp).getTime()
493
+ );
494
+ const spanMs = new Date(sorted[sorted.length - 1].timestamp).getTime() - new Date(sorted[0].timestamp).getTime();
495
+ const spanDays = Math.max(spanMs / (24 * 60 * 60 * 1000), 1);
496
+ const perDay = (entries.length / spanDays).toFixed(1);
497
+ decisionFrequency = `${perDay}/day`;
498
+ } else if (entries.length === 1) {
499
+ decisionFrequency = '1/day';
500
+ }
501
+
502
+ return {
503
+ totalDecisions: entries.length,
504
+ actorCounts,
505
+ mostActive,
506
+ decisionFrequency,
507
+ };
508
+ }
509
+
510
+ module.exports = {
511
+ lockTask,
512
+ unlockTask,
513
+ isTaskLocked,
514
+ assignTask,
515
+ getAuditTrail,
516
+ // Decision timeline (Phase 4)
517
+ recordDecision,
518
+ getTimeline,
519
+ getDecisionsByActor,
520
+ getDecisionSummary,
521
+ };
522
+