@trentapps/manager-protocol 1.1.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 (151) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +446 -0
  3. package/dist/analyzers/ArchitectureDetector.d.ts +44 -0
  4. package/dist/analyzers/ArchitectureDetector.d.ts.map +1 -0
  5. package/dist/analyzers/ArchitectureDetector.js +218 -0
  6. package/dist/analyzers/ArchitectureDetector.js.map +1 -0
  7. package/dist/analyzers/CSSAnalyzer.d.ts +104 -0
  8. package/dist/analyzers/CSSAnalyzer.d.ts.map +1 -0
  9. package/dist/analyzers/CSSAnalyzer.js +578 -0
  10. package/dist/analyzers/CSSAnalyzer.js.map +1 -0
  11. package/dist/analyzers/index.d.ts +5 -0
  12. package/dist/analyzers/index.d.ts.map +1 -0
  13. package/dist/analyzers/index.js +5 -0
  14. package/dist/analyzers/index.js.map +1 -0
  15. package/dist/cli.d.ts +8 -0
  16. package/dist/cli.d.ts.map +1 -0
  17. package/dist/cli.js +174 -0
  18. package/dist/cli.js.map +1 -0
  19. package/dist/design-system/index.d.ts +6 -0
  20. package/dist/design-system/index.d.ts.map +1 -0
  21. package/dist/design-system/index.js +6 -0
  22. package/dist/design-system/index.js.map +1 -0
  23. package/dist/design-system/tokens.d.ts +106 -0
  24. package/dist/design-system/tokens.d.ts.map +1 -0
  25. package/dist/design-system/tokens.js +554 -0
  26. package/dist/design-system/tokens.js.map +1 -0
  27. package/dist/engine/AppMonitor.d.ts +162 -0
  28. package/dist/engine/AppMonitor.d.ts.map +1 -0
  29. package/dist/engine/AppMonitor.js +754 -0
  30. package/dist/engine/AppMonitor.js.map +1 -0
  31. package/dist/engine/AuditLogger.d.ts +138 -0
  32. package/dist/engine/AuditLogger.d.ts.map +1 -0
  33. package/dist/engine/AuditLogger.js +448 -0
  34. package/dist/engine/AuditLogger.js.map +1 -0
  35. package/dist/engine/GitHubApprovalManager.d.ts +106 -0
  36. package/dist/engine/GitHubApprovalManager.d.ts.map +1 -0
  37. package/dist/engine/GitHubApprovalManager.js +315 -0
  38. package/dist/engine/GitHubApprovalManager.js.map +1 -0
  39. package/dist/engine/RateLimiter.d.ts +79 -0
  40. package/dist/engine/RateLimiter.d.ts.map +1 -0
  41. package/dist/engine/RateLimiter.js +232 -0
  42. package/dist/engine/RateLimiter.js.map +1 -0
  43. package/dist/engine/RulesEngine.d.ts +77 -0
  44. package/dist/engine/RulesEngine.d.ts.map +1 -0
  45. package/dist/engine/RulesEngine.js +400 -0
  46. package/dist/engine/RulesEngine.js.map +1 -0
  47. package/dist/engine/TaskManager.d.ts +173 -0
  48. package/dist/engine/TaskManager.d.ts.map +1 -0
  49. package/dist/engine/TaskManager.js +678 -0
  50. package/dist/engine/TaskManager.js.map +1 -0
  51. package/dist/engine/index.d.ts +9 -0
  52. package/dist/engine/index.d.ts.map +1 -0
  53. package/dist/engine/index.js +9 -0
  54. package/dist/engine/index.js.map +1 -0
  55. package/dist/index.d.ts +21 -0
  56. package/dist/index.d.ts.map +1 -0
  57. package/dist/index.js +29 -0
  58. package/dist/index.js.map +1 -0
  59. package/dist/rules/architecture.d.ts +9 -0
  60. package/dist/rules/architecture.d.ts.map +1 -0
  61. package/dist/rules/architecture.js +322 -0
  62. package/dist/rules/architecture.js.map +1 -0
  63. package/dist/rules/azure.d.ts +7 -0
  64. package/dist/rules/azure.d.ts.map +1 -0
  65. package/dist/rules/azure.js +138 -0
  66. package/dist/rules/azure.js.map +1 -0
  67. package/dist/rules/compliance.d.ts +9 -0
  68. package/dist/rules/compliance.d.ts.map +1 -0
  69. package/dist/rules/compliance.js +304 -0
  70. package/dist/rules/compliance.js.map +1 -0
  71. package/dist/rules/css.d.ts +10 -0
  72. package/dist/rules/css.d.ts.map +1 -0
  73. package/dist/rules/css.js +1239 -0
  74. package/dist/rules/css.js.map +1 -0
  75. package/dist/rules/flask.d.ts +7 -0
  76. package/dist/rules/flask.d.ts.map +1 -0
  77. package/dist/rules/flask.js +155 -0
  78. package/dist/rules/flask.js.map +1 -0
  79. package/dist/rules/index.d.ts +607 -0
  80. package/dist/rules/index.d.ts.map +1 -0
  81. package/dist/rules/index.js +401 -0
  82. package/dist/rules/index.js.map +1 -0
  83. package/dist/rules/ml-ai.d.ts +7 -0
  84. package/dist/rules/ml-ai.d.ts.map +1 -0
  85. package/dist/rules/ml-ai.js +150 -0
  86. package/dist/rules/ml-ai.js.map +1 -0
  87. package/dist/rules/operational.d.ts +9 -0
  88. package/dist/rules/operational.d.ts.map +1 -0
  89. package/dist/rules/operational.js +318 -0
  90. package/dist/rules/operational.js.map +1 -0
  91. package/dist/rules/security.d.ts +9 -0
  92. package/dist/rules/security.d.ts.map +1 -0
  93. package/dist/rules/security.js +287 -0
  94. package/dist/rules/security.js.map +1 -0
  95. package/dist/rules/storage.d.ts +7 -0
  96. package/dist/rules/storage.d.ts.map +1 -0
  97. package/dist/rules/storage.js +134 -0
  98. package/dist/rules/storage.js.map +1 -0
  99. package/dist/rules/stripe.d.ts +7 -0
  100. package/dist/rules/stripe.d.ts.map +1 -0
  101. package/dist/rules/stripe.js +140 -0
  102. package/dist/rules/stripe.js.map +1 -0
  103. package/dist/rules/testing.d.ts +7 -0
  104. package/dist/rules/testing.d.ts.map +1 -0
  105. package/dist/rules/testing.js +135 -0
  106. package/dist/rules/testing.js.map +1 -0
  107. package/dist/rules/ux.d.ts +9 -0
  108. package/dist/rules/ux.d.ts.map +1 -0
  109. package/dist/rules/ux.js +280 -0
  110. package/dist/rules/ux.js.map +1 -0
  111. package/dist/rules/websocket.d.ts +7 -0
  112. package/dist/rules/websocket.d.ts.map +1 -0
  113. package/dist/rules/websocket.js +136 -0
  114. package/dist/rules/websocket.js.map +1 -0
  115. package/dist/server.d.ts +49 -0
  116. package/dist/server.d.ts.map +1 -0
  117. package/dist/server.js +2330 -0
  118. package/dist/server.js.map +1 -0
  119. package/dist/supervisor/AgentSupervisor.d.ts +235 -0
  120. package/dist/supervisor/AgentSupervisor.d.ts.map +1 -0
  121. package/dist/supervisor/AgentSupervisor.js +596 -0
  122. package/dist/supervisor/AgentSupervisor.js.map +1 -0
  123. package/dist/supervisor/ManagedServerRegistry.d.ts +48 -0
  124. package/dist/supervisor/ManagedServerRegistry.d.ts.map +1 -0
  125. package/dist/supervisor/ManagedServerRegistry.js +145 -0
  126. package/dist/supervisor/ManagedServerRegistry.js.map +1 -0
  127. package/dist/supervisor/ProjectTracker.d.ts +188 -0
  128. package/dist/supervisor/ProjectTracker.d.ts.map +1 -0
  129. package/dist/supervisor/ProjectTracker.js +617 -0
  130. package/dist/supervisor/ProjectTracker.js.map +1 -0
  131. package/dist/supervisor/index.d.ts +6 -0
  132. package/dist/supervisor/index.d.ts.map +1 -0
  133. package/dist/supervisor/index.js +6 -0
  134. package/dist/supervisor/index.js.map +1 -0
  135. package/dist/types/index.d.ts +1176 -0
  136. package/dist/types/index.d.ts.map +1 -0
  137. package/dist/types/index.js +391 -0
  138. package/dist/types/index.js.map +1 -0
  139. package/dist/utils/errors.d.ts +86 -0
  140. package/dist/utils/errors.d.ts.map +1 -0
  141. package/dist/utils/errors.js +171 -0
  142. package/dist/utils/errors.js.map +1 -0
  143. package/dist/utils/index.d.ts +5 -0
  144. package/dist/utils/index.d.ts.map +1 -0
  145. package/dist/utils/index.js +5 -0
  146. package/dist/utils/index.js.map +1 -0
  147. package/dist/utils/shell.d.ts +22 -0
  148. package/dist/utils/shell.d.ts.map +1 -0
  149. package/dist/utils/shell.js +29 -0
  150. package/dist/utils/shell.js.map +1 -0
  151. package/package.json +63 -0
@@ -0,0 +1,678 @@
1
+ /**
2
+ * Enterprise Agent Supervisor - Task Manager
3
+ *
4
+ * Manages project tasks using GitHub Issues via the `gh` CLI.
5
+ * Tasks are stored as GitHub Issues, providing persistence and visibility.
6
+ *
7
+ * Features:
8
+ * - Auto-detects repo from current directory if not specified
9
+ * - Creates priority/status labels automatically
10
+ * - Caches repo detection for performance
11
+ * - Full GitHub Issues integration
12
+ */
13
+ import { exec } from 'child_process';
14
+ import { promisify } from 'util';
15
+ import { auditLogger } from './AuditLogger.js';
16
+ import { escapeForShell } from '../utils/shell.js';
17
+ const execAsync = promisify(exec);
18
+ export class TaskManager {
19
+ priorityPrefix;
20
+ statusPrefix;
21
+ cachedRepo = null;
22
+ initializedLabels = new Set();
23
+ ghVerified = false;
24
+ constructor(options = {}) {
25
+ this.priorityPrefix = options.priorityLabelPrefix || 'priority:';
26
+ this.statusPrefix = options.statusLabelPrefix || 'status:';
27
+ }
28
+ /**
29
+ * Verify gh CLI is installed and authenticated
30
+ */
31
+ async verifyGh() {
32
+ if (this.ghVerified)
33
+ return { ok: true };
34
+ try {
35
+ const { stdout } = await execAsync('gh auth status --json user 2>&1 || gh auth status');
36
+ this.ghVerified = true;
37
+ // Try to extract user
38
+ try {
39
+ const status = JSON.parse(stdout);
40
+ return { ok: true, user: status.user };
41
+ }
42
+ catch {
43
+ return { ok: true };
44
+ }
45
+ }
46
+ catch (error) {
47
+ return {
48
+ ok: false,
49
+ error: 'gh CLI not authenticated. Run: gh auth login'
50
+ };
51
+ }
52
+ }
53
+ /**
54
+ * Get the current repo from git remote or gh CLI
55
+ */
56
+ async getCurrentRepo() {
57
+ if (this.cachedRepo)
58
+ return this.cachedRepo;
59
+ try {
60
+ // Try to get repo from current directory
61
+ const { stdout } = await execAsync('gh repo view --json nameWithOwner -q .nameWithOwner 2>/dev/null');
62
+ this.cachedRepo = stdout.trim();
63
+ return this.cachedRepo;
64
+ }
65
+ catch {
66
+ return null;
67
+ }
68
+ }
69
+ /**
70
+ * Resolve project name - use provided or auto-detect
71
+ */
72
+ async resolveRepo(projectName) {
73
+ if (projectName)
74
+ return projectName;
75
+ const currentRepo = await this.getCurrentRepo();
76
+ if (currentRepo)
77
+ return currentRepo;
78
+ throw new Error('No repository specified and could not auto-detect from current directory. ' +
79
+ 'Either provide projectName or run from within a git repository.');
80
+ }
81
+ /**
82
+ * Execute a gh command and return parsed JSON output
83
+ */
84
+ async execGh(command) {
85
+ try {
86
+ const { stdout } = await execAsync(`gh ${command}`, {
87
+ maxBuffer: 10 * 1024 * 1024
88
+ });
89
+ return JSON.parse(stdout || '[]');
90
+ }
91
+ catch (error) {
92
+ if (error.stdout === '' || error.stdout === '[]') {
93
+ return [];
94
+ }
95
+ // Parse error message for better feedback
96
+ const errMsg = error.stderr || error.message || 'Unknown error';
97
+ if (errMsg.includes('Could not resolve to a Repository')) {
98
+ throw new Error(`Repository not found. Check the repo name format (owner/repo).`);
99
+ }
100
+ if (errMsg.includes('HTTP 404')) {
101
+ throw new Error(`Not found - check repository access permissions.`);
102
+ }
103
+ if (errMsg.includes('HTTP 401') || errMsg.includes('authentication')) {
104
+ throw new Error(`Authentication failed. Run: gh auth login`);
105
+ }
106
+ throw new Error(`gh command failed: ${errMsg}`);
107
+ }
108
+ }
109
+ /**
110
+ * Execute a gh command without JSON output
111
+ */
112
+ async execGhRaw(command) {
113
+ try {
114
+ const { stdout } = await execAsync(`gh ${command}`);
115
+ return stdout.trim();
116
+ }
117
+ catch (error) {
118
+ const errMsg = error.stderr || error.message || 'Unknown error';
119
+ throw new Error(`gh command failed: ${errMsg}`);
120
+ }
121
+ }
122
+ /**
123
+ * Ensure a label exists in the repo, create if not
124
+ */
125
+ async ensureLabel(repo, labelName) {
126
+ const cacheKey = `${repo}:${labelName}`;
127
+ if (this.initializedLabels.has(cacheKey))
128
+ return;
129
+ // Define colors for our labels
130
+ const labelColors = {
131
+ [`${this.priorityPrefix}critical`]: 'B60205',
132
+ [`${this.priorityPrefix}high`]: 'D93F0B',
133
+ [`${this.priorityPrefix}medium`]: 'FBCA04',
134
+ [`${this.priorityPrefix}low`]: '0E8A16',
135
+ [`${this.statusPrefix}in_progress`]: '1D76DB',
136
+ [`${this.statusPrefix}blocked`]: 'E99695',
137
+ [`${this.statusPrefix}cancelled`]: '808080',
138
+ 'needs-approval': 'FF6B6B',
139
+ };
140
+ try {
141
+ // Check if label exists
142
+ await this.execGh(`label view "${labelName}" --repo "${repo}" --json name`);
143
+ this.initializedLabels.add(cacheKey);
144
+ }
145
+ catch {
146
+ // Label doesn't exist, create it
147
+ try {
148
+ const color = labelColors[labelName] || '666666';
149
+ const description = labelName.startsWith(this.priorityPrefix)
150
+ ? `Priority: ${labelName.replace(this.priorityPrefix, '')}`
151
+ : labelName.startsWith(this.statusPrefix)
152
+ ? `Status: ${labelName.replace(this.statusPrefix, '')}`
153
+ : labelName === 'needs-approval'
154
+ ? 'Significant change requiring approval before implementation'
155
+ : '';
156
+ await this.execGhRaw(`label create "${labelName}" --repo "${repo}" --color "${color}" --description "${description}" --force`);
157
+ this.initializedLabels.add(cacheKey);
158
+ }
159
+ catch {
160
+ // Label creation failed, might be permissions - continue anyway
161
+ this.initializedLabels.add(cacheKey);
162
+ }
163
+ }
164
+ }
165
+ /**
166
+ * Convert GitHub Issue to ProjectTask
167
+ */
168
+ issueToTask(issue, projectName) {
169
+ const labels = issue.labels.map(l => l.name);
170
+ // Extract priority from labels
171
+ const priorityLabel = labels.find(l => l.startsWith(this.priorityPrefix));
172
+ const priority = (priorityLabel?.replace(this.priorityPrefix, '') || 'medium');
173
+ // Extract status from labels or state
174
+ let status = issue.state === 'OPEN' ? 'pending' : 'completed';
175
+ const statusLabel = labels.find(l => l.startsWith(this.statusPrefix));
176
+ if (statusLabel) {
177
+ const labelStatus = statusLabel.replace(this.statusPrefix, '');
178
+ if (['pending', 'in_progress', 'completed', 'blocked', 'cancelled'].includes(labelStatus)) {
179
+ status = labelStatus;
180
+ }
181
+ }
182
+ else if (issue.state === 'OPEN') {
183
+ if (labels.includes('in-progress') || labels.includes('wip')) {
184
+ status = 'in_progress';
185
+ }
186
+ }
187
+ // Filter out priority and status labels
188
+ const cleanLabels = labels.filter(l => !l.startsWith(this.priorityPrefix) &&
189
+ !l.startsWith(this.statusPrefix) &&
190
+ l !== 'in-progress' &&
191
+ l !== 'wip');
192
+ return {
193
+ id: String(issue.number),
194
+ projectName,
195
+ title: issue.title,
196
+ description: issue.body || undefined,
197
+ status,
198
+ priority,
199
+ assignee: issue.assignees[0]?.login,
200
+ labels: cleanLabels.length > 0 ? cleanLabels : undefined,
201
+ dueDate: issue.milestone?.title,
202
+ createdAt: issue.createdAt,
203
+ updatedAt: issue.updatedAt,
204
+ completedAt: issue.closedAt || undefined,
205
+ metadata: { url: issue.url }
206
+ };
207
+ }
208
+ /**
209
+ * Build labels array for gh command
210
+ */
211
+ buildLabels(priority, status, labels) {
212
+ const allLabels = [];
213
+ if (priority) {
214
+ allLabels.push(`${this.priorityPrefix}${priority}`);
215
+ }
216
+ if (status && status !== 'pending' && status !== 'completed') {
217
+ allLabels.push(`${this.statusPrefix}${status}`);
218
+ }
219
+ if (labels) {
220
+ allLabels.push(...labels);
221
+ }
222
+ return allLabels;
223
+ }
224
+ /**
225
+ * Create a new task (GitHub Issue) for a project
226
+ *
227
+ * @param params.projectName - Optional repo in "owner/repo" format. Auto-detects if not provided.
228
+ * @param params.needsApproval - Flag for significant changes requiring approval before implementation
229
+ */
230
+ async createTask(params) {
231
+ const repo = await this.resolveRepo(params.projectName);
232
+ const allLabels = this.buildLabels(params.priority, 'pending', params.labels);
233
+ // Add needs-approval label if flagged
234
+ if (params.needsApproval) {
235
+ allLabels.push('needs-approval');
236
+ }
237
+ // Ensure labels exist
238
+ for (const label of allLabels) {
239
+ if (label.startsWith(this.priorityPrefix) || label.startsWith(this.statusPrefix) || label === 'needs-approval') {
240
+ await this.ensureLabel(repo, label);
241
+ }
242
+ }
243
+ // Build the gh issue create command
244
+ let cmd = `issue create --repo "${repo}" --title ${escapeForShell(params.title)}`;
245
+ if (params.description) {
246
+ cmd += ` --body ${escapeForShell(params.description)}`;
247
+ }
248
+ if (allLabels.length > 0) {
249
+ cmd += ` --label "${allLabels.join(',')}"`;
250
+ }
251
+ if (params.assignee) {
252
+ // Handle @me specially
253
+ const assignee = params.assignee === '@me' ? '@me' : params.assignee;
254
+ cmd += ` --assignee "${assignee}"`;
255
+ }
256
+ // Create the issue - gh issue create returns the URL, not JSON
257
+ const issueUrl = await this.execGhRaw(cmd);
258
+ // Extract issue number from URL (e.g., https://github.com/owner/repo/issues/123)
259
+ const issueNumberMatch = issueUrl.match(/\/issues\/(\d+)/);
260
+ if (!issueNumberMatch) {
261
+ throw new Error(`Failed to parse issue number from: ${issueUrl}`);
262
+ }
263
+ const issueNumber = issueNumberMatch[1];
264
+ // Fetch the full issue details
265
+ const result = await this.execGh(`issue view ${issueNumber} --repo "${repo}" --json number,title,body,state,labels,assignees,createdAt,updatedAt,closedAt,url`);
266
+ const task = this.issueToTask(result, repo);
267
+ await auditLogger.log({
268
+ eventType: 'action_executed',
269
+ action: 'task_created',
270
+ outcome: 'success',
271
+ details: {
272
+ taskId: task.id,
273
+ projectName: repo,
274
+ title: params.title,
275
+ priority: task.priority,
276
+ ghIssueUrl: result.url
277
+ }
278
+ });
279
+ return task;
280
+ }
281
+ /**
282
+ * Get all tasks for a project
283
+ */
284
+ async getTasksByProject(projectName, filter) {
285
+ const repo = await this.resolveRepo(projectName);
286
+ let stateFilter = 'all';
287
+ if (filter?.status === 'completed' || filter?.status === 'cancelled') {
288
+ stateFilter = 'closed';
289
+ }
290
+ else if (filter?.status) {
291
+ // Status is pending, in_progress, or blocked - use open issues
292
+ stateFilter = 'open';
293
+ }
294
+ let cmd = `issue list --repo "${repo}" --state ${stateFilter} --json number,title,body,state,labels,assignees,createdAt,updatedAt,closedAt,url --limit 100`;
295
+ const labelFilters = [];
296
+ if (filter?.priority) {
297
+ labelFilters.push(`${this.priorityPrefix}${filter.priority}`);
298
+ }
299
+ if (filter?.status && !['pending', 'completed'].includes(filter.status)) {
300
+ labelFilters.push(`${this.statusPrefix}${filter.status}`);
301
+ }
302
+ if (filter?.labels) {
303
+ labelFilters.push(...filter.labels);
304
+ }
305
+ if (labelFilters.length > 0) {
306
+ cmd += ` --label "${labelFilters.join(',')}"`;
307
+ }
308
+ if (filter?.assignee) {
309
+ cmd += ` --assignee "${filter.assignee}"`;
310
+ }
311
+ const issues = await this.execGh(cmd);
312
+ let tasks = issues.map(issue => this.issueToTask(issue, repo));
313
+ if (filter?.status) {
314
+ tasks = tasks.filter(t => t.status === filter.status);
315
+ }
316
+ const priorityOrder = {
317
+ critical: 0,
318
+ high: 1,
319
+ medium: 2,
320
+ low: 3
321
+ };
322
+ return tasks.sort((a, b) => {
323
+ const priorityDiff = priorityOrder[a.priority] - priorityOrder[b.priority];
324
+ if (priorityDiff !== 0)
325
+ return priorityDiff;
326
+ return new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime();
327
+ });
328
+ }
329
+ /**
330
+ * Get pending tasks for a project
331
+ */
332
+ async getPendingTasks(projectName) {
333
+ return this.getTasksByProject(projectName, { status: 'pending' });
334
+ }
335
+ /**
336
+ * Get in-progress tasks
337
+ */
338
+ async getInProgressTasks(projectName) {
339
+ return this.getTasksByProject(projectName, { status: 'in_progress' });
340
+ }
341
+ /**
342
+ * Get a specific task by ID (issue number)
343
+ */
344
+ async getTask(projectName, taskId) {
345
+ try {
346
+ const repo = await this.resolveRepo(projectName);
347
+ const issue = await this.execGh(`issue view ${taskId} --repo "${repo}" --json number,title,body,state,labels,assignees,createdAt,updatedAt,closedAt,url`);
348
+ return this.issueToTask(issue, repo);
349
+ }
350
+ catch {
351
+ return null;
352
+ }
353
+ }
354
+ /**
355
+ * Update a task
356
+ */
357
+ async updateTask(projectName, taskId, updates) {
358
+ try {
359
+ const repo = await this.resolveRepo(projectName);
360
+ // Verify task exists before attempting update
361
+ const existingTask = await this.getTask(repo, taskId);
362
+ if (!existingTask) {
363
+ console.error(`[TaskManager] Cannot update task ${taskId}: task not found in ${repo}`);
364
+ return null;
365
+ }
366
+ let cmd = `issue edit ${taskId} --repo "${repo}"`;
367
+ if (updates.title) {
368
+ cmd += ` --title ${escapeForShell(updates.title)}`;
369
+ }
370
+ if (updates.description !== undefined) {
371
+ cmd += ` --body ${escapeForShell(updates.description || '')}`;
372
+ }
373
+ if (updates.priority || updates.labels) {
374
+ const newLabels = this.buildLabels(updates.priority || existingTask.priority, updates.status || existingTask.status, updates.labels || existingTask.labels);
375
+ for (const label of newLabels) {
376
+ if (label.startsWith(this.priorityPrefix) || label.startsWith(this.statusPrefix)) {
377
+ await this.ensureLabel(repo, label);
378
+ }
379
+ }
380
+ if (newLabels.length > 0) {
381
+ cmd += ` --add-label ${escapeForShell(newLabels.join(','))}`;
382
+ }
383
+ }
384
+ if (updates.assignee) {
385
+ cmd += ` --add-assignee ${escapeForShell(updates.assignee)}`;
386
+ }
387
+ await this.execGhRaw(cmd);
388
+ // Add comment if provided (with optional commit links)
389
+ if (updates.comment || updates.commits?.length) {
390
+ await this.addComment(repo, taskId, updates.comment, updates.commits);
391
+ }
392
+ if (updates.status) {
393
+ await this.updateTaskStatus(repo, taskId, updates.status);
394
+ }
395
+ else if (updates.closeWithComment) {
396
+ await this.updateTaskStatus(repo, taskId, 'completed');
397
+ }
398
+ const task = await this.getTask(repo, taskId);
399
+ if (task) {
400
+ await auditLogger.log({
401
+ eventType: 'action_executed',
402
+ action: 'task_updated',
403
+ outcome: 'success',
404
+ details: {
405
+ taskId,
406
+ projectName: repo,
407
+ updates: Object.keys(updates),
408
+ hasComment: !!updates.comment,
409
+ commitCount: updates.commits?.length || 0
410
+ }
411
+ });
412
+ }
413
+ return task;
414
+ }
415
+ catch (error) {
416
+ console.error(`[TaskManager] Failed to update task ${taskId}:`, error);
417
+ return null;
418
+ }
419
+ }
420
+ /**
421
+ * Add a comment to a task/issue
422
+ */
423
+ async addComment(projectName, taskId, comment, commits) {
424
+ try {
425
+ const repo = await this.resolveRepo(projectName);
426
+ // Build comment body
427
+ let body = '';
428
+ if (comment) {
429
+ body += comment;
430
+ }
431
+ // Add commit references
432
+ if (commits && commits.length > 0) {
433
+ if (body)
434
+ body += '\n\n';
435
+ body += '### Related Commits\n';
436
+ for (const commit of commits) {
437
+ // Short SHA for display, full for linking
438
+ const shortSha = commit.substring(0, 7);
439
+ body += `- ${shortSha}\n`;
440
+ }
441
+ }
442
+ if (!body) {
443
+ return false;
444
+ }
445
+ await this.execGhRaw(`issue comment ${taskId} --repo "${repo}" --body ${escapeForShell(body)}`);
446
+ await auditLogger.log({
447
+ eventType: 'action_executed',
448
+ action: 'task_comment_added',
449
+ outcome: 'success',
450
+ details: { taskId, projectName: repo, hasCommits: (commits?.length || 0) > 0 }
451
+ });
452
+ return true;
453
+ }
454
+ catch {
455
+ return false;
456
+ }
457
+ }
458
+ /**
459
+ * Link commits to a task by adding a comment
460
+ */
461
+ async linkCommits(projectName, taskId, commits, message) {
462
+ return this.addComment(projectName, taskId, message, commits);
463
+ }
464
+ /**
465
+ * Close a task with a resolution comment
466
+ */
467
+ async closeWithComment(projectName, taskId, resolution, commits) {
468
+ const repo = await this.resolveRepo(projectName);
469
+ // Add the resolution comment
470
+ await this.addComment(repo, taskId, `**Resolution:** ${resolution}`, commits);
471
+ // Close the issue
472
+ return this.updateTaskStatus(repo, taskId, 'completed');
473
+ }
474
+ /**
475
+ * Update task status
476
+ */
477
+ async updateTaskStatus(projectName, taskId, status) {
478
+ try {
479
+ const repo = await this.resolveRepo(projectName);
480
+ // Close or reopen based on status
481
+ if (status === 'completed' || status === 'cancelled') {
482
+ const reason = status === 'cancelled' ? ' --reason "not planned"' : '';
483
+ await this.execGhRaw(`issue close ${taskId} --repo "${repo}"${reason}`);
484
+ }
485
+ else {
486
+ const task = await this.getTask(repo, taskId);
487
+ if (task?.status === 'completed' || task?.status === 'cancelled') {
488
+ await this.execGhRaw(`issue reopen ${taskId} --repo "${repo}"`);
489
+ }
490
+ }
491
+ // Update status label
492
+ if (status !== 'pending' && status !== 'completed') {
493
+ const statusLabel = `${this.statusPrefix}${status}`;
494
+ await this.ensureLabel(repo, statusLabel);
495
+ await this.execGhRaw(`issue edit ${escapeForShell(taskId)} --repo ${escapeForShell(repo)} --add-label ${escapeForShell(statusLabel)}`);
496
+ }
497
+ // Remove old status labels
498
+ const oldStatuses = ['in_progress', 'blocked', 'cancelled'].filter(s => s !== status);
499
+ for (const oldStatus of oldStatuses) {
500
+ try {
501
+ await this.execGhRaw(`issue edit ${escapeForShell(taskId)} --repo ${escapeForShell(repo)} --remove-label ${escapeForShell(this.statusPrefix + oldStatus)}`);
502
+ }
503
+ catch {
504
+ // Ignore - label might not exist
505
+ }
506
+ }
507
+ return this.getTask(repo, taskId);
508
+ }
509
+ catch {
510
+ return null;
511
+ }
512
+ }
513
+ /**
514
+ * Start a task (set to in_progress)
515
+ */
516
+ async startTask(projectName, taskId) {
517
+ return this.updateTaskStatus(projectName, taskId, 'in_progress');
518
+ }
519
+ /**
520
+ * Complete a task
521
+ */
522
+ async completeTask(projectName, taskId) {
523
+ return this.updateTaskStatus(projectName, taskId, 'completed');
524
+ }
525
+ /**
526
+ * Block a task
527
+ */
528
+ async blockTask(projectName, taskId, reason) {
529
+ const repo = await this.resolveRepo(projectName);
530
+ if (reason) {
531
+ await this.execGhRaw(`issue comment ${taskId} --repo "${repo}" --body "Blocked: ${reason}"`);
532
+ }
533
+ return this.updateTaskStatus(repo, taskId, 'blocked');
534
+ }
535
+ /**
536
+ * Delete a task (close as "not planned")
537
+ */
538
+ async deleteTask(projectName, taskId) {
539
+ try {
540
+ const repo = await this.resolveRepo(projectName);
541
+ await this.execGhRaw(`issue close ${taskId} --repo "${repo}" --reason "not planned"`);
542
+ await auditLogger.log({
543
+ eventType: 'action_executed',
544
+ action: 'task_deleted',
545
+ outcome: 'success',
546
+ details: { taskId, projectName: repo }
547
+ });
548
+ return true;
549
+ }
550
+ catch {
551
+ return false;
552
+ }
553
+ }
554
+ /**
555
+ * List projects with issues
556
+ */
557
+ async listProjects() {
558
+ try {
559
+ const repos = await this.execGh('repo list --limit 20 --json nameWithOwner');
560
+ const projects = [];
561
+ const results = await Promise.allSettled(repos.slice(0, 10).map(async (repo) => {
562
+ try {
563
+ const [openIssues, closedIssues] = await Promise.all([
564
+ this.execGh(`issue list --repo "${repo.nameWithOwner}" --state open --json number,labels --limit 100`),
565
+ this.execGh(`issue list --repo "${repo.nameWithOwner}" --state closed --json number --limit 100`)
566
+ ]);
567
+ const inProgressCount = openIssues.filter(i => i.labels?.some(l => l.name === `${this.statusPrefix}in_progress` ||
568
+ l.name === 'in-progress' ||
569
+ l.name === 'wip')).length;
570
+ return {
571
+ name: repo.nameWithOwner,
572
+ taskCount: openIssues.length + closedIssues.length,
573
+ pendingCount: openIssues.length - inProgressCount,
574
+ inProgressCount,
575
+ completedCount: closedIssues.length
576
+ };
577
+ }
578
+ catch {
579
+ return null;
580
+ }
581
+ }));
582
+ for (const result of results) {
583
+ if (result.status === 'fulfilled' && result.value && result.value.taskCount > 0) {
584
+ projects.push(result.value);
585
+ }
586
+ }
587
+ return projects.sort((a, b) => b.taskCount - a.taskCount);
588
+ }
589
+ catch {
590
+ return [];
591
+ }
592
+ }
593
+ /**
594
+ * Get task statistics for a project
595
+ */
596
+ async getProjectStats(projectName) {
597
+ try {
598
+ const repo = await this.resolveRepo(projectName);
599
+ // Fetch only minimal fields needed for stats (labels, closedAt)
600
+ // Increased limit to 500 to capture more tasks for accurate stats
601
+ const [openIssues, closedIssues] = await Promise.all([
602
+ this.execGh(`issue list --repo "${repo}" --state open --json labels --limit 500`),
603
+ this.execGh(`issue list --repo "${repo}" --state closed --json labels,closedAt --limit 500`)
604
+ ]);
605
+ const byStatus = {};
606
+ const byPriority = {};
607
+ let completedThisWeek = 0;
608
+ const oneWeekAgo = new Date(Date.now() - 7 * 24 * 60 * 60 * 1000);
609
+ // Process open issues
610
+ for (const issue of openIssues) {
611
+ const labels = issue.labels?.map(l => l.name) || [];
612
+ // Extract status from labels
613
+ const status = labels.find(l => l.startsWith(this.statusPrefix))?.replace(this.statusPrefix, '') || 'pending';
614
+ byStatus[status] = (byStatus[status] || 0) + 1;
615
+ // Extract priority from labels
616
+ const priority = labels.find(l => l.startsWith(this.priorityPrefix))?.replace(this.priorityPrefix, '') || 'medium';
617
+ byPriority[priority] = (byPriority[priority] || 0) + 1;
618
+ }
619
+ // Process closed issues
620
+ for (const issue of closedIssues) {
621
+ const labels = issue.labels?.map(l => l.name) || [];
622
+ // Extract status from labels
623
+ const status = labels.find(l => l.startsWith(this.statusPrefix))?.replace(this.statusPrefix, '') || 'completed';
624
+ byStatus[status] = (byStatus[status] || 0) + 1;
625
+ // Extract priority from labels
626
+ const priority = labels.find(l => l.startsWith(this.priorityPrefix))?.replace(this.priorityPrefix, '') || 'medium';
627
+ byPriority[priority] = (byPriority[priority] || 0) + 1;
628
+ // Count completed this week
629
+ if (issue.closedAt && new Date(issue.closedAt) >= oneWeekAgo) {
630
+ completedThisWeek++;
631
+ }
632
+ }
633
+ const total = openIssues.length + closedIssues.length;
634
+ return {
635
+ total,
636
+ byStatus,
637
+ byPriority,
638
+ overdue: 0,
639
+ completedThisWeek
640
+ };
641
+ }
642
+ catch (error) {
643
+ console.error('[TaskManager] Failed to get project stats:', error);
644
+ return null;
645
+ }
646
+ }
647
+ /**
648
+ * Search tasks
649
+ */
650
+ async searchTasks(query, projectName) {
651
+ try {
652
+ let cmd = `issue list --search "${query}" --state all --json number,title,body,state,labels,assignees,createdAt,updatedAt,closedAt,url --limit 50`;
653
+ if (projectName) {
654
+ cmd += ` --repo "${projectName}"`;
655
+ }
656
+ const issues = await this.execGh(cmd);
657
+ return issues.map(issue => {
658
+ const repoMatch = issue.url.match(/github\.com\/([^/]+\/[^/]+)\//);
659
+ const repo = repoMatch ? repoMatch[1] : projectName || 'unknown';
660
+ return this.issueToTask(issue, repo);
661
+ });
662
+ }
663
+ catch {
664
+ return [];
665
+ }
666
+ }
667
+ /**
668
+ * Get count of completed tasks (GitHub doesn't support bulk delete)
669
+ */
670
+ async clearCompletedTasks(projectName) {
671
+ const repo = await this.resolveRepo(projectName);
672
+ const closedIssues = await this.execGh(`issue list --repo "${repo}" --state closed --json number --limit 100`);
673
+ return closedIssues.length;
674
+ }
675
+ }
676
+ // Export singleton instance
677
+ export const taskManager = new TaskManager();
678
+ //# sourceMappingURL=TaskManager.js.map