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