@baselineos/persona 0.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/.turbo/turbo-build.log +14 -0
- package/.turbo/turbo-test.log +250 -0
- package/LICENSE +17 -0
- package/README.md +19 -0
- package/dist/index.d.ts +530 -0
- package/dist/index.js +2445 -0
- package/package.json +34 -0
- package/src/__tests__/persona-ui.test.ts +336 -0
- package/src/__tests__/smoke.test.ts +16 -0
- package/src/engine.ts +1196 -0
- package/src/index.ts +33 -0
- package/src/personas/agile-pm.ts +399 -0
- package/src/personas/dev-lead.ts +654 -0
- package/src/ui.ts +1042 -0
- package/tsconfig.json +8 -0
|
@@ -0,0 +1,654 @@
|
|
|
1
|
+
import { EventEmitter } from 'events';
|
|
2
|
+
import { randomUUID } from 'crypto';
|
|
3
|
+
import { exec } from 'child_process';
|
|
4
|
+
import { promisify } from 'util';
|
|
5
|
+
import { readFile, writeFile, access } from 'fs/promises';
|
|
6
|
+
import { constants as fsConstants } from 'fs';
|
|
7
|
+
|
|
8
|
+
const execAsync = promisify(exec);
|
|
9
|
+
|
|
10
|
+
// ─── Type Definitions ────────────────────────────────────────────────────────
|
|
11
|
+
|
|
12
|
+
export interface Task {
|
|
13
|
+
id: string;
|
|
14
|
+
title: string;
|
|
15
|
+
description: string;
|
|
16
|
+
type: string;
|
|
17
|
+
priority: string;
|
|
18
|
+
estimatedHours: number;
|
|
19
|
+
assignedAt: string;
|
|
20
|
+
status: string;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface TeamMember {
|
|
24
|
+
id: string;
|
|
25
|
+
name: string;
|
|
26
|
+
role: string;
|
|
27
|
+
skills: string[];
|
|
28
|
+
capacity: number;
|
|
29
|
+
currentTasks: Task[];
|
|
30
|
+
performance: {
|
|
31
|
+
velocity: number;
|
|
32
|
+
quality: number;
|
|
33
|
+
collaboration: number;
|
|
34
|
+
};
|
|
35
|
+
joinedAt: string;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export interface ReviewComment {
|
|
39
|
+
id: string;
|
|
40
|
+
author: string;
|
|
41
|
+
content: string;
|
|
42
|
+
line?: number;
|
|
43
|
+
file?: string;
|
|
44
|
+
type: string;
|
|
45
|
+
createdAt: string;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export interface CodeReview {
|
|
49
|
+
id: string;
|
|
50
|
+
title: string;
|
|
51
|
+
description: string;
|
|
52
|
+
files: string[];
|
|
53
|
+
reviewer: string;
|
|
54
|
+
author: string;
|
|
55
|
+
status: string;
|
|
56
|
+
comments: ReviewComment[];
|
|
57
|
+
createdAt: string;
|
|
58
|
+
approvedAt?: string;
|
|
59
|
+
approver?: string;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export interface TechDebtItem {
|
|
63
|
+
id: string;
|
|
64
|
+
title: string;
|
|
65
|
+
description: string;
|
|
66
|
+
category: string;
|
|
67
|
+
severity: string;
|
|
68
|
+
estimatedEffort: number;
|
|
69
|
+
impact: string;
|
|
70
|
+
file?: string;
|
|
71
|
+
line?: number;
|
|
72
|
+
identifiedAt: string;
|
|
73
|
+
resolvedAt?: string;
|
|
74
|
+
status: string;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export interface CodebaseAnalysis {
|
|
78
|
+
totalFiles: number;
|
|
79
|
+
filesByType: Record<string, number>;
|
|
80
|
+
filesByExtension: Record<string, number>;
|
|
81
|
+
totalLines?: number;
|
|
82
|
+
codeQualityScore: number;
|
|
83
|
+
techDebtItems: number;
|
|
84
|
+
analyzedAt: string;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export interface BuildResult {
|
|
88
|
+
id: string;
|
|
89
|
+
status: string;
|
|
90
|
+
command: string;
|
|
91
|
+
startTime: string;
|
|
92
|
+
endTime?: string;
|
|
93
|
+
duration?: number;
|
|
94
|
+
output: string;
|
|
95
|
+
errors: string[];
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export interface TestResult {
|
|
99
|
+
id: string;
|
|
100
|
+
status: string;
|
|
101
|
+
command: string;
|
|
102
|
+
startTime: string;
|
|
103
|
+
endTime?: string;
|
|
104
|
+
duration?: number;
|
|
105
|
+
passed: number;
|
|
106
|
+
failed: number;
|
|
107
|
+
skipped: number;
|
|
108
|
+
output: string;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export interface WorkspaceData {
|
|
112
|
+
team: TeamMember[];
|
|
113
|
+
reviews: CodeReview[];
|
|
114
|
+
techDebt: TechDebtItem[];
|
|
115
|
+
lastUpdated: string;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export interface DevLeadStatus {
|
|
119
|
+
persona: string;
|
|
120
|
+
teamSize: number;
|
|
121
|
+
activeReviews: number;
|
|
122
|
+
techDebtItems: number;
|
|
123
|
+
openTasks: number;
|
|
124
|
+
teamCapacity: number;
|
|
125
|
+
averageQuality: number;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// ─── Software Development Lead Persona ──────────────────────────────────────
|
|
129
|
+
|
|
130
|
+
export class SoftwareDevelopmentLeadPersona extends EventEmitter {
|
|
131
|
+
private team: Map<string, TeamMember>;
|
|
132
|
+
private reviews: Map<string, CodeReview>;
|
|
133
|
+
private techDebt: TechDebtItem[];
|
|
134
|
+
private builds: BuildResult[];
|
|
135
|
+
private tests: TestResult[];
|
|
136
|
+
private workspacePath: string;
|
|
137
|
+
|
|
138
|
+
constructor(workspacePath: string = './workspace') {
|
|
139
|
+
super();
|
|
140
|
+
this.team = new Map();
|
|
141
|
+
this.reviews = new Map();
|
|
142
|
+
this.techDebt = [];
|
|
143
|
+
this.builds = [];
|
|
144
|
+
this.tests = [];
|
|
145
|
+
this.workspacePath = workspacePath;
|
|
146
|
+
console.log('[SoftwareDevelopmentLeadPersona] Persona initialized');
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// ─── Team Management ──────────────────────────────────────────────────────
|
|
150
|
+
|
|
151
|
+
addTeamMember(name: string, role: string, skills: string[], capacity: number = 40): TeamMember {
|
|
152
|
+
const member: TeamMember = {
|
|
153
|
+
id: randomUUID(),
|
|
154
|
+
name,
|
|
155
|
+
role,
|
|
156
|
+
skills,
|
|
157
|
+
capacity,
|
|
158
|
+
currentTasks: [],
|
|
159
|
+
performance: {
|
|
160
|
+
velocity: 1.0,
|
|
161
|
+
quality: 1.0,
|
|
162
|
+
collaboration: 1.0,
|
|
163
|
+
},
|
|
164
|
+
joinedAt: new Date().toISOString(),
|
|
165
|
+
};
|
|
166
|
+
|
|
167
|
+
this.team.set(member.id, member);
|
|
168
|
+
this.emit('team:memberAdded', { memberId: member.id, name });
|
|
169
|
+
console.log(`[SoftwareDevelopmentLeadPersona] Team member added: ${name} (${role})`);
|
|
170
|
+
return member;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
assignTask(memberId: string, taskData: Omit<Task, 'id' | 'assignedAt' | 'status'>): Task {
|
|
174
|
+
const member = this.team.get(memberId);
|
|
175
|
+
if (!member) {
|
|
176
|
+
throw new Error(`Team member '${memberId}' not found`);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
const currentLoad = member.currentTasks.reduce((sum, t) => sum + t.estimatedHours, 0);
|
|
180
|
+
if (currentLoad + taskData.estimatedHours > member.capacity) {
|
|
181
|
+
console.log(`[SoftwareDevelopmentLeadPersona] Warning: ${member.name} may be over capacity (${currentLoad + taskData.estimatedHours}/${member.capacity}h)`);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
const task: Task = {
|
|
185
|
+
...taskData,
|
|
186
|
+
id: randomUUID(),
|
|
187
|
+
assignedAt: new Date().toISOString(),
|
|
188
|
+
status: 'assigned',
|
|
189
|
+
};
|
|
190
|
+
|
|
191
|
+
member.currentTasks.push(task);
|
|
192
|
+
this.emit('task:assigned', { memberId, taskId: task.id, title: task.title });
|
|
193
|
+
console.log(`[SoftwareDevelopmentLeadPersona] Task assigned to ${member.name}: ${task.title}`);
|
|
194
|
+
return task;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// ─── Code Reviews ────────────────────────────────────────────────────────
|
|
198
|
+
|
|
199
|
+
initiateCodeReview(
|
|
200
|
+
title: string,
|
|
201
|
+
description: string,
|
|
202
|
+
files: string[],
|
|
203
|
+
author: string,
|
|
204
|
+
reviewer: string
|
|
205
|
+
): CodeReview {
|
|
206
|
+
const review: CodeReview = {
|
|
207
|
+
id: randomUUID(),
|
|
208
|
+
title,
|
|
209
|
+
description,
|
|
210
|
+
files,
|
|
211
|
+
reviewer,
|
|
212
|
+
author,
|
|
213
|
+
status: 'pending',
|
|
214
|
+
comments: [],
|
|
215
|
+
createdAt: new Date().toISOString(),
|
|
216
|
+
};
|
|
217
|
+
|
|
218
|
+
this.reviews.set(review.id, review);
|
|
219
|
+
this.emit('review:created', { reviewId: review.id, title });
|
|
220
|
+
console.log(`[SoftwareDevelopmentLeadPersona] Code review initiated: ${title}`);
|
|
221
|
+
return review;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
addReviewComment(
|
|
225
|
+
reviewId: string,
|
|
226
|
+
author: string,
|
|
227
|
+
content: string,
|
|
228
|
+
options: { line?: number; file?: string; type?: string } = {}
|
|
229
|
+
): ReviewComment {
|
|
230
|
+
const review = this.reviews.get(reviewId);
|
|
231
|
+
if (!review) {
|
|
232
|
+
throw new Error(`Code review '${reviewId}' not found`);
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
const comment: ReviewComment = {
|
|
236
|
+
id: randomUUID(),
|
|
237
|
+
author,
|
|
238
|
+
content,
|
|
239
|
+
line: options.line,
|
|
240
|
+
file: options.file,
|
|
241
|
+
type: options.type || 'comment',
|
|
242
|
+
createdAt: new Date().toISOString(),
|
|
243
|
+
};
|
|
244
|
+
|
|
245
|
+
review.comments.push(comment);
|
|
246
|
+
|
|
247
|
+
if (review.status === 'pending') {
|
|
248
|
+
review.status = 'in-review';
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
this.emit('review:commented', { reviewId, commentId: comment.id, author });
|
|
252
|
+
console.log(`[SoftwareDevelopmentLeadPersona] Review comment added by ${author} on ${review.title}`);
|
|
253
|
+
return comment;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
approveCodeReview(reviewId: string, approver: string): CodeReview {
|
|
257
|
+
const review = this.reviews.get(reviewId);
|
|
258
|
+
if (!review) {
|
|
259
|
+
throw new Error(`Code review '${reviewId}' not found`);
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
review.status = 'approved';
|
|
263
|
+
review.approvedAt = new Date().toISOString();
|
|
264
|
+
review.approver = approver;
|
|
265
|
+
|
|
266
|
+
this.emit('review:approved', { reviewId, approver });
|
|
267
|
+
console.log(`[SoftwareDevelopmentLeadPersona] Code review approved: ${review.title} by ${approver}`);
|
|
268
|
+
return review;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
// ─── Technical Debt ───────────────────────────────────────────────────────
|
|
272
|
+
|
|
273
|
+
identifyTechnicalDebt(
|
|
274
|
+
title: string,
|
|
275
|
+
description: string,
|
|
276
|
+
category: string,
|
|
277
|
+
severity: string,
|
|
278
|
+
estimatedEffort: number,
|
|
279
|
+
impact: string,
|
|
280
|
+
options: { file?: string; line?: number } = {}
|
|
281
|
+
): TechDebtItem {
|
|
282
|
+
const item: TechDebtItem = {
|
|
283
|
+
id: randomUUID(),
|
|
284
|
+
title,
|
|
285
|
+
description,
|
|
286
|
+
category,
|
|
287
|
+
severity,
|
|
288
|
+
estimatedEffort,
|
|
289
|
+
impact,
|
|
290
|
+
file: options.file,
|
|
291
|
+
line: options.line,
|
|
292
|
+
identifiedAt: new Date().toISOString(),
|
|
293
|
+
status: 'identified',
|
|
294
|
+
};
|
|
295
|
+
|
|
296
|
+
this.techDebt.push(item);
|
|
297
|
+
this.emit('techDebt:identified', { itemId: item.id, title, severity });
|
|
298
|
+
console.log(`[SoftwareDevelopmentLeadPersona] Tech debt identified: ${title} (${severity})`);
|
|
299
|
+
return item;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
prioritizeTechnicalDebt(criteria: string = 'severity'): TechDebtItem[] {
|
|
303
|
+
const severityOrder: Record<string, number> = {
|
|
304
|
+
critical: 0,
|
|
305
|
+
high: 1,
|
|
306
|
+
medium: 2,
|
|
307
|
+
low: 3,
|
|
308
|
+
};
|
|
309
|
+
|
|
310
|
+
switch (criteria) {
|
|
311
|
+
case 'severity':
|
|
312
|
+
this.techDebt.sort((a, b) => {
|
|
313
|
+
const aOrder = severityOrder[a.severity] ?? 4;
|
|
314
|
+
const bOrder = severityOrder[b.severity] ?? 4;
|
|
315
|
+
return aOrder - bOrder;
|
|
316
|
+
});
|
|
317
|
+
break;
|
|
318
|
+
case 'effort':
|
|
319
|
+
this.techDebt.sort((a, b) => a.estimatedEffort - b.estimatedEffort);
|
|
320
|
+
break;
|
|
321
|
+
case 'impact': {
|
|
322
|
+
const impactOrder: Record<string, number> = { critical: 0, high: 1, medium: 2, low: 3 };
|
|
323
|
+
this.techDebt.sort((a, b) => {
|
|
324
|
+
const aOrder = impactOrder[a.impact] ?? 4;
|
|
325
|
+
const bOrder = impactOrder[b.impact] ?? 4;
|
|
326
|
+
return aOrder - bOrder;
|
|
327
|
+
});
|
|
328
|
+
break;
|
|
329
|
+
}
|
|
330
|
+
case 'category':
|
|
331
|
+
this.techDebt.sort((a, b) => a.category.localeCompare(b.category));
|
|
332
|
+
break;
|
|
333
|
+
default:
|
|
334
|
+
this.techDebt.sort((a, b) => {
|
|
335
|
+
const aOrder = severityOrder[a.severity] ?? 4;
|
|
336
|
+
const bOrder = severityOrder[b.severity] ?? 4;
|
|
337
|
+
return aOrder - bOrder;
|
|
338
|
+
});
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
this.emit('techDebt:prioritized', { criteria, itemCount: this.techDebt.length });
|
|
342
|
+
console.log(`[SoftwareDevelopmentLeadPersona] Tech debt prioritized by ${criteria}: ${this.techDebt.length} items`);
|
|
343
|
+
return [...this.techDebt];
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
// ─── Codebase Analysis ────────────────────────────────────────────────────
|
|
347
|
+
|
|
348
|
+
async analyzeCodebase(directory: string): Promise<CodebaseAnalysis> {
|
|
349
|
+
console.log(`[SoftwareDevelopmentLeadPersona] Analyzing codebase: ${directory}`);
|
|
350
|
+
|
|
351
|
+
const filesByType = await this.countFilesByType(directory);
|
|
352
|
+
const filesByExtension = await this.countFilesByExtension(directory);
|
|
353
|
+
const totalFiles = Object.values(filesByExtension).reduce((sum, count) => sum + count, 0);
|
|
354
|
+
const codeQualityScore = this.calculateCodeQualityScore();
|
|
355
|
+
const techDebtCount = this.techDebt.filter((item) => item.status !== 'resolved').length;
|
|
356
|
+
|
|
357
|
+
const analysis: CodebaseAnalysis = {
|
|
358
|
+
totalFiles,
|
|
359
|
+
filesByType,
|
|
360
|
+
filesByExtension,
|
|
361
|
+
codeQualityScore,
|
|
362
|
+
techDebtItems: techDebtCount,
|
|
363
|
+
analyzedAt: new Date().toISOString(),
|
|
364
|
+
};
|
|
365
|
+
|
|
366
|
+
this.emit('codebase:analyzed', { directory, totalFiles, codeQualityScore });
|
|
367
|
+
console.log(`[SoftwareDevelopmentLeadPersona] Codebase analysis complete: ${totalFiles} files, quality score: ${codeQualityScore}`);
|
|
368
|
+
return analysis;
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
private async countFilesByType(directory: string): Promise<Record<string, number>> {
|
|
372
|
+
const filesByType: Record<string, number> = {
|
|
373
|
+
source: 0,
|
|
374
|
+
test: 0,
|
|
375
|
+
config: 0,
|
|
376
|
+
documentation: 0,
|
|
377
|
+
asset: 0,
|
|
378
|
+
other: 0,
|
|
379
|
+
};
|
|
380
|
+
|
|
381
|
+
try {
|
|
382
|
+
const { stdout } = await execAsync(`find "${directory}" -type f -not -path "*/node_modules/*" -not -path "*/.git/*" 2>/dev/null | head -5000`);
|
|
383
|
+
const files = stdout.trim().split('\n').filter(Boolean);
|
|
384
|
+
|
|
385
|
+
for (const file of files) {
|
|
386
|
+
const lower = file.toLowerCase();
|
|
387
|
+
if (lower.match(/\.(test|spec)\.(ts|js|tsx|jsx)$/)) {
|
|
388
|
+
filesByType['test']++;
|
|
389
|
+
} else if (lower.match(/\.(ts|js|tsx|jsx|py|rb|go|rs|java|c|cpp|h)$/)) {
|
|
390
|
+
filesByType['source']++;
|
|
391
|
+
} else if (lower.match(/\.(json|yaml|yml|toml|ini|env|conf|cfg)$/)) {
|
|
392
|
+
filesByType['config']++;
|
|
393
|
+
} else if (lower.match(/\.(md|txt|rst|doc|docx|pdf)$/)) {
|
|
394
|
+
filesByType['documentation']++;
|
|
395
|
+
} else if (lower.match(/\.(png|jpg|jpeg|gif|svg|ico|mp4|mp3|woff|ttf)$/)) {
|
|
396
|
+
filesByType['asset']++;
|
|
397
|
+
} else {
|
|
398
|
+
filesByType['other']++;
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
} catch (error: unknown) {
|
|
402
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
403
|
+
console.log(`[SoftwareDevelopmentLeadPersona] Error counting files by type: ${message}`);
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
return filesByType;
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
private async countFilesByExtension(directory: string): Promise<Record<string, number>> {
|
|
410
|
+
const filesByExtension: Record<string, number> = {};
|
|
411
|
+
|
|
412
|
+
try {
|
|
413
|
+
const { stdout } = await execAsync(`find "${directory}" -type f -not -path "*/node_modules/*" -not -path "*/.git/*" 2>/dev/null | head -5000`);
|
|
414
|
+
const files = stdout.trim().split('\n').filter(Boolean);
|
|
415
|
+
|
|
416
|
+
for (const file of files) {
|
|
417
|
+
const parts = file.split('.');
|
|
418
|
+
const ext = parts.length > 1 ? `.${parts[parts.length - 1]}` : 'no-extension';
|
|
419
|
+
filesByExtension[ext] = (filesByExtension[ext] || 0) + 1;
|
|
420
|
+
}
|
|
421
|
+
} catch (error: unknown) {
|
|
422
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
423
|
+
console.log(`[SoftwareDevelopmentLeadPersona] Error counting files by extension: ${message}`);
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
return filesByExtension;
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
private async scanCodebase(directory: string, pattern: string): Promise<string[]> {
|
|
430
|
+
try {
|
|
431
|
+
const { stdout } = await execAsync(`grep -rl "${pattern}" "${directory}" --include="*.ts" --include="*.js" 2>/dev/null | head -100`);
|
|
432
|
+
return stdout.trim().split('\n').filter(Boolean);
|
|
433
|
+
} catch {
|
|
434
|
+
return [];
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
private calculateCodeQualityScore(): number {
|
|
439
|
+
let score = 100;
|
|
440
|
+
|
|
441
|
+
const unresolvedDebt = this.techDebt.filter((item) => item.status !== 'resolved');
|
|
442
|
+
|
|
443
|
+
for (const item of unresolvedDebt) {
|
|
444
|
+
switch (item.severity) {
|
|
445
|
+
case 'critical':
|
|
446
|
+
score -= 15;
|
|
447
|
+
break;
|
|
448
|
+
case 'high':
|
|
449
|
+
score -= 10;
|
|
450
|
+
break;
|
|
451
|
+
case 'medium':
|
|
452
|
+
score -= 5;
|
|
453
|
+
break;
|
|
454
|
+
case 'low':
|
|
455
|
+
score -= 2;
|
|
456
|
+
break;
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
const pendingReviews = Array.from(this.reviews.values()).filter(
|
|
461
|
+
(r) => r.status === 'pending' || r.status === 'in-review'
|
|
462
|
+
);
|
|
463
|
+
score -= pendingReviews.length * 3;
|
|
464
|
+
|
|
465
|
+
return Math.max(0, Math.min(100, Math.round(score)));
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
// ─── Build & Test ─────────────────────────────────────────────────────────
|
|
469
|
+
|
|
470
|
+
async runBuild(command: string = 'npm run build'): Promise<BuildResult> {
|
|
471
|
+
const build: BuildResult = {
|
|
472
|
+
id: randomUUID(),
|
|
473
|
+
status: 'running',
|
|
474
|
+
command,
|
|
475
|
+
startTime: new Date().toISOString(),
|
|
476
|
+
output: '',
|
|
477
|
+
errors: [],
|
|
478
|
+
};
|
|
479
|
+
|
|
480
|
+
console.log(`[SoftwareDevelopmentLeadPersona] Running build: ${command}`);
|
|
481
|
+
|
|
482
|
+
try {
|
|
483
|
+
const { stdout, stderr } = await execAsync(command, {
|
|
484
|
+
cwd: this.workspacePath,
|
|
485
|
+
timeout: 300000,
|
|
486
|
+
});
|
|
487
|
+
|
|
488
|
+
build.status = 'success';
|
|
489
|
+
build.output = stdout;
|
|
490
|
+
if (stderr) {
|
|
491
|
+
build.errors = stderr.split('\n').filter(Boolean);
|
|
492
|
+
}
|
|
493
|
+
} catch (error: unknown) {
|
|
494
|
+
build.status = 'failed';
|
|
495
|
+
if (error instanceof Error) {
|
|
496
|
+
build.output = (error as Error & { stdout?: string }).stdout || '';
|
|
497
|
+
build.errors = [(error as Error & { stderr?: string }).stderr || error.message];
|
|
498
|
+
} else {
|
|
499
|
+
build.errors = [String(error)];
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
build.endTime = new Date().toISOString();
|
|
504
|
+
build.duration = new Date(build.endTime).getTime() - new Date(build.startTime).getTime();
|
|
505
|
+
|
|
506
|
+
this.builds.push(build);
|
|
507
|
+
this.emit('build:completed', { buildId: build.id, status: build.status });
|
|
508
|
+
console.log(`[SoftwareDevelopmentLeadPersona] Build ${build.status}: ${command} (${build.duration}ms)`);
|
|
509
|
+
return build;
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
async runTests(command: string = 'npm test'): Promise<TestResult> {
|
|
513
|
+
const testResult: TestResult = {
|
|
514
|
+
id: randomUUID(),
|
|
515
|
+
status: 'running',
|
|
516
|
+
command,
|
|
517
|
+
startTime: new Date().toISOString(),
|
|
518
|
+
passed: 0,
|
|
519
|
+
failed: 0,
|
|
520
|
+
skipped: 0,
|
|
521
|
+
output: '',
|
|
522
|
+
};
|
|
523
|
+
|
|
524
|
+
console.log(`[SoftwareDevelopmentLeadPersona] Running tests: ${command}`);
|
|
525
|
+
|
|
526
|
+
try {
|
|
527
|
+
const { stdout, stderr } = await execAsync(command, {
|
|
528
|
+
cwd: this.workspacePath,
|
|
529
|
+
timeout: 600000,
|
|
530
|
+
});
|
|
531
|
+
|
|
532
|
+
testResult.output = stdout + (stderr ? '\n' + stderr : '');
|
|
533
|
+
testResult.status = 'passed';
|
|
534
|
+
|
|
535
|
+
const passedMatch = stdout.match(/(\d+)\s*(passing|passed|pass)/i);
|
|
536
|
+
const failedMatch = stdout.match(/(\d+)\s*(failing|failed|fail)/i);
|
|
537
|
+
const skippedMatch = stdout.match(/(\d+)\s*(pending|skipped|skip)/i);
|
|
538
|
+
|
|
539
|
+
testResult.passed = passedMatch ? parseInt(passedMatch[1], 10) : 0;
|
|
540
|
+
testResult.failed = failedMatch ? parseInt(failedMatch[1], 10) : 0;
|
|
541
|
+
testResult.skipped = skippedMatch ? parseInt(skippedMatch[1], 10) : 0;
|
|
542
|
+
|
|
543
|
+
if (testResult.failed > 0) {
|
|
544
|
+
testResult.status = 'failed';
|
|
545
|
+
}
|
|
546
|
+
} catch (error: unknown) {
|
|
547
|
+
testResult.status = 'failed';
|
|
548
|
+
if (error instanceof Error) {
|
|
549
|
+
testResult.output = (error as Error & { stdout?: string }).stdout || error.message;
|
|
550
|
+
} else {
|
|
551
|
+
testResult.output = String(error);
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
testResult.endTime = new Date().toISOString();
|
|
556
|
+
testResult.duration = new Date(testResult.endTime).getTime() - new Date(testResult.startTime).getTime();
|
|
557
|
+
|
|
558
|
+
this.tests.push(testResult);
|
|
559
|
+
this.emit('tests:completed', {
|
|
560
|
+
testId: testResult.id,
|
|
561
|
+
status: testResult.status,
|
|
562
|
+
passed: testResult.passed,
|
|
563
|
+
failed: testResult.failed,
|
|
564
|
+
});
|
|
565
|
+
console.log(`[SoftwareDevelopmentLeadPersona] Tests ${testResult.status}: ${testResult.passed} passed, ${testResult.failed} failed, ${testResult.skipped} skipped (${testResult.duration}ms)`);
|
|
566
|
+
return testResult;
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
// ─── File Utilities ───────────────────────────────────────────────────────
|
|
570
|
+
|
|
571
|
+
async fileExists(filePath: string): Promise<boolean> {
|
|
572
|
+
try {
|
|
573
|
+
await access(filePath, fsConstants.F_OK);
|
|
574
|
+
return true;
|
|
575
|
+
} catch {
|
|
576
|
+
return false;
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
// ─── Workspace Persistence ────────────────────────────────────────────────
|
|
581
|
+
|
|
582
|
+
async loadWorkspaceData(): Promise<WorkspaceData | null> {
|
|
583
|
+
try {
|
|
584
|
+
const filePath = `${this.workspacePath}/dev-lead-data.json`;
|
|
585
|
+
const data = await readFile(filePath, 'utf-8');
|
|
586
|
+
const parsed: WorkspaceData = JSON.parse(data);
|
|
587
|
+
|
|
588
|
+
for (const member of parsed.team) {
|
|
589
|
+
this.team.set(member.id, member);
|
|
590
|
+
}
|
|
591
|
+
for (const review of parsed.reviews) {
|
|
592
|
+
this.reviews.set(review.id, review);
|
|
593
|
+
}
|
|
594
|
+
this.techDebt = parsed.techDebt || [];
|
|
595
|
+
|
|
596
|
+
console.log(`[SoftwareDevelopmentLeadPersona] Workspace data loaded: ${this.team.size} members, ${this.reviews.size} reviews, ${this.techDebt.length} debt items`);
|
|
597
|
+
return parsed;
|
|
598
|
+
} catch (error: unknown) {
|
|
599
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
600
|
+
console.log(`[SoftwareDevelopmentLeadPersona] No existing workspace data found: ${message}`);
|
|
601
|
+
return null;
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
async saveWorkspaceData(): Promise<void> {
|
|
606
|
+
try {
|
|
607
|
+
const data: WorkspaceData = {
|
|
608
|
+
team: Array.from(this.team.values()),
|
|
609
|
+
reviews: Array.from(this.reviews.values()),
|
|
610
|
+
techDebt: this.techDebt,
|
|
611
|
+
lastUpdated: new Date().toISOString(),
|
|
612
|
+
};
|
|
613
|
+
|
|
614
|
+
const filePath = `${this.workspacePath}/dev-lead-data.json`;
|
|
615
|
+
await writeFile(filePath, JSON.stringify(data, null, 2), 'utf-8');
|
|
616
|
+
console.log('[SoftwareDevelopmentLeadPersona] Workspace data saved');
|
|
617
|
+
} catch (error: unknown) {
|
|
618
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
619
|
+
console.log(`[SoftwareDevelopmentLeadPersona] Failed to save workspace data: ${message}`);
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
// ─── Status ───────────────────────────────────────────────────────────────
|
|
624
|
+
|
|
625
|
+
getPersonaStatus(): DevLeadStatus {
|
|
626
|
+
const activeReviews = Array.from(this.reviews.values()).filter(
|
|
627
|
+
(r) => r.status === 'pending' || r.status === 'in-review'
|
|
628
|
+
);
|
|
629
|
+
|
|
630
|
+
let openTasks = 0;
|
|
631
|
+
let totalCapacity = 0;
|
|
632
|
+
let totalQuality = 0;
|
|
633
|
+
let memberCount = 0;
|
|
634
|
+
|
|
635
|
+
for (const member of this.team.values()) {
|
|
636
|
+
openTasks += member.currentTasks.filter((t) => t.status !== 'completed' && t.status !== 'cancelled').length;
|
|
637
|
+
totalCapacity += member.capacity - member.currentTasks.reduce((sum, t) => sum + t.estimatedHours, 0);
|
|
638
|
+
totalQuality += member.performance.quality;
|
|
639
|
+
memberCount++;
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
const unresolvedDebt = this.techDebt.filter((item) => item.status !== 'resolved');
|
|
643
|
+
|
|
644
|
+
return {
|
|
645
|
+
persona: 'Software Development Lead',
|
|
646
|
+
teamSize: this.team.size,
|
|
647
|
+
activeReviews: activeReviews.length,
|
|
648
|
+
techDebtItems: unresolvedDebt.length,
|
|
649
|
+
openTasks,
|
|
650
|
+
teamCapacity: Math.max(0, totalCapacity),
|
|
651
|
+
averageQuality: memberCount > 0 ? Math.round((totalQuality / memberCount) * 100) / 100 : 0,
|
|
652
|
+
};
|
|
653
|
+
}
|
|
654
|
+
}
|