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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-teammate",
3
- "version": "0.1.152",
3
+ "version": "0.1.154",
4
4
  "description": "CLI bootstrapper for Claude Teammate.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -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
+ }
@@ -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 processedIssues = [];
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
- processedIssues.push(processed);
278
+ reconcileQueueEntry(state.issues, processed, (entry) => entry?.key === issue.key, shouldDisplayIssueInState);
279
+ await writeState(runtimePaths.stateFile, state);
271
280
  }
272
- const activeIssues = processedIssues.filter((issue) => shouldDisplayIssueInState(issue));
281
+ state.pollerCurrent.jira = "";
273
282
  return {
274
- stateUpdates: { issueCount: result.total, issues: activeIssues },
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
- processedIssues.push(processed);
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
- processedIssues.push(processed);
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
- processedIssues.push(processed);
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: processedIssues.slice(0, 20) },
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
- processedPrs.push(processed);
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
- processedPrs.push(processed);
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
- processedPrs.push(processed);
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: processedPrs.slice(0, 20) },
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: processedPrs.slice(0, 20) },
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: [],
@@ -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.split("/").filter(Boolean);
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 renderCard = (item, isActive, type, idx) => {
1968
- const activeClass = isActive && idx === 0 ? " active" : "";
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, idx) => renderCard(item, isActive, row.type, idx)).join("")
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">