claude-teammate 0.1.149 → 0.1.151

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.149",
3
+ "version": "0.1.151",
4
4
  "description": "CLI bootstrapper for Claude Teammate.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -66,7 +66,7 @@ export async function runStatusCommand({ projectRoot, args = [] }) {
66
66
  process.stdout.write("GitHub issues:\n");
67
67
  for (const issue of state.githubIssues) {
68
68
  process.stdout.write(
69
- `- ${issue.repoUrl}#${issue.issueNumber}${issue.prUrl ? ` | PR: ${issue.prUrl}` : ""}${issue.branchName ? ` | branch: ${issue.branchName}` : ""}${issue.lastCommentAction ? ` | action: ${issue.lastCommentAction}` : ""}\n`
69
+ `- ${formatForgeQueueLabel(issue.repoUrl, issue.issueNumber)}${issue.title ? ` | ${issue.title}` : ""}${issue.workflowState ? ` | ${issue.workflowState}` : ""}${issue.prUrl ? ` | PR: ${issue.prUrl}` : ""}${issue.branchName ? ` | branch: ${issue.branchName}` : ""}${issue.action ? ` | action: ${issue.action}` : ""}\n`
70
70
  );
71
71
  }
72
72
  }
@@ -75,9 +75,20 @@ export async function runStatusCommand({ projectRoot, args = [] }) {
75
75
  process.stdout.write("Draft PRs:\n");
76
76
  for (const pullRequest of state.draftPrs) {
77
77
  process.stdout.write(
78
- `- ${pullRequest.pullRequestUrl}${pullRequest.branchName ? ` | branch: ${pullRequest.branchName}` : ""}${pullRequest.status ? ` | status: ${pullRequest.status}` : ""}${pullRequest.action ? ` | action: ${pullRequest.action}` : ""}\n`
78
+ `- ${formatForgeQueueLabel(pullRequest.repoUrl, pullRequest.pullRequestNumber)}${pullRequest.title ? ` | ${pullRequest.title}` : ""}${pullRequest.branchName ? ` | branch: ${pullRequest.branchName}` : ""}${pullRequest.status ? ` | status: ${pullRequest.status}` : ""}${pullRequest.action ? ` | action: ${pullRequest.action}` : ""}\n`
79
79
  );
80
80
  }
81
81
  }
82
82
  }
83
83
  }
84
+
85
+ function formatForgeQueueLabel(repoUrl, itemNumber) {
86
+ try {
87
+ const url = new URL(String(repoUrl || "").trim());
88
+ const parts = url.pathname.split("/").filter(Boolean);
89
+ const repoName = parts[parts.length - 1] || "repo";
90
+ return `${repoName} #${itemNumber}`;
91
+ } catch {
92
+ return `repo #${itemNumber}`;
93
+ }
94
+ }
@@ -711,7 +711,7 @@ async function processJiraIssue({ issue, jira, forgeRegistry, botUser, config, p
711
711
  let epicMemory = epicMemoryRecord.data;
712
712
  let issueMemory = issueMemoryRecord.data;
713
713
  let liveRepos = deriveTaskRepos(detail, epicMemory, forgeRegistry);
714
- const labels = normalizeLabels(detail.labels);
714
+ let labels = normalizeLabels(detail.labels);
715
715
 
716
716
  if (isDoneStatus(detail.status)) {
717
717
  return buildIssueState(detail, { repos: liveRepos }, issueMemory, null);
@@ -845,30 +845,44 @@ async function processJiraIssue({ issue, jira, forgeRegistry, botUser, config, p
845
845
  await saveProgress("Repository ready. Analyzing requirements...");
846
846
  }
847
847
 
848
+ const linkedSource = await fetchLinkedForgeState({
849
+ issueMemory,
850
+ forgeRegistry,
851
+ logger
852
+ });
853
+ await reconcileJiraWithLinkedForgeState({
854
+ detail,
855
+ jira,
856
+ linkedSource,
857
+ logger
858
+ });
859
+ labels = normalizeLabels(detail.labels);
860
+
848
861
  if (!isInProgressStatus(detail.status)) {
849
- return buildIssueState(detail, { repos: liveRepos }, issueMemory, null);
862
+ return buildIssueState(detail, { repos: liveRepos }, issueMemory, null, null, linkedSource);
850
863
  }
851
864
 
852
865
  if (labels.includes(OPERATIONAL_LABELS.waitingHuman)) {
853
866
  if (!hasNewHumanReplyWhileWaiting(detail, botUser, jira)) {
854
- return buildIssueState(detail, { repos: liveRepos }, issueMemory, null);
867
+ return buildIssueState(detail, { repos: liveRepos }, issueMemory, null, null, linkedSource);
855
868
  }
856
869
 
857
870
  await replaceJiraOperationalLabels(jira, detail, []);
871
+ labels = normalizeLabels(detail.labels);
858
872
  await logger.info("Human reply received for waiting task; resuming processing", {
859
873
  issue: detail.key
860
874
  });
861
875
  }
862
876
 
863
877
  if (labels.includes(OPERATIONAL_LABELS.waitingApproval)) {
864
- return buildIssueState(detail, { repos: liveRepos }, issueMemory, null);
878
+ return buildIssueState(detail, { repos: liveRepos }, issueMemory, null, null, linkedSource);
865
879
  }
866
880
 
867
881
  let claudeResult = null;
868
882
  const latestInput = getLatestClarificationInput(detail, botUser, jira);
869
883
  if (!latestInput) {
870
884
  await saveProgress("Task restarted. Please comment with any updated instructions or reply to continue.");
871
- return buildIssueState(detail, { repos: liveRepos }, issueMemory, null);
885
+ return buildIssueState(detail, { repos: liveRepos }, issueMemory, null, null, linkedSource);
872
886
  }
873
887
 
874
888
  const reopenContext = buildReopenContext({
@@ -881,7 +895,7 @@ async function processJiraIssue({ issue, jira, forgeRegistry, botUser, config, p
881
895
  if (isClaudeUsageLimited()) {
882
896
  await logger.info("Skipping Claude: usage limit active", { issue: detail.key, resumesAt: new Date(claudeUsageLimitUntil).toISOString() });
883
897
  await saveProgress(`Claude usage limit active. Will retry automatically at ${new Date(claudeUsageLimitUntil).toISOString().replace("T", " ").slice(0, 16)} UTC.`);
884
- return buildIssueState(detail, { repos: liveRepos }, issueMemory, null);
898
+ return buildIssueState(detail, { repos: liveRepos }, issueMemory, null, null, linkedSource);
885
899
  }
886
900
 
887
901
  await saveProgress("Analyzing requirements...");
@@ -2343,10 +2357,10 @@ async function continueGitHubIssueCreation({
2343
2357
  issueMemory.github_issues = dedupeGitHubIssues(issueMemory.github_issues);
2344
2358
  issueMemory = await saveIssueMemory(issueMemoryRecord.filePath, detail, issueMemory);
2345
2359
 
2346
- const successComment = `Created repository issue: [#${githubIssue.number}](${githubIssue.url})`;
2347
- if (!existingGitHubIssue) {
2348
- await ensureJiraComment(detail, jira, botUser, successComment);
2349
- }
2360
+ const successComment = existingGitHubIssue
2361
+ ? `Using existing repository issue: [#${githubIssue.number}](${githubIssue.url})`
2362
+ : `Created repository issue: [#${githubIssue.number}](${githubIssue.url})`;
2363
+ await ensureJiraComment(detail, jira, botUser, successComment);
2350
2364
  await replaceJiraOperationalLabels(jira, detail, [OPERATIONAL_LABELS.waitingApproval]);
2351
2365
  issueMemory.last_error = "";
2352
2366
  issueMemory = await saveIssueMemory(issueMemoryRecord.filePath, detail, issueMemory);
@@ -3219,6 +3233,7 @@ function buildGitHubIssueState(issue, githubIssueMemory, action) {
3219
3233
  repoUrl: githubIssueMemory.repo_url,
3220
3234
  issueNumber: String(issue.number),
3221
3235
  issueUrl: issue.url,
3236
+ title: issue.title || "",
3222
3237
  state: issue.state || "",
3223
3238
  labels: normalizeLabels(issue.labels),
3224
3239
  workflowState: live.phase,
@@ -3236,6 +3251,7 @@ function buildDraftPrState(pullRequest, status, action) {
3236
3251
  repoUrl,
3237
3252
  pullRequestNumber: String(pullRequest.number),
3238
3253
  pullRequestUrl: pullRequest.url,
3254
+ title: pullRequest.title || "",
3239
3255
  branchName: pullRequest.headRef || "",
3240
3256
  status,
3241
3257
  labels: normalizeLabels(pullRequest.labels),
@@ -3258,6 +3274,125 @@ function buildDefaultIssueMemory(workflowState) {
3258
3274
  };
3259
3275
  }
3260
3276
 
3277
+ async function fetchLinkedForgeState({ issueMemory, forgeRegistry, logger }) {
3278
+ const linkedSource = {
3279
+ githubIssues: [],
3280
+ pullRequests: []
3281
+ };
3282
+
3283
+ const entries = Array.isArray(issueMemory?.github_issues) ? issueMemory.github_issues : [];
3284
+ const seenIssues = new Set();
3285
+ const seenPullRequests = new Set();
3286
+
3287
+ for (const entry of entries) {
3288
+ const repoUrl = String(entry?.repo_url || "").trim();
3289
+ if (!repoUrl) {
3290
+ continue;
3291
+ }
3292
+
3293
+ let provider;
3294
+ try {
3295
+ provider = forgeRegistry.getClientForRepo(repoUrl);
3296
+ } catch (error) {
3297
+ await logger.error("Failed to resolve linked forge client", { repo: repoUrl, error });
3298
+ continue;
3299
+ }
3300
+
3301
+ const issueNumber = String(entry?.number || "").trim();
3302
+ if (issueNumber) {
3303
+ const issueKey = `${repoUrl}#${issueNumber}`;
3304
+ if (!seenIssues.has(issueKey)) {
3305
+ seenIssues.add(issueKey);
3306
+ try {
3307
+ const issue = await provider.fetchIssue(repoUrl, issueNumber);
3308
+ linkedSource.githubIssues.push({
3309
+ repoUrl,
3310
+ issueNumber: String(issue.number || issueNumber),
3311
+ issueUrl: issue.url || entry.url || "",
3312
+ title: issue.title || "",
3313
+ state: issue.state || "",
3314
+ labels: normalizeLabels(issue.labels)
3315
+ });
3316
+ } catch (error) {
3317
+ await logger.error("Failed to fetch linked forge issue", {
3318
+ repo: repoUrl,
3319
+ issue: issueNumber,
3320
+ error
3321
+ });
3322
+ }
3323
+ }
3324
+ }
3325
+
3326
+ const pullRequestNumber = String(entry?.pr_number || "").trim();
3327
+ if (pullRequestNumber) {
3328
+ const pullKey = `${repoUrl}!${pullRequestNumber}`;
3329
+ if (!seenPullRequests.has(pullKey)) {
3330
+ seenPullRequests.add(pullKey);
3331
+ try {
3332
+ const pullRequest = await provider.fetchPullRequest(repoUrl, pullRequestNumber);
3333
+ linkedSource.pullRequests.push({
3334
+ repoUrl,
3335
+ pullRequestNumber: String(pullRequest.number || pullRequestNumber),
3336
+ pullRequestUrl: pullRequest.url || entry.pr_url || "",
3337
+ title: pullRequest.title || "",
3338
+ state: pullRequest.state || "",
3339
+ draft: pullRequest.draft === true,
3340
+ labels: normalizeLabels(pullRequest.labels),
3341
+ status: getPullRequestStatus(pullRequest.body || "")
3342
+ });
3343
+ } catch (error) {
3344
+ await logger.error("Failed to fetch linked pull request", {
3345
+ repo: repoUrl,
3346
+ pr: pullRequestNumber,
3347
+ error
3348
+ });
3349
+ }
3350
+ }
3351
+ }
3352
+ }
3353
+
3354
+ return linkedSource;
3355
+ }
3356
+
3357
+ async function reconcileJiraWithLinkedForgeState({ detail, jira, linkedSource, logger }) {
3358
+ const pullRequests = Array.isArray(linkedSource?.pullRequests) ? linkedSource.pullRequests : [];
3359
+ const pullRequestStates = pullRequests.map((pullRequest) => derivePullRequestLiveState(pullRequest));
3360
+
3361
+ if (pullRequestStates.some((pullRequest) => pullRequest.phase === "done")) {
3362
+ if (!isDoneStatus(detail.status)) {
3363
+ const transition = await jira.transitionIssueToStatus(detail.key, "Done");
3364
+ if (transition.transitioned) {
3365
+ detail.status = "Done";
3366
+ }
3367
+ await logger.info("Linked pull request is complete; reconciling Jira to Done", {
3368
+ issue: detail.key,
3369
+ moved: transition.transitioned ? "yes" : "no"
3370
+ });
3371
+ }
3372
+ await replaceJiraOperationalLabels(jira, detail, []);
3373
+ return;
3374
+ }
3375
+
3376
+ if (pullRequestStates.length > 0 && normalizeLabels(detail.labels).includes(OPERATIONAL_LABELS.waitingApproval)) {
3377
+ await replaceJiraOperationalLabels(jira, detail, []);
3378
+ }
3379
+
3380
+ if (
3381
+ pullRequestStates.some((pullRequest) => pullRequest.phase === "in_pr_review") &&
3382
+ !isInReviewStatus(detail.status) &&
3383
+ !isDoneStatus(detail.status)
3384
+ ) {
3385
+ const transition = await jira.transitionIssueToStatus(detail.key, "In Review");
3386
+ if (transition.transitioned) {
3387
+ detail.status = "In Review";
3388
+ }
3389
+ await logger.info("Linked pull request entered review; reconciling Jira to In Review", {
3390
+ issue: detail.key,
3391
+ moved: transition.transitioned ? "yes" : "no"
3392
+ });
3393
+ }
3394
+ }
3395
+
3261
3396
  function buildPrSubtaskState(previous = {}) {
3262
3397
  return {
3263
3398
  phase: String(previous?.phase || "").trim(),
@@ -1947,10 +1947,19 @@ 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 forgeRepoName = (url) => {
1951
+ try {
1952
+ const u = new URL(url || "");
1953
+ const parts = u.pathname.split("/").filter(Boolean);
1954
+ return parts[parts.length - 1] || "repo";
1955
+ } catch {
1956
+ return "repo";
1957
+ }
1958
+ };
1950
1959
 
1951
1960
  const rows = [
1952
1961
  { label: "Jira", items: s.issues || [], busyKey: "jira", type: "jira" },
1953
- { label: "GitHub", items: s.githubIssues || [], busyKey: "github", type: "jira" },
1962
+ { label: "GitHub", items: s.githubIssues || [], busyKey: "github", type: "github" },
1954
1963
  { label: "Draft PRs", items: s.draftPrs || [], busyKey: "draftPr", type: "pr" },
1955
1964
  { label: "Review PRs", items: s.reviewPrs || [], busyKey: "reviewPr", type: "pr" }
1956
1965
  ];
@@ -1964,9 +1973,16 @@ function renderHeartbeatQueues(data) {
1964
1973
  : "";
1965
1974
  return `<div class="hbq-card${activeClass}" ${onclick}>${key}</div>`;
1966
1975
  }
1967
- const label = item.pullRequestNumber ? esc("#" + item.pullRequestNumber) : "PR";
1976
+ if (type === "github") {
1977
+ const label = esc(`${forgeRepoName(item.issueUrl || item.repoUrl)} ${item.issueNumber ? `#${item.issueNumber}` : "issue"}`);
1978
+ const href = item.issueUrl ? esc(item.issueUrl) : "#";
1979
+ const title = esc(item.title || label);
1980
+ return `<a class="hbq-card${activeClass}" href="${href}" target="_blank" style="text-decoration:none" title="${title}">${label}</a>`;
1981
+ }
1982
+ const label = esc(`${forgeRepoName(item.pullRequestUrl || item.repoUrl)} ${item.pullRequestNumber ? `#${item.pullRequestNumber}` : "PR"}`);
1968
1983
  const href = item.pullRequestUrl ? esc(item.pullRequestUrl) : "#";
1969
- return `<a class="hbq-card${activeClass}" href="${href}" target="_blank" style="text-decoration:none">${label}</a>`;
1984
+ const title = esc(item.title || label);
1985
+ return `<a class="hbq-card${activeClass}" href="${href}" target="_blank" style="text-decoration:none" title="${title}">${label}</a>`;
1970
1986
  };
1971
1987
 
1972
1988
  wrap.innerHTML = `