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