@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.
- package/.agent/CheatSheet.md +350 -0
- package/.agent/README.md +76 -0
- package/.agent/agents/README.md +155 -0
- package/.agent/agents/architect.md +185 -0
- package/.agent/agents/backend-specialist.md +276 -0
- package/.agent/agents/build-error-resolver.md +207 -0
- package/.agent/agents/code-reviewer.md +162 -0
- package/.agent/agents/database-architect.md +138 -0
- package/.agent/agents/devops-engineer.md +144 -0
- package/.agent/agents/doc-updater.md +229 -0
- package/.agent/agents/e2e-runner.md +145 -0
- package/.agent/agents/explorer-agent.md +143 -0
- package/.agent/agents/frontend-specialist.md +144 -0
- package/.agent/agents/go-reviewer.md +128 -0
- package/.agent/agents/knowledge-agent.md +197 -0
- package/.agent/agents/mobile-developer.md +150 -0
- package/.agent/agents/performance-optimizer.md +175 -0
- package/.agent/agents/planner.md +133 -0
- package/.agent/agents/pr-reviewer.md +148 -0
- package/.agent/agents/python-reviewer.md +123 -0
- package/.agent/agents/refactor-cleaner.md +201 -0
- package/.agent/agents/reliability-engineer.md +156 -0
- package/.agent/agents/security-reviewer.md +141 -0
- package/.agent/agents/sprint-orchestrator.md +124 -0
- package/.agent/agents/tdd-guide.md +179 -0
- package/.agent/agents/typescript-reviewer.md +110 -0
- package/.agent/checklists/README.md +102 -0
- package/.agent/checklists/pre-commit.md +93 -0
- package/.agent/checklists/session-end.md +99 -0
- package/.agent/checklists/session-start.md +102 -0
- package/.agent/checklists/task-complete.md +81 -0
- package/.agent/commands/README.md +130 -0
- package/.agent/commands/adr.md +29 -0
- package/.agent/commands/ask.md +28 -0
- package/.agent/commands/build.md +30 -0
- package/.agent/commands/changelog.md +40 -0
- package/.agent/commands/checkpoint.md +28 -0
- package/.agent/commands/code-review.md +65 -0
- package/.agent/commands/compact.md +28 -0
- package/.agent/commands/cook.md +30 -0
- package/.agent/commands/db.md +30 -0
- package/.agent/commands/debug.md +31 -0
- package/.agent/commands/deploy.md +37 -0
- package/.agent/commands/design.md +29 -0
- package/.agent/commands/doc.md +30 -0
- package/.agent/commands/eval.md +30 -0
- package/.agent/commands/fix.md +32 -0
- package/.agent/commands/git.md +32 -0
- package/.agent/commands/help.md +273 -0
- package/.agent/commands/implement.md +30 -0
- package/.agent/commands/integrate.md +32 -0
- package/.agent/commands/learn.md +29 -0
- package/.agent/commands/perf.md +31 -0
- package/.agent/commands/plan.md +56 -0
- package/.agent/commands/pr-describe.md +65 -0
- package/.agent/commands/pr-fix.md +45 -0
- package/.agent/commands/pr-merge.md +45 -0
- package/.agent/commands/pr-review.md +50 -0
- package/.agent/commands/pr-split.md +54 -0
- package/.agent/commands/pr-status.md +56 -0
- package/.agent/commands/pr.md +58 -0
- package/.agent/commands/refactor.md +32 -0
- package/.agent/commands/research.md +28 -0
- package/.agent/commands/scout.md +30 -0
- package/.agent/commands/security-scan.md +33 -0
- package/.agent/commands/setup.md +31 -0
- package/.agent/commands/status.md +59 -0
- package/.agent/commands/tdd.md +73 -0
- package/.agent/commands/verify.md +58 -0
- package/.agent/contexts/brainstorm.md +26 -0
- package/.agent/contexts/debug.md +28 -0
- package/.agent/contexts/implement.md +29 -0
- package/.agent/contexts/plan-quality-log.md +30 -0
- package/.agent/contexts/review.md +27 -0
- package/.agent/contexts/ship.md +28 -0
- package/.agent/decisions/001-trust-grade-governance.md +46 -0
- package/.agent/decisions/002-cross-ide-generation.md +15 -0
- package/.agent/engine/identity.json +4 -0
- package/.agent/engine/loading-rules.json +193 -0
- package/.agent/engine/marketplace-index.json +29 -0
- package/.agent/engine/mcp-servers/filesystem.json +9 -0
- package/.agent/engine/mcp-servers/github.json +11 -0
- package/.agent/engine/mcp-servers/postgres.json +11 -0
- package/.agent/engine/mcp-servers/supabase.json +11 -0
- package/.agent/engine/mcp-servers/vercel.json +11 -0
- package/.agent/engine/reliability-config.json +14 -0
- package/.agent/engine/sdlc-map.json +50 -0
- package/.agent/engine/workflow-state.json +167 -0
- package/.agent/hooks/README.md +101 -0
- package/.agent/hooks/hooks.json +104 -0
- package/.agent/hooks/templates/session-end.md +110 -0
- package/.agent/hooks/templates/session-start.md +95 -0
- package/.agent/manifest.json +466 -0
- package/.agent/rules/agent-upgrade-policy.md +56 -0
- package/.agent/rules/architecture.md +111 -0
- package/.agent/rules/coding-style.md +75 -0
- package/.agent/rules/documentation.md +74 -0
- package/.agent/rules/git-workflow.md +140 -0
- package/.agent/rules/quality-gate.md +117 -0
- package/.agent/rules/security.md +67 -0
- package/.agent/rules/sprint-tracking.md +103 -0
- package/.agent/rules/testing.md +80 -0
- package/.agent/rules/workflow-standards.md +30 -0
- package/.agent/rules.md +293 -0
- package/.agent/session-context.md +69 -0
- package/.agent/session-state.json +27 -0
- package/.agent/skills/README.md +135 -0
- package/.agent/skills/api-patterns/SKILL.md +117 -0
- package/.agent/skills/app-builder/SKILL.md +202 -0
- package/.agent/skills/architecture/SKILL.md +101 -0
- package/.agent/skills/behavioral-modes/SKILL.md +295 -0
- package/.agent/skills/brainstorming/SKILL.md +156 -0
- package/.agent/skills/clean-code/SKILL.md +142 -0
- package/.agent/skills/context-budget/SKILL.md +78 -0
- package/.agent/skills/continuous-learning/SKILL.md +145 -0
- package/.agent/skills/database-design/SKILL.md +303 -0
- package/.agent/skills/debugging-strategies/SKILL.md +158 -0
- package/.agent/skills/deployment-procedures/SKILL.md +191 -0
- package/.agent/skills/docker-patterns/SKILL.md +161 -0
- package/.agent/skills/eval-harness/SKILL.md +89 -0
- package/.agent/skills/frontend-patterns/SKILL.md +141 -0
- package/.agent/skills/git-workflow/SKILL.md +159 -0
- package/.agent/skills/i18n-localization/SKILL.md +191 -0
- package/.agent/skills/intelligent-routing/SKILL.md +180 -0
- package/.agent/skills/mcp-integration/SKILL.md +240 -0
- package/.agent/skills/mobile-design/SKILL.md +191 -0
- package/.agent/skills/nodejs-patterns/SKILL.md +164 -0
- package/.agent/skills/parallel-agents/SKILL.md +200 -0
- package/.agent/skills/performance-profiling/SKILL.md +134 -0
- package/.agent/skills/plan-validation/SKILL.md +192 -0
- package/.agent/skills/plan-writing/SKILL.md +183 -0
- package/.agent/skills/plan-writing/domain-enhancers.md +184 -0
- package/.agent/skills/plan-writing/plan-retrospective.md +116 -0
- package/.agent/skills/plan-writing/plan-schema.md +119 -0
- package/.agent/skills/pr-toolkit/SKILL.md +174 -0
- package/.agent/skills/production-readiness/SKILL.md +126 -0
- package/.agent/skills/security-practices/SKILL.md +109 -0
- package/.agent/skills/shell-conventions/SKILL.md +92 -0
- package/.agent/skills/strategic-compact/SKILL.md +62 -0
- package/.agent/skills/testing-patterns/SKILL.md +141 -0
- package/.agent/skills/typescript-expert/SKILL.md +160 -0
- package/.agent/skills/ui-ux-pro-max/SKILL.md +137 -0
- package/.agent/skills/ui-ux-pro-max/data/charts.csv +26 -0
- package/.agent/skills/ui-ux-pro-max/data/colors.csv +97 -0
- package/.agent/skills/ui-ux-pro-max/data/icons.csv +101 -0
- package/.agent/skills/ui-ux-pro-max/data/landing.csv +31 -0
- package/.agent/skills/ui-ux-pro-max/data/products.csv +97 -0
- package/.agent/skills/ui-ux-pro-max/data/react-performance.csv +45 -0
- package/.agent/skills/ui-ux-pro-max/data/stacks/astro.csv +54 -0
- package/.agent/skills/ui-ux-pro-max/data/stacks/flutter.csv +53 -0
- package/.agent/skills/ui-ux-pro-max/data/stacks/html-tailwind.csv +56 -0
- package/.agent/skills/ui-ux-pro-max/data/stacks/jetpack-compose.csv +53 -0
- package/.agent/skills/ui-ux-pro-max/data/stacks/nextjs.csv +53 -0
- package/.agent/skills/ui-ux-pro-max/data/stacks/nuxt-ui.csv +51 -0
- package/.agent/skills/ui-ux-pro-max/data/stacks/nuxtjs.csv +59 -0
- package/.agent/skills/ui-ux-pro-max/data/stacks/react-native.csv +52 -0
- package/.agent/skills/ui-ux-pro-max/data/stacks/react.csv +54 -0
- package/.agent/skills/ui-ux-pro-max/data/stacks/shadcn.csv +61 -0
- package/.agent/skills/ui-ux-pro-max/data/stacks/svelte.csv +54 -0
- package/.agent/skills/ui-ux-pro-max/data/stacks/swiftui.csv +51 -0
- package/.agent/skills/ui-ux-pro-max/data/stacks/vue.csv +50 -0
- package/.agent/skills/ui-ux-pro-max/data/styles.csv +68 -0
- package/.agent/skills/ui-ux-pro-max/data/typography.csv +58 -0
- package/.agent/skills/ui-ux-pro-max/data/ui-reasoning.csv +101 -0
- package/.agent/skills/ui-ux-pro-max/data/ux-guidelines.csv +100 -0
- package/.agent/skills/ui-ux-pro-max/data/web-interface.csv +31 -0
- package/.agent/skills/ui-ux-pro-max/scripts/core.py +253 -0
- package/.agent/skills/ui-ux-pro-max/scripts/design_system.py +1067 -0
- package/.agent/skills/ui-ux-pro-max/scripts/search.py +114 -0
- package/.agent/skills/verification-loop/SKILL.md +89 -0
- package/.agent/skills/webapp-testing/SKILL.md +175 -0
- package/.agent/templates/adr-template.md +32 -0
- package/.agent/templates/bug-report.md +37 -0
- package/.agent/templates/feature-request.md +32 -0
- package/.agent/workflows/README.md +101 -0
- package/.agent/workflows/brainstorm.md +86 -0
- package/.agent/workflows/create.md +85 -0
- package/.agent/workflows/debug.md +83 -0
- package/.agent/workflows/deploy.md +114 -0
- package/.agent/workflows/enhance.md +85 -0
- package/.agent/workflows/orchestrate.md +106 -0
- package/.agent/workflows/plan.md +105 -0
- package/.agent/workflows/pr-fix.md +163 -0
- package/.agent/workflows/pr-merge.md +117 -0
- package/.agent/workflows/pr-review.md +178 -0
- package/.agent/workflows/pr-split.md +118 -0
- package/.agent/workflows/pr.md +184 -0
- package/.agent/workflows/preflight.md +107 -0
- package/.agent/workflows/preview.md +95 -0
- package/.agent/workflows/quality-gate.md +103 -0
- package/.agent/workflows/retrospective.md +100 -0
- package/.agent/workflows/review.md +104 -0
- package/.agent/workflows/status.md +89 -0
- package/.agent/workflows/test.md +98 -0
- package/.agent/workflows/ui-ux-pro-max.md +93 -0
- package/.agent/workflows/upgrade.md +97 -0
- package/LICENSE +21 -0
- package/README.md +218 -0
- package/bin/kit.js +773 -0
- package/lib/agent-registry.js +228 -0
- package/lib/agent-reputation.js +343 -0
- package/lib/circuit-breaker.js +195 -0
- package/lib/cli-commands.js +322 -0
- package/lib/config-validator.js +274 -0
- package/lib/conflict-detector.js +252 -0
- package/lib/constants.js +47 -0
- package/lib/engineering-manager.js +336 -0
- package/lib/error-budget.js +370 -0
- package/lib/hook-system.js +256 -0
- package/lib/ide-generator.js +434 -0
- package/lib/identity.js +240 -0
- package/lib/io.js +146 -0
- package/lib/learning-engine.js +163 -0
- package/lib/loading-engine.js +421 -0
- package/lib/logger.js +118 -0
- package/lib/marketplace.js +321 -0
- package/lib/plugin-system.js +604 -0
- package/lib/plugin-verifier.js +197 -0
- package/lib/rate-limiter.js +113 -0
- package/lib/security-scanner.js +312 -0
- package/lib/self-healing.js +468 -0
- package/lib/session-manager.js +264 -0
- package/lib/skill-sandbox.js +244 -0
- package/lib/task-governance.js +522 -0
- package/lib/task-model.js +332 -0
- package/lib/updater.js +240 -0
- package/lib/verify.js +279 -0
- package/lib/workflow-engine.js +373 -0
- package/lib/workflow-events.js +166 -0
- package/lib/workflow-persistence.js +160 -0
- 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
|
+
|