@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.
- package/LICENSE +21 -0
- package/README.md +446 -0
- package/dist/analyzers/ArchitectureDetector.d.ts +44 -0
- package/dist/analyzers/ArchitectureDetector.d.ts.map +1 -0
- package/dist/analyzers/ArchitectureDetector.js +218 -0
- package/dist/analyzers/ArchitectureDetector.js.map +1 -0
- package/dist/analyzers/CSSAnalyzer.d.ts +104 -0
- package/dist/analyzers/CSSAnalyzer.d.ts.map +1 -0
- package/dist/analyzers/CSSAnalyzer.js +578 -0
- package/dist/analyzers/CSSAnalyzer.js.map +1 -0
- package/dist/analyzers/index.d.ts +5 -0
- package/dist/analyzers/index.d.ts.map +1 -0
- package/dist/analyzers/index.js +5 -0
- package/dist/analyzers/index.js.map +1 -0
- package/dist/cli.d.ts +8 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +174 -0
- package/dist/cli.js.map +1 -0
- package/dist/design-system/index.d.ts +6 -0
- package/dist/design-system/index.d.ts.map +1 -0
- package/dist/design-system/index.js +6 -0
- package/dist/design-system/index.js.map +1 -0
- package/dist/design-system/tokens.d.ts +106 -0
- package/dist/design-system/tokens.d.ts.map +1 -0
- package/dist/design-system/tokens.js +554 -0
- package/dist/design-system/tokens.js.map +1 -0
- package/dist/engine/AppMonitor.d.ts +162 -0
- package/dist/engine/AppMonitor.d.ts.map +1 -0
- package/dist/engine/AppMonitor.js +754 -0
- package/dist/engine/AppMonitor.js.map +1 -0
- package/dist/engine/AuditLogger.d.ts +138 -0
- package/dist/engine/AuditLogger.d.ts.map +1 -0
- package/dist/engine/AuditLogger.js +448 -0
- package/dist/engine/AuditLogger.js.map +1 -0
- package/dist/engine/GitHubApprovalManager.d.ts +106 -0
- package/dist/engine/GitHubApprovalManager.d.ts.map +1 -0
- package/dist/engine/GitHubApprovalManager.js +315 -0
- package/dist/engine/GitHubApprovalManager.js.map +1 -0
- package/dist/engine/RateLimiter.d.ts +79 -0
- package/dist/engine/RateLimiter.d.ts.map +1 -0
- package/dist/engine/RateLimiter.js +232 -0
- package/dist/engine/RateLimiter.js.map +1 -0
- package/dist/engine/RulesEngine.d.ts +77 -0
- package/dist/engine/RulesEngine.d.ts.map +1 -0
- package/dist/engine/RulesEngine.js +400 -0
- package/dist/engine/RulesEngine.js.map +1 -0
- package/dist/engine/TaskManager.d.ts +173 -0
- package/dist/engine/TaskManager.d.ts.map +1 -0
- package/dist/engine/TaskManager.js +678 -0
- package/dist/engine/TaskManager.js.map +1 -0
- package/dist/engine/index.d.ts +9 -0
- package/dist/engine/index.d.ts.map +1 -0
- package/dist/engine/index.js +9 -0
- package/dist/engine/index.js.map +1 -0
- package/dist/index.d.ts +21 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +29 -0
- package/dist/index.js.map +1 -0
- package/dist/rules/architecture.d.ts +9 -0
- package/dist/rules/architecture.d.ts.map +1 -0
- package/dist/rules/architecture.js +322 -0
- package/dist/rules/architecture.js.map +1 -0
- package/dist/rules/azure.d.ts +7 -0
- package/dist/rules/azure.d.ts.map +1 -0
- package/dist/rules/azure.js +138 -0
- package/dist/rules/azure.js.map +1 -0
- package/dist/rules/compliance.d.ts +9 -0
- package/dist/rules/compliance.d.ts.map +1 -0
- package/dist/rules/compliance.js +304 -0
- package/dist/rules/compliance.js.map +1 -0
- package/dist/rules/css.d.ts +10 -0
- package/dist/rules/css.d.ts.map +1 -0
- package/dist/rules/css.js +1239 -0
- package/dist/rules/css.js.map +1 -0
- package/dist/rules/flask.d.ts +7 -0
- package/dist/rules/flask.d.ts.map +1 -0
- package/dist/rules/flask.js +155 -0
- package/dist/rules/flask.js.map +1 -0
- package/dist/rules/index.d.ts +607 -0
- package/dist/rules/index.d.ts.map +1 -0
- package/dist/rules/index.js +401 -0
- package/dist/rules/index.js.map +1 -0
- package/dist/rules/ml-ai.d.ts +7 -0
- package/dist/rules/ml-ai.d.ts.map +1 -0
- package/dist/rules/ml-ai.js +150 -0
- package/dist/rules/ml-ai.js.map +1 -0
- package/dist/rules/operational.d.ts +9 -0
- package/dist/rules/operational.d.ts.map +1 -0
- package/dist/rules/operational.js +318 -0
- package/dist/rules/operational.js.map +1 -0
- package/dist/rules/security.d.ts +9 -0
- package/dist/rules/security.d.ts.map +1 -0
- package/dist/rules/security.js +287 -0
- package/dist/rules/security.js.map +1 -0
- package/dist/rules/storage.d.ts +7 -0
- package/dist/rules/storage.d.ts.map +1 -0
- package/dist/rules/storage.js +134 -0
- package/dist/rules/storage.js.map +1 -0
- package/dist/rules/stripe.d.ts +7 -0
- package/dist/rules/stripe.d.ts.map +1 -0
- package/dist/rules/stripe.js +140 -0
- package/dist/rules/stripe.js.map +1 -0
- package/dist/rules/testing.d.ts +7 -0
- package/dist/rules/testing.d.ts.map +1 -0
- package/dist/rules/testing.js +135 -0
- package/dist/rules/testing.js.map +1 -0
- package/dist/rules/ux.d.ts +9 -0
- package/dist/rules/ux.d.ts.map +1 -0
- package/dist/rules/ux.js +280 -0
- package/dist/rules/ux.js.map +1 -0
- package/dist/rules/websocket.d.ts +7 -0
- package/dist/rules/websocket.d.ts.map +1 -0
- package/dist/rules/websocket.js +136 -0
- package/dist/rules/websocket.js.map +1 -0
- package/dist/server.d.ts +49 -0
- package/dist/server.d.ts.map +1 -0
- package/dist/server.js +2330 -0
- package/dist/server.js.map +1 -0
- package/dist/supervisor/AgentSupervisor.d.ts +235 -0
- package/dist/supervisor/AgentSupervisor.d.ts.map +1 -0
- package/dist/supervisor/AgentSupervisor.js +596 -0
- package/dist/supervisor/AgentSupervisor.js.map +1 -0
- package/dist/supervisor/ManagedServerRegistry.d.ts +48 -0
- package/dist/supervisor/ManagedServerRegistry.d.ts.map +1 -0
- package/dist/supervisor/ManagedServerRegistry.js +145 -0
- package/dist/supervisor/ManagedServerRegistry.js.map +1 -0
- package/dist/supervisor/ProjectTracker.d.ts +188 -0
- package/dist/supervisor/ProjectTracker.d.ts.map +1 -0
- package/dist/supervisor/ProjectTracker.js +617 -0
- package/dist/supervisor/ProjectTracker.js.map +1 -0
- package/dist/supervisor/index.d.ts +6 -0
- package/dist/supervisor/index.d.ts.map +1 -0
- package/dist/supervisor/index.js +6 -0
- package/dist/supervisor/index.js.map +1 -0
- package/dist/types/index.d.ts +1176 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/index.js +391 -0
- package/dist/types/index.js.map +1 -0
- package/dist/utils/errors.d.ts +86 -0
- package/dist/utils/errors.d.ts.map +1 -0
- package/dist/utils/errors.js +171 -0
- package/dist/utils/errors.js.map +1 -0
- package/dist/utils/index.d.ts +5 -0
- package/dist/utils/index.d.ts.map +1 -0
- package/dist/utils/index.js +5 -0
- package/dist/utils/index.js.map +1 -0
- package/dist/utils/shell.d.ts +22 -0
- package/dist/utils/shell.d.ts.map +1 -0
- package/dist/utils/shell.js +29 -0
- package/dist/utils/shell.js.map +1 -0
- 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
|