claude-teammate 0.1.152 → 0.1.154
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/package.json +1 -1
- package/src/commands/status.js +32 -0
- package/src/commands/worker.js +240 -32
- package/src/dashboard/ui.html +21 -4
package/package.json
CHANGED
package/src/commands/status.js
CHANGED
|
@@ -53,6 +53,20 @@ export async function runStatusCommand({ projectRoot, args = [] }) {
|
|
|
53
53
|
process.stdout.write(`Last PR error: ${state.lastPrError}\n`);
|
|
54
54
|
}
|
|
55
55
|
|
|
56
|
+
const pollerCurrent = state.pollerCurrent || {};
|
|
57
|
+
if (pollerCurrent.jira) {
|
|
58
|
+
process.stdout.write(`Current Jira item: ${pollerCurrent.jira}\n`);
|
|
59
|
+
}
|
|
60
|
+
if (pollerCurrent.github) {
|
|
61
|
+
process.stdout.write(`Current GitHub item: ${formatQueueIdLabel(pollerCurrent.github, "#")}\n`);
|
|
62
|
+
}
|
|
63
|
+
if (pollerCurrent.draftPr) {
|
|
64
|
+
process.stdout.write(`Current Draft PR: ${formatQueueIdLabel(pollerCurrent.draftPr, "!")}\n`);
|
|
65
|
+
}
|
|
66
|
+
if (pollerCurrent.reviewPr) {
|
|
67
|
+
process.stdout.write(`Current Review PR: ${formatQueueIdLabel(pollerCurrent.reviewPr, "!")}\n`);
|
|
68
|
+
}
|
|
69
|
+
|
|
56
70
|
if (Array.isArray(state.issues) && state.issues.length > 0) {
|
|
57
71
|
process.stdout.write("Issues:\n");
|
|
58
72
|
for (const issue of state.issues) {
|
|
@@ -79,6 +93,15 @@ export async function runStatusCommand({ projectRoot, args = [] }) {
|
|
|
79
93
|
);
|
|
80
94
|
}
|
|
81
95
|
}
|
|
96
|
+
|
|
97
|
+
if (Array.isArray(state.reviewPrs) && state.reviewPrs.length > 0) {
|
|
98
|
+
process.stdout.write("Review PRs:\n");
|
|
99
|
+
for (const pullRequest of state.reviewPrs) {
|
|
100
|
+
process.stdout.write(
|
|
101
|
+
`- ${formatForgeQueueLabel(pullRequest.repoUrl, pullRequest.pullRequestNumber)}${pullRequest.title ? ` | ${pullRequest.title}` : ""}${Number.isInteger(pullRequest.suggestionsCount) ? ` | suggestions: ${pullRequest.suggestionsCount}` : ""}\n`
|
|
102
|
+
);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
82
105
|
}
|
|
83
106
|
}
|
|
84
107
|
|
|
@@ -92,3 +115,12 @@ function formatForgeQueueLabel(repoUrl, itemNumber) {
|
|
|
92
115
|
return `repo #${itemNumber}`;
|
|
93
116
|
}
|
|
94
117
|
}
|
|
118
|
+
|
|
119
|
+
function formatQueueIdLabel(queueId, separator) {
|
|
120
|
+
const value = String(queueId || "").trim();
|
|
121
|
+
if (!value) {
|
|
122
|
+
return "";
|
|
123
|
+
}
|
|
124
|
+
const [repoUrl, itemNumber] = value.split(separator);
|
|
125
|
+
return formatForgeQueueLabel(repoUrl, itemNumber);
|
|
126
|
+
}
|
package/src/commands/worker.js
CHANGED
|
@@ -170,29 +170,30 @@ export async function runWorkerCommand({ projectRoot }) {
|
|
|
170
170
|
...previousState,
|
|
171
171
|
projectRoot,
|
|
172
172
|
startedAt: new Date().toISOString(),
|
|
173
|
-
lastPollAt: null,
|
|
174
|
-
lastSuccessAt: null,
|
|
173
|
+
lastPollAt: previousState.lastPollAt ?? null,
|
|
174
|
+
lastSuccessAt: previousState.lastSuccessAt ?? null,
|
|
175
175
|
lastError: null,
|
|
176
|
-
issueCount: 0,
|
|
177
|
-
issues: [],
|
|
178
|
-
lastGitHubPollAt: null,
|
|
179
|
-
lastGitHubSuccessAt: null,
|
|
176
|
+
issueCount: previousState.issueCount ?? 0,
|
|
177
|
+
issues: Array.isArray(previousState.issues) ? previousState.issues : [],
|
|
178
|
+
lastGitHubPollAt: previousState.lastGitHubPollAt ?? null,
|
|
179
|
+
lastGitHubSuccessAt: previousState.lastGitHubSuccessAt ?? null,
|
|
180
180
|
lastGitHubError: null,
|
|
181
|
-
githubIssueCount: 0,
|
|
182
|
-
githubIssues: [],
|
|
183
|
-
lastPrPollAt: null,
|
|
184
|
-
lastPrSuccessAt: null,
|
|
181
|
+
githubIssueCount: previousState.githubIssueCount ?? 0,
|
|
182
|
+
githubIssues: Array.isArray(previousState.githubIssues) ? previousState.githubIssues : [],
|
|
183
|
+
lastPrPollAt: previousState.lastPrPollAt ?? null,
|
|
184
|
+
lastPrSuccessAt: previousState.lastPrSuccessAt ?? null,
|
|
185
185
|
lastPrError: null,
|
|
186
|
-
draftPrCount: 0,
|
|
187
|
-
draftPrs: [],
|
|
186
|
+
draftPrCount: previousState.draftPrCount ?? 0,
|
|
187
|
+
draftPrs: Array.isArray(previousState.draftPrs) ? previousState.draftPrs : [],
|
|
188
188
|
prCommentReview: buildPrSubtaskState(previousState.prCommentReview),
|
|
189
189
|
prImplementation: buildPrSubtaskState(previousState.prImplementation),
|
|
190
|
-
lastReviewPollAt: null,
|
|
191
|
-
lastReviewSuccessAt: null,
|
|
190
|
+
lastReviewPollAt: previousState.lastReviewPollAt ?? null,
|
|
191
|
+
lastReviewSuccessAt: previousState.lastReviewSuccessAt ?? null,
|
|
192
192
|
lastReviewError: null,
|
|
193
|
-
reviewPrCount: 0,
|
|
194
|
-
reviewPrs: [],
|
|
195
|
-
pollerBusy: { jira: false, github: false, draftPr: false, reviewPr: false }
|
|
193
|
+
reviewPrCount: previousState.reviewPrCount ?? 0,
|
|
194
|
+
reviewPrs: Array.isArray(previousState.reviewPrs) ? previousState.reviewPrs : [],
|
|
195
|
+
pollerBusy: { jira: false, github: false, draftPr: false, reviewPr: false },
|
|
196
|
+
pollerCurrent: { jira: "", github: "", draftPr: "", reviewPr: "" }
|
|
196
197
|
};
|
|
197
198
|
|
|
198
199
|
const updatePrSubtaskState = async (key, updates) => {
|
|
@@ -255,8 +256,15 @@ export async function runWorkerCommand({ projectRoot }) {
|
|
|
255
256
|
await writeState(runtimePaths.stateFile, state);
|
|
256
257
|
await runPoll("", "Jira", state, async () => {
|
|
257
258
|
const result = await jira.fetchAssignedIssues();
|
|
258
|
-
const
|
|
259
|
+
const queuedIssues = result.issues
|
|
260
|
+
.map((issue) => buildQueuedJiraIssueState(issue))
|
|
261
|
+
.filter((issue) => shouldDisplayIssueInState(issue));
|
|
262
|
+
state.issueCount = result.total;
|
|
263
|
+
state.issues = queuedIssues;
|
|
264
|
+
await writeState(runtimePaths.stateFile, state);
|
|
259
265
|
for (const issue of result.issues) {
|
|
266
|
+
state.pollerCurrent.jira = String(issue?.key || "");
|
|
267
|
+
await writeState(runtimePaths.stateFile, state);
|
|
260
268
|
const processed = await processJiraIssue({
|
|
261
269
|
issue,
|
|
262
270
|
jira,
|
|
@@ -267,15 +275,17 @@ export async function runWorkerCommand({ projectRoot }) {
|
|
|
267
275
|
runtimePaths,
|
|
268
276
|
logger
|
|
269
277
|
});
|
|
270
|
-
|
|
278
|
+
reconcileQueueEntry(state.issues, processed, (entry) => entry?.key === issue.key, shouldDisplayIssueInState);
|
|
279
|
+
await writeState(runtimePaths.stateFile, state);
|
|
271
280
|
}
|
|
272
|
-
|
|
281
|
+
state.pollerCurrent.jira = "";
|
|
273
282
|
return {
|
|
274
|
-
stateUpdates: { issueCount: result.total, issues:
|
|
283
|
+
stateUpdates: { issueCount: result.total, issues: state.issues },
|
|
275
284
|
logInfo: { issues: result.issues.length, assigned: result.total }
|
|
276
285
|
};
|
|
277
286
|
}, { stateFile: runtimePaths.stateFile, logger, flagRef: jiraFlag });
|
|
278
287
|
state.pollerBusy.jira = false;
|
|
288
|
+
state.pollerCurrent.jira = "";
|
|
279
289
|
await writeState(runtimePaths.stateFile, state);
|
|
280
290
|
};
|
|
281
291
|
|
|
@@ -289,7 +299,6 @@ export async function runWorkerCommand({ projectRoot }) {
|
|
|
289
299
|
await runPoll("GitHub", "GitHub", state, async () => {
|
|
290
300
|
const repos = filterReposForActiveForge(await listKnownRepos(projectRoot), forgeRegistry);
|
|
291
301
|
const reposByUrl = new Map(repos.map((repo) => [repo.url, repo]));
|
|
292
|
-
const processedIssues = [];
|
|
293
302
|
let trackedIssueCount = 0;
|
|
294
303
|
const activeClient = forgeRegistry.getActiveClient();
|
|
295
304
|
if (activeClient?.id === "github") {
|
|
@@ -302,9 +311,16 @@ export async function runWorkerCommand({ projectRoot }) {
|
|
|
302
311
|
authorLogin: botUser.login || ""
|
|
303
312
|
});
|
|
304
313
|
trackedIssueCount = issues.length;
|
|
314
|
+
state.githubIssueCount = trackedIssueCount;
|
|
315
|
+
state.githubIssues = issues.map(buildQueuedTrackedIssueState);
|
|
316
|
+
await writeState(runtimePaths.stateFile, state);
|
|
305
317
|
for (const issue of issues) {
|
|
318
|
+
state.pollerCurrent.github = buildIssueQueueId(issue.repoUrl, issue.number);
|
|
319
|
+
await writeState(runtimePaths.stateFile, state);
|
|
306
320
|
const repo = reposByUrl.get(issue.repoUrl);
|
|
307
321
|
if (!repo) {
|
|
322
|
+
reconcileQueueEntry(state.githubIssues, null, (entry) => buildIssueQueueId(entry.repoUrl, entry.issueNumber) === buildIssueQueueId(issue.repoUrl, issue.number), () => false);
|
|
323
|
+
await writeState(runtimePaths.stateFile, state);
|
|
308
324
|
continue;
|
|
309
325
|
}
|
|
310
326
|
const provider = forgeRegistry.getClientForRepo(repo.url);
|
|
@@ -325,8 +341,9 @@ export async function runWorkerCommand({ projectRoot }) {
|
|
|
325
341
|
logger
|
|
326
342
|
});
|
|
327
343
|
if (processed) {
|
|
328
|
-
|
|
344
|
+
reconcileQueueEntry(state.githubIssues, processed, (entry) => buildIssueQueueId(entry.repoUrl, entry.issueNumber) === buildIssueQueueId(issue.repoUrl, issue.number), () => true);
|
|
329
345
|
}
|
|
346
|
+
await writeState(runtimePaths.stateFile, state);
|
|
330
347
|
}
|
|
331
348
|
} else if (activeClient?.id === "gitlab") {
|
|
332
349
|
const firstRepoUrl = repos[0]?.url || "";
|
|
@@ -339,9 +356,16 @@ export async function runWorkerCommand({ projectRoot }) {
|
|
|
339
356
|
authorLogin: botUser.login || ""
|
|
340
357
|
});
|
|
341
358
|
trackedIssueCount = issues.length;
|
|
359
|
+
state.githubIssueCount = trackedIssueCount;
|
|
360
|
+
state.githubIssues = issues.map(buildQueuedTrackedIssueState);
|
|
361
|
+
await writeState(runtimePaths.stateFile, state);
|
|
342
362
|
for (const issue of issues) {
|
|
363
|
+
state.pollerCurrent.github = buildIssueQueueId(issue.repoUrl, issue.number);
|
|
364
|
+
await writeState(runtimePaths.stateFile, state);
|
|
343
365
|
const repo = reposByUrl.get(issue.repoUrl);
|
|
344
366
|
if (!repo) {
|
|
367
|
+
reconcileQueueEntry(state.githubIssues, null, (entry) => buildIssueQueueId(entry.repoUrl, entry.issueNumber) === buildIssueQueueId(issue.repoUrl, issue.number), () => false);
|
|
368
|
+
await writeState(runtimePaths.stateFile, state);
|
|
345
369
|
continue;
|
|
346
370
|
}
|
|
347
371
|
const provider = forgeRegistry.getClientForRepo(repo.url);
|
|
@@ -362,10 +386,14 @@ export async function runWorkerCommand({ projectRoot }) {
|
|
|
362
386
|
logger
|
|
363
387
|
});
|
|
364
388
|
if (processed) {
|
|
365
|
-
|
|
389
|
+
reconcileQueueEntry(state.githubIssues, processed, (entry) => buildIssueQueueId(entry.repoUrl, entry.issueNumber) === buildIssueQueueId(issue.repoUrl, issue.number), () => true);
|
|
366
390
|
}
|
|
391
|
+
await writeState(runtimePaths.stateFile, state);
|
|
367
392
|
}
|
|
368
393
|
} else {
|
|
394
|
+
state.githubIssues = [];
|
|
395
|
+
state.githubIssueCount = 0;
|
|
396
|
+
await writeState(runtimePaths.stateFile, state);
|
|
369
397
|
for (const repo of repos) {
|
|
370
398
|
const provider = forgeRegistry.getClientForRepo(repo.url);
|
|
371
399
|
const botUser = await getForgeBotUserForRepo(forgeRegistry, forgeBotUsers, repo.url, logger);
|
|
@@ -380,7 +408,14 @@ export async function runWorkerCommand({ projectRoot }) {
|
|
|
380
408
|
});
|
|
381
409
|
const botIssues = issues.filter((issue) => isForgeBotAuthor(issue.author, botUser));
|
|
382
410
|
trackedIssueCount += botIssues.length;
|
|
411
|
+
state.githubIssueCount = trackedIssueCount;
|
|
412
|
+
state.githubIssues.push(...botIssues.map(buildQueuedTrackedIssueState).filter((candidate) =>
|
|
413
|
+
!state.githubIssues.some((entry) => buildIssueQueueId(entry.repoUrl, entry.issueNumber) === buildIssueQueueId(candidate.repoUrl, candidate.issueNumber))
|
|
414
|
+
));
|
|
415
|
+
await writeState(runtimePaths.stateFile, state);
|
|
383
416
|
for (const issue of botIssues) {
|
|
417
|
+
state.pollerCurrent.github = buildIssueQueueId(issue.repoUrl, issue.number);
|
|
418
|
+
await writeState(runtimePaths.stateFile, state);
|
|
384
419
|
const processed = await processGitHubIssue({
|
|
385
420
|
repo,
|
|
386
421
|
issue,
|
|
@@ -393,17 +428,20 @@ export async function runWorkerCommand({ projectRoot }) {
|
|
|
393
428
|
logger
|
|
394
429
|
});
|
|
395
430
|
if (processed) {
|
|
396
|
-
|
|
431
|
+
reconcileQueueEntry(state.githubIssues, processed, (entry) => buildIssueQueueId(entry.repoUrl, entry.issueNumber) === buildIssueQueueId(issue.repoUrl, issue.number), () => true);
|
|
397
432
|
}
|
|
433
|
+
await writeState(runtimePaths.stateFile, state);
|
|
398
434
|
}
|
|
399
435
|
}
|
|
400
436
|
}
|
|
437
|
+
state.pollerCurrent.github = "";
|
|
401
438
|
return {
|
|
402
|
-
stateUpdates: { githubIssueCount: trackedIssueCount, githubIssues:
|
|
439
|
+
stateUpdates: { githubIssueCount: trackedIssueCount, githubIssues: state.githubIssues },
|
|
403
440
|
logInfo: { tracked: trackedIssueCount }
|
|
404
441
|
};
|
|
405
442
|
}, { stateFile: runtimePaths.stateFile, logger, flagRef: githubFlag });
|
|
406
443
|
state.pollerBusy.github = false;
|
|
444
|
+
state.pollerCurrent.github = "";
|
|
407
445
|
await writeState(runtimePaths.stateFile, state);
|
|
408
446
|
};
|
|
409
447
|
|
|
@@ -418,7 +456,6 @@ export async function runWorkerCommand({ projectRoot }) {
|
|
|
418
456
|
await runPoll("Pr", "Pull request", state, async () => {
|
|
419
457
|
const repos = filterReposForActiveForge(await listKnownRepos(projectRoot), forgeRegistry);
|
|
420
458
|
const reposByUrl = new Map(repos.map((repo) => [repo.url, repo]));
|
|
421
|
-
const processedPrs = [];
|
|
422
459
|
let trackedPrCount = 0;
|
|
423
460
|
const activeClient = forgeRegistry.getActiveClient();
|
|
424
461
|
if (activeClient?.id === "github") {
|
|
@@ -431,9 +468,16 @@ export async function runWorkerCommand({ projectRoot }) {
|
|
|
431
468
|
authorLogin: botUser.login || ""
|
|
432
469
|
});
|
|
433
470
|
trackedPrCount = pullRequests.length;
|
|
471
|
+
state.draftPrCount = trackedPrCount;
|
|
472
|
+
state.draftPrs = pullRequests.map(buildQueuedTrackedPullRequestState);
|
|
473
|
+
await writeState(runtimePaths.stateFile, state);
|
|
434
474
|
for (const pullRequest of pullRequests) {
|
|
475
|
+
state.pollerCurrent.draftPr = buildPullRequestQueueId(pullRequest.repoUrl, pullRequest.number);
|
|
476
|
+
await writeState(runtimePaths.stateFile, state);
|
|
435
477
|
const repo = reposByUrl.get(pullRequest.repoUrl);
|
|
436
478
|
if (!repo) {
|
|
479
|
+
reconcileQueueEntry(state.draftPrs, null, (entry) => buildPullRequestQueueId(entry.repoUrl, entry.pullRequestNumber) === buildPullRequestQueueId(pullRequest.repoUrl, pullRequest.number), () => false);
|
|
480
|
+
await writeState(runtimePaths.stateFile, state);
|
|
437
481
|
continue;
|
|
438
482
|
}
|
|
439
483
|
const provider = forgeRegistry.getClientForRepo(repo.url);
|
|
@@ -456,8 +500,9 @@ export async function runWorkerCommand({ projectRoot }) {
|
|
|
456
500
|
jiraBotUser
|
|
457
501
|
});
|
|
458
502
|
if (processed) {
|
|
459
|
-
|
|
503
|
+
reconcileQueueEntry(state.draftPrs, processed, (entry) => buildPullRequestQueueId(entry.repoUrl, entry.pullRequestNumber) === buildPullRequestQueueId(pullRequest.repoUrl, pullRequest.number), shouldKeepTrackedPullRequestState);
|
|
460
504
|
}
|
|
505
|
+
await writeState(runtimePaths.stateFile, state);
|
|
461
506
|
}
|
|
462
507
|
} else if (activeClient?.id === "gitlab") {
|
|
463
508
|
const firstRepoUrl = repos[0]?.url || "";
|
|
@@ -470,9 +515,16 @@ export async function runWorkerCommand({ projectRoot }) {
|
|
|
470
515
|
authorLogin: botUser.login || ""
|
|
471
516
|
});
|
|
472
517
|
trackedPrCount = pullRequests.length;
|
|
518
|
+
state.draftPrCount = trackedPrCount;
|
|
519
|
+
state.draftPrs = pullRequests.map(buildQueuedTrackedPullRequestState);
|
|
520
|
+
await writeState(runtimePaths.stateFile, state);
|
|
473
521
|
for (const pullRequest of pullRequests) {
|
|
522
|
+
state.pollerCurrent.draftPr = buildPullRequestQueueId(pullRequest.repoUrl, pullRequest.number);
|
|
523
|
+
await writeState(runtimePaths.stateFile, state);
|
|
474
524
|
const repo = reposByUrl.get(pullRequest.repoUrl);
|
|
475
525
|
if (!repo) {
|
|
526
|
+
reconcileQueueEntry(state.draftPrs, null, (entry) => buildPullRequestQueueId(entry.repoUrl, entry.pullRequestNumber) === buildPullRequestQueueId(pullRequest.repoUrl, pullRequest.number), () => false);
|
|
527
|
+
await writeState(runtimePaths.stateFile, state);
|
|
476
528
|
continue;
|
|
477
529
|
}
|
|
478
530
|
const provider = forgeRegistry.getClientForRepo(repo.url);
|
|
@@ -495,10 +547,14 @@ export async function runWorkerCommand({ projectRoot }) {
|
|
|
495
547
|
jiraBotUser
|
|
496
548
|
});
|
|
497
549
|
if (processed) {
|
|
498
|
-
|
|
550
|
+
reconcileQueueEntry(state.draftPrs, processed, (entry) => buildPullRequestQueueId(entry.repoUrl, entry.pullRequestNumber) === buildPullRequestQueueId(pullRequest.repoUrl, pullRequest.number), shouldKeepTrackedPullRequestState);
|
|
499
551
|
}
|
|
552
|
+
await writeState(runtimePaths.stateFile, state);
|
|
500
553
|
}
|
|
501
554
|
} else {
|
|
555
|
+
state.draftPrs = [];
|
|
556
|
+
state.draftPrCount = 0;
|
|
557
|
+
await writeState(runtimePaths.stateFile, state);
|
|
502
558
|
for (const repo of repos) {
|
|
503
559
|
const provider = forgeRegistry.getClientForRepo(repo.url);
|
|
504
560
|
const botUser = await getForgeBotUserForRepo(forgeRegistry, forgeBotUsers, repo.url, logger);
|
|
@@ -515,7 +571,14 @@ export async function runWorkerCommand({ projectRoot }) {
|
|
|
515
571
|
(pullRequest) => isForgeBotAuthor(pullRequest.author, botUser)
|
|
516
572
|
);
|
|
517
573
|
trackedPrCount += botPullRequests.length;
|
|
574
|
+
state.draftPrCount = trackedPrCount;
|
|
575
|
+
state.draftPrs.push(...botPullRequests.map(buildQueuedTrackedPullRequestState).filter((candidate) =>
|
|
576
|
+
!state.draftPrs.some((entry) => buildPullRequestQueueId(entry.repoUrl, entry.pullRequestNumber) === buildPullRequestQueueId(candidate.repoUrl, candidate.pullRequestNumber))
|
|
577
|
+
));
|
|
578
|
+
await writeState(runtimePaths.stateFile, state);
|
|
518
579
|
for (const pullRequest of botPullRequests) {
|
|
580
|
+
state.pollerCurrent.draftPr = buildPullRequestQueueId(pullRequest.repoUrl, pullRequest.number);
|
|
581
|
+
await writeState(runtimePaths.stateFile, state);
|
|
519
582
|
const processed = await processTrackedPullRequest({
|
|
520
583
|
projectRoot,
|
|
521
584
|
runtimePaths,
|
|
@@ -530,17 +593,20 @@ export async function runWorkerCommand({ projectRoot }) {
|
|
|
530
593
|
jiraBotUser
|
|
531
594
|
});
|
|
532
595
|
if (processed) {
|
|
533
|
-
|
|
596
|
+
reconcileQueueEntry(state.draftPrs, processed, (entry) => buildPullRequestQueueId(entry.repoUrl, entry.pullRequestNumber) === buildPullRequestQueueId(pullRequest.repoUrl, pullRequest.number), shouldKeepTrackedPullRequestState);
|
|
534
597
|
}
|
|
598
|
+
await writeState(runtimePaths.stateFile, state);
|
|
535
599
|
}
|
|
536
600
|
}
|
|
537
601
|
}
|
|
602
|
+
state.pollerCurrent.draftPr = "";
|
|
538
603
|
return {
|
|
539
|
-
stateUpdates: { draftPrCount: trackedPrCount, draftPrs:
|
|
604
|
+
stateUpdates: { draftPrCount: trackedPrCount, draftPrs: state.draftPrs },
|
|
540
605
|
logInfo: { tracked: trackedPrCount }
|
|
541
606
|
};
|
|
542
607
|
}, { stateFile: runtimePaths.stateFile, logger, flagRef: prFlag });
|
|
543
608
|
state.pollerBusy.draftPr = false;
|
|
609
|
+
state.pollerCurrent.draftPr = "";
|
|
544
610
|
await writeState(runtimePaths.stateFile, state);
|
|
545
611
|
};
|
|
546
612
|
|
|
@@ -558,13 +624,20 @@ export async function runWorkerCommand({ projectRoot }) {
|
|
|
558
624
|
repoUrls: repos.map((repo) => repo.url)
|
|
559
625
|
});
|
|
560
626
|
let reviewedPrCount = prs.length;
|
|
627
|
+
state.reviewPrCount = reviewedPrCount;
|
|
628
|
+
state.reviewPrs = prs.map(buildQueuedReviewPullRequestState);
|
|
629
|
+
await writeState(runtimePaths.stateFile, state);
|
|
561
630
|
for (const pr of prs) {
|
|
631
|
+
state.pollerCurrent.reviewPr = buildPullRequestQueueId(pr.repoUrl, pr.number);
|
|
632
|
+
await writeState(runtimePaths.stateFile, state);
|
|
562
633
|
if (!pr.repoUrl) {
|
|
563
634
|
await logger.error("PR review skipped because repository URL is missing", {
|
|
564
635
|
pr: pr.number,
|
|
565
636
|
title: pr.title
|
|
566
637
|
});
|
|
567
638
|
reviewedPrCount -= 1;
|
|
639
|
+
reconcileQueueEntry(state.reviewPrs, null, (entry) => buildPullRequestQueueId(entry.repoUrl, entry.pullRequestNumber) === buildPullRequestQueueId(pr.repoUrl, pr.number), () => false);
|
|
640
|
+
await writeState(runtimePaths.stateFile, state);
|
|
568
641
|
continue;
|
|
569
642
|
}
|
|
570
643
|
const provider = forgeRegistry.getClientForRepo(pr.repoUrl);
|
|
@@ -579,8 +652,21 @@ export async function runWorkerCommand({ projectRoot }) {
|
|
|
579
652
|
repoUrl: pr.repoUrl,
|
|
580
653
|
pullRequestNumber: String(pr.number),
|
|
581
654
|
pullRequestUrl: prDetail.url,
|
|
655
|
+
title: prDetail.title || pr.title || "",
|
|
582
656
|
suggestionsCount: result.suggestions.length
|
|
583
657
|
});
|
|
658
|
+
reconcileQueueEntry(
|
|
659
|
+
state.reviewPrs,
|
|
660
|
+
{
|
|
661
|
+
repoUrl: pr.repoUrl,
|
|
662
|
+
pullRequestNumber: String(pr.number),
|
|
663
|
+
pullRequestUrl: prDetail.url,
|
|
664
|
+
title: prDetail.title || pr.title || "",
|
|
665
|
+
suggestionsCount: result.suggestions.length
|
|
666
|
+
},
|
|
667
|
+
(entry) => buildPullRequestQueueId(entry.repoUrl, entry.pullRequestNumber) === buildPullRequestQueueId(pr.repoUrl, pr.number),
|
|
668
|
+
() => true
|
|
669
|
+
);
|
|
584
670
|
await logger.info("PR review submitted", {
|
|
585
671
|
repo: pr.repoUrl,
|
|
586
672
|
pr: pr.number,
|
|
@@ -603,13 +689,16 @@ export async function runWorkerCommand({ projectRoot }) {
|
|
|
603
689
|
});
|
|
604
690
|
}
|
|
605
691
|
}
|
|
692
|
+
await writeState(runtimePaths.stateFile, state);
|
|
606
693
|
}
|
|
694
|
+
state.pollerCurrent.reviewPr = "";
|
|
607
695
|
return {
|
|
608
|
-
stateUpdates: { reviewPrCount: reviewedPrCount, reviewPrs:
|
|
696
|
+
stateUpdates: { reviewPrCount: reviewedPrCount, reviewPrs: state.reviewPrs },
|
|
609
697
|
logInfo: { reviewed: reviewedPrCount }
|
|
610
698
|
};
|
|
611
699
|
}, { stateFile: runtimePaths.stateFile, logger, flagRef: reviewFlag });
|
|
612
700
|
state.pollerBusy.reviewPr = false;
|
|
701
|
+
state.pollerCurrent.reviewPr = "";
|
|
613
702
|
await writeState(runtimePaths.stateFile, state);
|
|
614
703
|
};
|
|
615
704
|
|
|
@@ -3274,6 +3363,125 @@ function buildDefaultIssueMemory(workflowState) {
|
|
|
3274
3363
|
};
|
|
3275
3364
|
}
|
|
3276
3365
|
|
|
3366
|
+
function buildQueuedJiraIssueState(issue) {
|
|
3367
|
+
const live = deriveJiraLiveState({
|
|
3368
|
+
issue,
|
|
3369
|
+
issueMemory: { github_issues: [] },
|
|
3370
|
+
githubIssues: [],
|
|
3371
|
+
pullRequests: []
|
|
3372
|
+
});
|
|
3373
|
+
|
|
3374
|
+
return {
|
|
3375
|
+
key: issue.key,
|
|
3376
|
+
status: issue.status,
|
|
3377
|
+
labels: normalizeLabels(issue.labels),
|
|
3378
|
+
projectKey: issue.projectKey,
|
|
3379
|
+
workflowState: live.phase,
|
|
3380
|
+
blocker: live.blocker,
|
|
3381
|
+
nextAction: live.nextAction,
|
|
3382
|
+
repoCount: 0,
|
|
3383
|
+
repoUrls: [],
|
|
3384
|
+
localRepoPaths: [],
|
|
3385
|
+
githubIssueUrls: [],
|
|
3386
|
+
claudeDecision: null,
|
|
3387
|
+
lastError: null,
|
|
3388
|
+
sourceOfTruth: live.sourceOfTruth
|
|
3389
|
+
};
|
|
3390
|
+
}
|
|
3391
|
+
|
|
3392
|
+
function buildQueuedTrackedIssueState(issue) {
|
|
3393
|
+
const live = deriveForgeIssueLiveState({
|
|
3394
|
+
repoUrl: issue.repoUrl,
|
|
3395
|
+
issueNumber: issue.number,
|
|
3396
|
+
issueUrl: issue.url,
|
|
3397
|
+
state: issue.state,
|
|
3398
|
+
labels: issue.labels
|
|
3399
|
+
});
|
|
3400
|
+
|
|
3401
|
+
return {
|
|
3402
|
+
repoUrl: issue.repoUrl,
|
|
3403
|
+
issueNumber: String(issue.number),
|
|
3404
|
+
issueUrl: issue.url,
|
|
3405
|
+
title: issue.title || "",
|
|
3406
|
+
state: issue.state || "",
|
|
3407
|
+
labels: normalizeLabels(issue.labels),
|
|
3408
|
+
workflowState: live.phase,
|
|
3409
|
+
branchName: "",
|
|
3410
|
+
prUrl: "",
|
|
3411
|
+
action: "queued"
|
|
3412
|
+
};
|
|
3413
|
+
}
|
|
3414
|
+
|
|
3415
|
+
function buildQueuedTrackedPullRequestState(pullRequest) {
|
|
3416
|
+
const repoUrl = String(pullRequest.repoUrl || pullRequest.url || "")
|
|
3417
|
+
.replace(/\/pull\/\d+$/u, "")
|
|
3418
|
+
.replace(/\/-\/merge_requests\/\d+$/u, "");
|
|
3419
|
+
const live = derivePullRequestLiveState({
|
|
3420
|
+
...pullRequest,
|
|
3421
|
+
repoUrl,
|
|
3422
|
+
pullRequestNumber: pullRequest.number,
|
|
3423
|
+
pullRequestUrl: pullRequest.url,
|
|
3424
|
+
status: getPullRequestStatus(pullRequest.body || "")
|
|
3425
|
+
});
|
|
3426
|
+
|
|
3427
|
+
return {
|
|
3428
|
+
repoUrl,
|
|
3429
|
+
pullRequestNumber: String(pullRequest.number),
|
|
3430
|
+
pullRequestUrl: pullRequest.url,
|
|
3431
|
+
title: pullRequest.title || "",
|
|
3432
|
+
branchName: pullRequest.headRef || "",
|
|
3433
|
+
status: getPullRequestStatus(pullRequest.body || ""),
|
|
3434
|
+
labels: normalizeLabels(pullRequest.labels),
|
|
3435
|
+
workflowState: live.phase,
|
|
3436
|
+
action: "queued"
|
|
3437
|
+
};
|
|
3438
|
+
}
|
|
3439
|
+
|
|
3440
|
+
function buildQueuedReviewPullRequestState(pullRequest) {
|
|
3441
|
+
return {
|
|
3442
|
+
repoUrl: pullRequest.repoUrl || "",
|
|
3443
|
+
pullRequestNumber: String(pullRequest.number || ""),
|
|
3444
|
+
pullRequestUrl: pullRequest.pullRequestUrl || pullRequest.url || "",
|
|
3445
|
+
title: pullRequest.title || "",
|
|
3446
|
+
suggestionsCount: null
|
|
3447
|
+
};
|
|
3448
|
+
}
|
|
3449
|
+
|
|
3450
|
+
function buildIssueQueueId(repoUrl, issueNumber) {
|
|
3451
|
+
return `${String(repoUrl || "").trim()}#${String(issueNumber || "").trim()}`;
|
|
3452
|
+
}
|
|
3453
|
+
|
|
3454
|
+
function buildPullRequestQueueId(repoUrl, pullRequestNumber) {
|
|
3455
|
+
return `${String(repoUrl || "").trim()}!${String(pullRequestNumber || "").trim()}`;
|
|
3456
|
+
}
|
|
3457
|
+
|
|
3458
|
+
function reconcileQueueEntry(queue, nextValue, matcher, keepPredicate = () => true) {
|
|
3459
|
+
if (!Array.isArray(queue)) {
|
|
3460
|
+
return;
|
|
3461
|
+
}
|
|
3462
|
+
|
|
3463
|
+
const index = queue.findIndex(matcher);
|
|
3464
|
+
const shouldKeep = nextValue && keepPredicate(nextValue);
|
|
3465
|
+
|
|
3466
|
+
if (index === -1) {
|
|
3467
|
+
if (shouldKeep) {
|
|
3468
|
+
queue.push(nextValue);
|
|
3469
|
+
}
|
|
3470
|
+
return;
|
|
3471
|
+
}
|
|
3472
|
+
|
|
3473
|
+
if (!shouldKeep) {
|
|
3474
|
+
queue.splice(index, 1);
|
|
3475
|
+
return;
|
|
3476
|
+
}
|
|
3477
|
+
|
|
3478
|
+
queue[index] = nextValue;
|
|
3479
|
+
}
|
|
3480
|
+
|
|
3481
|
+
function shouldKeepTrackedPullRequestState(pullRequest) {
|
|
3482
|
+
return String(pullRequest?.workflowState || "").trim() !== "done";
|
|
3483
|
+
}
|
|
3484
|
+
|
|
3277
3485
|
async function fetchLinkedForgeState({ issueMemory, forgeRegistry, logger }) {
|
|
3278
3486
|
const linkedSource = {
|
|
3279
3487
|
githubIssues: [],
|
package/src/dashboard/ui.html
CHANGED
|
@@ -1947,10 +1947,16 @@ function renderHeartbeatQueues(data) {
|
|
|
1947
1947
|
const wrap = document.getElementById("heartbeat-queues-wrap");
|
|
1948
1948
|
const s = data.state || {};
|
|
1949
1949
|
const busy = s.pollerBusy || {};
|
|
1950
|
+
const current = s.pollerCurrent || {};
|
|
1950
1951
|
const forgeRepoName = (url) => {
|
|
1951
1952
|
try {
|
|
1952
1953
|
const u = new URL(url || "");
|
|
1953
|
-
const parts = u.pathname
|
|
1954
|
+
const parts = u.pathname
|
|
1955
|
+
.replace(/\/pull\/\d+$/u, "")
|
|
1956
|
+
.replace(/\/issues\/\d+$/u, "")
|
|
1957
|
+
.replace(/\/-\/merge_requests\/\d+$/u, "")
|
|
1958
|
+
.split("/")
|
|
1959
|
+
.filter(Boolean);
|
|
1954
1960
|
return parts[parts.length - 1] || "repo";
|
|
1955
1961
|
} catch {
|
|
1956
1962
|
return "repo";
|
|
@@ -1964,8 +1970,18 @@ function renderHeartbeatQueues(data) {
|
|
|
1964
1970
|
{ label: "Review PRs", items: s.reviewPrs || [], busyKey: "reviewPr", type: "pr" }
|
|
1965
1971
|
];
|
|
1966
1972
|
|
|
1967
|
-
const
|
|
1968
|
-
|
|
1973
|
+
const itemQueueId = (item, type) => {
|
|
1974
|
+
if (type === "jira") {
|
|
1975
|
+
return String(item.key || "");
|
|
1976
|
+
}
|
|
1977
|
+
if (type === "github") {
|
|
1978
|
+
return `${String(item.repoUrl || "").trim()}#${String(item.issueNumber || "").trim()}`;
|
|
1979
|
+
}
|
|
1980
|
+
return `${String(item.repoUrl || "").trim()}!${String(item.pullRequestNumber || "").trim()}`;
|
|
1981
|
+
};
|
|
1982
|
+
|
|
1983
|
+
const renderCard = (item, isCurrent, type) => {
|
|
1984
|
+
const activeClass = isCurrent ? " active" : "";
|
|
1969
1985
|
if (type === "jira") {
|
|
1970
1986
|
const key = esc(item.key || "");
|
|
1971
1987
|
const onclick = item.key
|
|
@@ -1993,8 +2009,9 @@ function renderHeartbeatQueues(data) {
|
|
|
1993
2009
|
<div class="hbq-body">
|
|
1994
2010
|
${rows.map(row => {
|
|
1995
2011
|
const isActive = !!busy[row.busyKey];
|
|
2012
|
+
const currentId = String(current[row.busyKey] || "");
|
|
1996
2013
|
const cards = row.items.length > 0
|
|
1997
|
-
? row.items.map((item
|
|
2014
|
+
? row.items.map((item) => renderCard(item, isActive && itemQueueId(item, row.type) === currentId, row.type)).join("")
|
|
1998
2015
|
: `<span class="hbq-idle">idle</span>`;
|
|
1999
2016
|
return `
|
|
2000
2017
|
<div class="hbq-row">
|