taskode 0.4.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/README.md +398 -0
- package/WORKFLOW.md +53 -0
- package/bin/taskode.js +7 -0
- package/package.json +30 -0
- package/public/app.js +1110 -0
- package/public/index.html +101 -0
- package/public/styles.css +821 -0
- package/src/app-server.js +555 -0
- package/src/auth.js +13 -0
- package/src/cli.js +176 -0
- package/src/orchestrator.js +655 -0
- package/src/path-safety.js +80 -0
- package/src/policy.js +23 -0
- package/src/review.js +197 -0
- package/src/runner.js +133 -0
- package/src/server.js +168 -0
- package/src/ssh.js +82 -0
- package/src/store.js +355 -0
- package/src/tracker/github.js +143 -0
- package/src/tracker/index.js +71 -0
- package/src/tracker/linear.js +229 -0
- package/src/tracker/local.js +75 -0
- package/src/workflow-store.js +77 -0
- package/src/workflow.js +339 -0
- package/src/workspace.js +291 -0
package/src/store.js
ADDED
|
@@ -0,0 +1,355 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
|
|
4
|
+
const DEFAULT_SESSION = {
|
|
5
|
+
sessionId: null,
|
|
6
|
+
threadId: null,
|
|
7
|
+
turnId: null,
|
|
8
|
+
turnCount: 0,
|
|
9
|
+
inputTokens: 0,
|
|
10
|
+
outputTokens: 0,
|
|
11
|
+
totalTokens: 0,
|
|
12
|
+
lastEvent: null,
|
|
13
|
+
workerHost: null,
|
|
14
|
+
rateLimits: null
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
const DEFAULT_DIFF_SUMMARY = {
|
|
18
|
+
total: 0,
|
|
19
|
+
added: 0,
|
|
20
|
+
modified: 0,
|
|
21
|
+
deleted: 0
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
const DEFAULT_DB = {
|
|
25
|
+
tasks: [],
|
|
26
|
+
runs: [],
|
|
27
|
+
logs: [],
|
|
28
|
+
audit: [],
|
|
29
|
+
retries: {},
|
|
30
|
+
runtime: {},
|
|
31
|
+
seq: { task: 0, run: 0 }
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
export class TaskodeStore {
|
|
35
|
+
constructor(dbPath) {
|
|
36
|
+
this.dbPath = dbPath;
|
|
37
|
+
ensureDir(path.dirname(dbPath));
|
|
38
|
+
if (!fs.existsSync(dbPath)) {
|
|
39
|
+
this.write(DEFAULT_DB);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
read() {
|
|
44
|
+
const data = JSON.parse(fs.readFileSync(this.dbPath, 'utf8'));
|
|
45
|
+
return {
|
|
46
|
+
...DEFAULT_DB,
|
|
47
|
+
...data,
|
|
48
|
+
seq: { ...DEFAULT_DB.seq, ...(data.seq || {}) },
|
|
49
|
+
retries: { ...(data.retries || {}) },
|
|
50
|
+
runtime: { ...(data.runtime || {}) },
|
|
51
|
+
tasks: Array.isArray(data.tasks) ? data.tasks.map(normalizeTask) : [],
|
|
52
|
+
runs: Array.isArray(data.runs) ? data.runs.map(normalizeRun) : [],
|
|
53
|
+
logs: Array.isArray(data.logs) ? data.logs : [],
|
|
54
|
+
audit: Array.isArray(data.audit) ? data.audit : []
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
write(data) {
|
|
59
|
+
fs.writeFileSync(this.dbPath, JSON.stringify(data, null, 2), 'utf8');
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
listTasks() {
|
|
63
|
+
const db = this.read();
|
|
64
|
+
return db.tasks.sort((a, b) => b.updatedAt.localeCompare(a.updatedAt));
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
createTask(input) {
|
|
68
|
+
const db = this.read();
|
|
69
|
+
const id = input.id || `T-${++db.seq.task}`;
|
|
70
|
+
const task = buildTask({ ...input, id });
|
|
71
|
+
db.tasks.push(task);
|
|
72
|
+
this.write(db);
|
|
73
|
+
return task;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
upsertTask(input) {
|
|
77
|
+
const db = this.read();
|
|
78
|
+
const index = findTaskIndex(db.tasks, input.id, input.externalId);
|
|
79
|
+
const now = new Date().toISOString();
|
|
80
|
+
|
|
81
|
+
if (index === -1) {
|
|
82
|
+
const id = input.id || `T-${++db.seq.task}`;
|
|
83
|
+
const task = buildTask({ ...input, id, createdAt: input.createdAt || now, updatedAt: now });
|
|
84
|
+
db.tasks.push(task);
|
|
85
|
+
this.write(db);
|
|
86
|
+
return task;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const existing = db.tasks[index];
|
|
90
|
+
db.tasks[index] = normalizeTask({
|
|
91
|
+
...existing,
|
|
92
|
+
...sanitizeTaskPatch(input),
|
|
93
|
+
id: existing.id,
|
|
94
|
+
createdAt: existing.createdAt,
|
|
95
|
+
updatedAt: now
|
|
96
|
+
});
|
|
97
|
+
this.write(db);
|
|
98
|
+
return db.tasks[index];
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
updateTask(id, patch) {
|
|
102
|
+
const db = this.read();
|
|
103
|
+
const index = db.tasks.findIndex((task) => task.id === id);
|
|
104
|
+
if (index === -1) return null;
|
|
105
|
+
db.tasks[index] = normalizeTask({
|
|
106
|
+
...db.tasks[index],
|
|
107
|
+
...sanitizeTaskPatch(patch),
|
|
108
|
+
updatedAt: new Date().toISOString()
|
|
109
|
+
});
|
|
110
|
+
this.write(db);
|
|
111
|
+
return db.tasks[index];
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
updateTaskByIdentifier(identifier, patch) {
|
|
115
|
+
return this.updateTask(identifier, patch);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
getTask(id) {
|
|
119
|
+
return this.read().tasks.find((task) => task.id === id) || null;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
getTaskByExternalId(externalId) {
|
|
123
|
+
return this.read().tasks.find((task) => task.externalId === externalId) || null;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
listRuns(taskId) {
|
|
127
|
+
return this.read().runs
|
|
128
|
+
.filter((run) => !taskId || run.taskId === taskId || run.issueIdentifier === taskId)
|
|
129
|
+
.sort((a, b) => b.startedAt.localeCompare(a.startedAt));
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
getRun(runId) {
|
|
133
|
+
return this.read().runs.find((run) => run.id === runId) || null;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
createRun(input) {
|
|
137
|
+
const db = this.read();
|
|
138
|
+
const id = `R-${++db.seq.run}`;
|
|
139
|
+
const now = new Date().toISOString();
|
|
140
|
+
const run = normalizeRun({
|
|
141
|
+
id,
|
|
142
|
+
taskId: input.taskId,
|
|
143
|
+
issueIdentifier: input.issueIdentifier || input.taskId,
|
|
144
|
+
attempt: input.attempt || 1,
|
|
145
|
+
status: input.status || 'queued',
|
|
146
|
+
summary: input.summary || 'Run queued',
|
|
147
|
+
logs: [`${now} queued`],
|
|
148
|
+
startedAt: now,
|
|
149
|
+
reviewStatus: input.reviewStatus || 'none'
|
|
150
|
+
});
|
|
151
|
+
db.runs.push(run);
|
|
152
|
+
this.write(db);
|
|
153
|
+
return run;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
appendRunLog(runId, line) {
|
|
157
|
+
const db = this.read();
|
|
158
|
+
const run = db.runs.find((entry) => entry.id === runId);
|
|
159
|
+
if (!run) return null;
|
|
160
|
+
run.logs.push(`${new Date().toISOString()} ${line}`);
|
|
161
|
+
this.write(db);
|
|
162
|
+
return run;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
updateRunSession(runId, patch) {
|
|
166
|
+
const db = this.read();
|
|
167
|
+
const run = db.runs.find((entry) => entry.id === runId);
|
|
168
|
+
if (!run) return null;
|
|
169
|
+
run.session = { ...run.session, ...patch };
|
|
170
|
+
this.write(db);
|
|
171
|
+
return run;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
updateRun(runId, patch) {
|
|
175
|
+
const db = this.read();
|
|
176
|
+
const run = db.runs.find((entry) => entry.id === runId);
|
|
177
|
+
if (!run) return null;
|
|
178
|
+
Object.assign(run, normalizeRun({ ...run, ...patch }));
|
|
179
|
+
this.write(db);
|
|
180
|
+
return run;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
appendSystemLog(message) {
|
|
184
|
+
const db = this.read();
|
|
185
|
+
db.logs.push({ at: new Date().toISOString(), message });
|
|
186
|
+
if (db.logs.length > 2000) db.logs = db.logs.slice(-2000);
|
|
187
|
+
this.write(db);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
listSystemLogs(limit = 200) {
|
|
191
|
+
const logs = this.read().logs;
|
|
192
|
+
return logs.slice(-limit).reverse();
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
appendAudit(entry) {
|
|
196
|
+
const db = this.read();
|
|
197
|
+
db.audit.push({ at: new Date().toISOString(), ...entry });
|
|
198
|
+
if (db.audit.length > 5000) db.audit = db.audit.slice(-5000);
|
|
199
|
+
this.write(db);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
listAudit(limit = 200) {
|
|
203
|
+
const audit = this.read().audit;
|
|
204
|
+
return audit.slice(-limit).reverse();
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
setRetry(issueId, retryEntry) {
|
|
208
|
+
const db = this.read();
|
|
209
|
+
db.retries[issueId] = retryEntry;
|
|
210
|
+
this.write(db);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
removeRetry(issueId) {
|
|
214
|
+
const db = this.read();
|
|
215
|
+
delete db.retries[issueId];
|
|
216
|
+
this.write(db);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
listRetries() {
|
|
220
|
+
return this.read().retries;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
saveRuntime(runtime) {
|
|
224
|
+
const db = this.read();
|
|
225
|
+
db.runtime = runtime;
|
|
226
|
+
this.write(db);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
loadRuntime() {
|
|
230
|
+
return this.read().runtime;
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
function buildTask(input) {
|
|
235
|
+
const now = new Date().toISOString();
|
|
236
|
+
return normalizeTask({
|
|
237
|
+
id: input.id,
|
|
238
|
+
title: input.title,
|
|
239
|
+
description: input.description || '',
|
|
240
|
+
type: input.type || 'todo',
|
|
241
|
+
status: input.status || 'todo',
|
|
242
|
+
priority: input.priority || 'medium',
|
|
243
|
+
externalId: input.externalId || null,
|
|
244
|
+
trackerKind: input.trackerKind || 'local',
|
|
245
|
+
trackerState: input.trackerState || null,
|
|
246
|
+
url: input.url || null,
|
|
247
|
+
labels: input.labels || [],
|
|
248
|
+
dependencies: input.dependencies || [],
|
|
249
|
+
createdAt: input.createdAt || now,
|
|
250
|
+
updatedAt: input.updatedAt || now
|
|
251
|
+
});
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
function normalizeTask(task) {
|
|
255
|
+
const now = new Date().toISOString();
|
|
256
|
+
return {
|
|
257
|
+
id: task.id,
|
|
258
|
+
title: String(task.title || ''),
|
|
259
|
+
description: String(task.description || ''),
|
|
260
|
+
type: String(task.type || 'todo'),
|
|
261
|
+
status: normalizeStatus(task.status),
|
|
262
|
+
priority: normalizePriority(task.priority),
|
|
263
|
+
externalId: task.externalId ?? null,
|
|
264
|
+
trackerKind: task.trackerKind || 'local',
|
|
265
|
+
trackerState: task.trackerState || null,
|
|
266
|
+
url: task.url || null,
|
|
267
|
+
labels: normalizeStringArray(task.labels),
|
|
268
|
+
dependencies: normalizeStringArray(task.dependencies),
|
|
269
|
+
createdAt: task.createdAt || now,
|
|
270
|
+
updatedAt: task.updatedAt || now
|
|
271
|
+
};
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
function sanitizeTaskPatch(patch) {
|
|
275
|
+
const next = {};
|
|
276
|
+
|
|
277
|
+
if (patch.title !== undefined) next.title = String(patch.title || '');
|
|
278
|
+
if (patch.description !== undefined) next.description = String(patch.description || '');
|
|
279
|
+
if (patch.type !== undefined) next.type = String(patch.type || 'todo');
|
|
280
|
+
if (patch.status !== undefined) next.status = normalizeStatus(patch.status);
|
|
281
|
+
if (patch.priority !== undefined) next.priority = normalizePriority(patch.priority);
|
|
282
|
+
if (patch.externalId !== undefined) next.externalId = patch.externalId ?? null;
|
|
283
|
+
if (patch.trackerKind !== undefined) next.trackerKind = patch.trackerKind || 'local';
|
|
284
|
+
if (patch.trackerState !== undefined) next.trackerState = patch.trackerState || null;
|
|
285
|
+
if (patch.url !== undefined) next.url = patch.url || null;
|
|
286
|
+
if (patch.labels !== undefined) next.labels = normalizeStringArray(patch.labels);
|
|
287
|
+
if (patch.dependencies !== undefined) next.dependencies = normalizeStringArray(patch.dependencies);
|
|
288
|
+
|
|
289
|
+
return next;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
function normalizeRun(run) {
|
|
293
|
+
return {
|
|
294
|
+
id: run.id,
|
|
295
|
+
taskId: run.taskId,
|
|
296
|
+
issueIdentifier: run.issueIdentifier || run.taskId,
|
|
297
|
+
attempt: Number(run.attempt || 1),
|
|
298
|
+
status: run.status || 'queued',
|
|
299
|
+
summary: run.summary || 'Run queued',
|
|
300
|
+
logs: Array.isArray(run.logs) ? run.logs : [],
|
|
301
|
+
session: { ...DEFAULT_SESSION, ...(run.session || {}) },
|
|
302
|
+
startedAt: run.startedAt || new Date().toISOString(),
|
|
303
|
+
finishedAt: run.finishedAt || null,
|
|
304
|
+
workspacePath: run.workspacePath || null,
|
|
305
|
+
workspaceKey: run.workspaceKey || null,
|
|
306
|
+
logFile: run.logFile || null,
|
|
307
|
+
diffFile: run.diffFile || null,
|
|
308
|
+
changedFiles: Array.isArray(run.changedFiles) ? run.changedFiles.map(normalizeChangedFile) : [],
|
|
309
|
+
diffSummary: { ...DEFAULT_DIFF_SUMMARY, ...(run.diffSummary || {}) },
|
|
310
|
+
reviewStatus: run.reviewStatus || 'none',
|
|
311
|
+
appliedAt: run.appliedAt || null,
|
|
312
|
+
error: run.error || null
|
|
313
|
+
};
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
function normalizeChangedFile(file) {
|
|
317
|
+
return {
|
|
318
|
+
path: String(file.path || ''),
|
|
319
|
+
status: file.status || 'modified'
|
|
320
|
+
};
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
function normalizeStatus(status) {
|
|
324
|
+
const value = String(status || 'todo').toLowerCase();
|
|
325
|
+
if (['todo', 'in_progress', 'blocked', 'review', 'done', 'failed', 'cancelled'].includes(value)) {
|
|
326
|
+
return value;
|
|
327
|
+
}
|
|
328
|
+
return 'todo';
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
function normalizePriority(priority) {
|
|
332
|
+
const value = String(priority || 'medium').toLowerCase();
|
|
333
|
+
if (['low', 'medium', 'high'].includes(value)) return value;
|
|
334
|
+
return 'medium';
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
function normalizeStringArray(value) {
|
|
338
|
+
if (!Array.isArray(value)) return [];
|
|
339
|
+
return [...new Set(value.map((entry) => String(entry || '').trim()).filter(Boolean))];
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
function findTaskIndex(tasks, id, externalId) {
|
|
343
|
+
if (id) {
|
|
344
|
+
const byId = tasks.findIndex((task) => task.id === id);
|
|
345
|
+
if (byId !== -1) return byId;
|
|
346
|
+
}
|
|
347
|
+
if (externalId) {
|
|
348
|
+
return tasks.findIndex((task) => task.externalId === externalId);
|
|
349
|
+
}
|
|
350
|
+
return -1;
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
function ensureDir(dir) {
|
|
354
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
355
|
+
}
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
export class GitHubTracker {
|
|
2
|
+
constructor({ config, store }) {
|
|
3
|
+
this.config = config;
|
|
4
|
+
this.store = store;
|
|
5
|
+
|
|
6
|
+
if (!config.tracker.githubToken) {
|
|
7
|
+
throw new Error('GITHUB tracker requires tracker.github_token or GITHUB_TOKEN');
|
|
8
|
+
}
|
|
9
|
+
if (!config.tracker.githubOwner || !config.tracker.githubRepo) {
|
|
10
|
+
throw new Error('GITHUB tracker requires tracker.github_owner and tracker.github_repo');
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
async fetchCandidates() {
|
|
15
|
+
const issues = await this.callGitHub(
|
|
16
|
+
`/repos/${this.config.tracker.githubOwner}/${this.config.tracker.githubRepo}/issues?state=open&per_page=50`
|
|
17
|
+
);
|
|
18
|
+
|
|
19
|
+
const mapped = issues
|
|
20
|
+
.filter((issue) => !issue.pull_request)
|
|
21
|
+
.map(mapIssue)
|
|
22
|
+
.filter((issue) => labelsMatch(issue.labels, this.config.tracker.githubLabels));
|
|
23
|
+
|
|
24
|
+
return this.syncIssues(mapped);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
async fetchByIds(ids) {
|
|
28
|
+
const mapped = [];
|
|
29
|
+
|
|
30
|
+
for (const id of ids) {
|
|
31
|
+
const issue = await this.callGitHub(
|
|
32
|
+
`/repos/${this.config.tracker.githubOwner}/${this.config.tracker.githubRepo}/issues/${id}`
|
|
33
|
+
);
|
|
34
|
+
if (issue.pull_request) continue;
|
|
35
|
+
mapped.push(mapIssue(issue));
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
return this.syncIssues(mapped);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
async writeComment(issueId, body) {
|
|
42
|
+
await this.callGitHub(
|
|
43
|
+
`/repos/${this.config.tracker.githubOwner}/${this.config.tracker.githubRepo}/issues/${issueId}/comments`,
|
|
44
|
+
{
|
|
45
|
+
method: 'POST',
|
|
46
|
+
body: { body }
|
|
47
|
+
}
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
async transitionState(issueId, stateName) {
|
|
52
|
+
const normalized = String(stateName || '').toLowerCase();
|
|
53
|
+
const state = ['done', 'closed'].some((token) => normalized.includes(token)) ? 'closed' : 'open';
|
|
54
|
+
|
|
55
|
+
await this.callGitHub(
|
|
56
|
+
`/repos/${this.config.tracker.githubOwner}/${this.config.tracker.githubRepo}/issues/${issueId}`,
|
|
57
|
+
{
|
|
58
|
+
method: 'PATCH',
|
|
59
|
+
body: { state }
|
|
60
|
+
}
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
async linkPullRequest(issueId, prUrl) {
|
|
65
|
+
await this.writeComment(issueId, `PR: ${prUrl}`);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
async callGitHub(resource, options = {}) {
|
|
69
|
+
const response = await fetch(`${this.config.tracker.githubBaseUrl}${resource}`, {
|
|
70
|
+
method: options.method || 'GET',
|
|
71
|
+
headers: {
|
|
72
|
+
Accept: 'application/vnd.github+json',
|
|
73
|
+
Authorization: `Bearer ${this.config.tracker.githubToken}`,
|
|
74
|
+
'Content-Type': 'application/json',
|
|
75
|
+
'User-Agent': 'taskode'
|
|
76
|
+
},
|
|
77
|
+
body: options.body ? JSON.stringify(options.body) : undefined
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
if (!response.ok) {
|
|
81
|
+
throw new Error(`GitHub API failed: ${response.status}`);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
return response.json();
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
syncIssues(issues) {
|
|
88
|
+
for (const issue of issues) {
|
|
89
|
+
this.store.upsertTask({
|
|
90
|
+
id: issue.identifier,
|
|
91
|
+
title: issue.title,
|
|
92
|
+
description: issue.description,
|
|
93
|
+
type: 'issue',
|
|
94
|
+
status: mergeSyncedStatus(this.store.getTask(issue.identifier)?.status, normalizeTrackerState(issue.state)),
|
|
95
|
+
priority: issue.priority,
|
|
96
|
+
externalId: issue.id,
|
|
97
|
+
trackerKind: 'github',
|
|
98
|
+
trackerState: issue.state,
|
|
99
|
+
url: issue.url,
|
|
100
|
+
labels: issue.labels,
|
|
101
|
+
dependencies: issue.blocked_by
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
return issues;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function mapIssue(issue) {
|
|
110
|
+
const labels = (issue.labels || []).map((label) => String(label.name || '').toLowerCase());
|
|
111
|
+
|
|
112
|
+
return {
|
|
113
|
+
id: String(issue.number),
|
|
114
|
+
identifier: `GH-${issue.number}`,
|
|
115
|
+
title: issue.title,
|
|
116
|
+
description: issue.body || '',
|
|
117
|
+
priority: labelsToPriority(labels),
|
|
118
|
+
state: issue.state,
|
|
119
|
+
url: issue.html_url,
|
|
120
|
+
labels,
|
|
121
|
+
blocked_by: []
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function labelsMatch(labels, requiredLabels) {
|
|
126
|
+
if (!requiredLabels.length) return true;
|
|
127
|
+
return requiredLabels.every((required) => labels.includes(required.toLowerCase()));
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function labelsToPriority(labels) {
|
|
131
|
+
if (labels.includes('priority:high') || labels.includes('p0') || labels.includes('p1')) return 'high';
|
|
132
|
+
if (labels.includes('priority:low') || labels.includes('p3')) return 'low';
|
|
133
|
+
return 'medium';
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function normalizeTrackerState(state) {
|
|
137
|
+
return String(state || '').toLowerCase() === 'closed' ? 'done' : 'todo';
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function mergeSyncedStatus(currentStatus, remoteStatus) {
|
|
141
|
+
if (['review', 'done', 'failed'].includes(currentStatus)) return currentStatus;
|
|
142
|
+
return remoteStatus;
|
|
143
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { GitHubTracker } from './github.js';
|
|
2
|
+
import { LinearTracker } from './linear.js';
|
|
3
|
+
import { LocalTracker } from './local.js';
|
|
4
|
+
|
|
5
|
+
export function createTracker({ store, config }) {
|
|
6
|
+
return new TrackerFacade({ store, config });
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
class TrackerFacade {
|
|
10
|
+
constructor({ store, config }) {
|
|
11
|
+
this.store = store;
|
|
12
|
+
this.config = config;
|
|
13
|
+
this.kind = null;
|
|
14
|
+
this.impl = null;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
async fetchCandidates() {
|
|
18
|
+
return this.current().fetchCandidates();
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
async fetchByIds(ids) {
|
|
22
|
+
return this.current().fetchByIds(ids);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
async writeComment(issueId, body) {
|
|
26
|
+
return this.current().writeComment(issueId, body);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
async transitionState(issueId, stateName) {
|
|
30
|
+
return this.current().transitionState(issueId, stateName);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
async linkPullRequest(issueId, prUrl) {
|
|
34
|
+
return this.current().linkPullRequest(issueId, prUrl);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
async executeDynamicTool(name, argumentsValue) {
|
|
38
|
+
if (typeof this.current().executeDynamicTool === 'function') {
|
|
39
|
+
return this.current().executeDynamicTool(name, argumentsValue);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return {
|
|
43
|
+
success: false,
|
|
44
|
+
output: `Unsupported dynamic tool: ${name}`,
|
|
45
|
+
contentItems: [{ type: 'inputText', text: `Unsupported dynamic tool: ${name}` }]
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
current() {
|
|
50
|
+
const nextKind = this.config.tracker.kind;
|
|
51
|
+
if (this.impl && this.kind === nextKind) {
|
|
52
|
+
return this.impl;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
this.kind = nextKind;
|
|
56
|
+
this.impl = instantiateTracker({ store: this.store, config: this.config, kind: nextKind });
|
|
57
|
+
return this.impl;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function instantiateTracker({ store, config, kind }) {
|
|
62
|
+
if (kind === 'linear') {
|
|
63
|
+
return new LinearTracker({ config, store });
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
if (kind === 'github') {
|
|
67
|
+
return new GitHubTracker({ config, store });
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
return new LocalTracker({ store, config });
|
|
71
|
+
}
|