claude-teammate 0.1.312 → 0.1.314

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.312",
3
+ "version": "0.1.314",
4
4
  "description": "CLI bootstrapper for Claude Teammate.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -422,6 +422,8 @@ export function buildGitHubPRPlanBreakdownSystemPrompt() {
422
422
  "Each step must be self-contained: a single developer can implement it in isolation by reading only that step description.",
423
423
  "Steps must be ordered by dependency — steps that others depend on come first.",
424
424
  "Each step description must be specific enough to act on without reading the full PR body: name the exact files, classes, functions, or fields to change and what the change is.",
425
+ "Each step must describe a product code, configuration, schema, or test-code change in the target repository.",
426
+ "Do not create process-only steps such as opening a terminal, changing directories, locating files, reading files, running grep/ls/find, or using an editor.",
425
427
  "Aim for 3-8 steps. Do not produce trivial steps or steps that only say 'test' or 'verify'.",
426
428
  LANGUAGE_PRESERVATION_RULES,
427
429
  "Return only structured output matching the provided schema.",
@@ -444,9 +446,10 @@ ${JSON.stringify(input.memory?.epic || {}, null, 2)}
444
446
  Instructions:
445
447
  - Produce an ordered list of atomic implementation steps.
446
448
  - Each step must be specific enough to implement without reading this full description again: name the exact files, classes, methods, or enum values involved.
449
+ - Each step must describe a code/config/schema/test-code change, not an execution tactic or repository exploration action.
447
450
  - Steps must be ordered by dependency (earlier steps must not depend on later ones).
448
451
  - Aim for 3-8 steps covering the full scope of the plan.
449
- - Do not include steps for committing, pushing, or testing unless the test is part of the plan.`;
452
+ - Do not include steps for committing, pushing, opening a terminal, changing directories, locating files, reading files, running grep/ls/find, using an editor, or testing unless the test is part of the plan.`;
450
453
  }
451
454
 
452
455
  export function buildGitHubPRStepBreakdownSystemPrompt() {
@@ -455,6 +458,8 @@ export function buildGitHubPRStepBreakdownSystemPrompt() {
455
458
  "Each sub-step must be self-contained and implementable by reading only that sub-step description.",
456
459
  "Sub-steps must be ordered by dependency.",
457
460
  "Each sub-step must be specific: name the exact files, classes, functions, or fields to change.",
461
+ "Each sub-step must describe a code/config/schema/test-code change, not an execution tactic or repository exploration action.",
462
+ "Do not create process-only sub-steps such as opening a terminal, changing directories, locating files, reading files, running grep/ls/find, or using an editor.",
458
463
  "Do not produce trivial sub-steps.",
459
464
  LANGUAGE_PRESERVATION_RULES,
460
465
  "Return only structured output matching the provided schema."
@@ -473,8 +478,9 @@ ${input.step}
473
478
  Instructions:
474
479
  - Produce 2-4 ordered atomic sub-steps that together complete this step.
475
480
  - Each sub-step must be specific: name the exact files, classes, methods, or values to change.
481
+ - Each sub-step must describe a code/config/schema/test-code change, not an execution tactic or repository exploration action.
476
482
  - Sub-steps must be ordered by dependency.
477
- - Do not include steps for committing, pushing, or testing.`;
483
+ - Do not include steps for committing, pushing, opening a terminal, changing directories, locating files, reading files, running grep/ls/find, using an editor, or testing.`;
478
484
  }
479
485
 
480
486
  export function buildGitHubPRImplementationStepSystemPrompt() {
package/src/repo.js CHANGED
@@ -770,6 +770,31 @@ export async function branchHasChangesAgainstMain(repoPath, baseRef) {
770
770
  return Number.isFinite(count) && count > 0;
771
771
  }
772
772
 
773
+ /**
774
+ * True when HEAD's own work has already landed on the base branch (e.g. the
775
+ * branch was merged via another MR / a human merged it directly). In that case
776
+ * HEAD is an ancestor of the base tip while still being a distinct commit, so a
777
+ * branch-vs-base diff is empty and re-running Claude can never produce changes.
778
+ * Callers use this to terminate instead of looping on "no changes to commit".
779
+ * A brand-new branch that merely points at the base tip (HEAD === base) returns
780
+ * false — that is an unstarted PR, not a merged one. Best-effort; never throws.
781
+ */
782
+ export async function branchMergedIntoBase(repoPath, baseRef) {
783
+ const branch = baseRef || (await resolveRemoteDefaultBranch(repoPath));
784
+ try {
785
+ const headSha = (await execGitOutput(repoPath, ["rev-parse", "HEAD"])).trim();
786
+ const baseSha = (await execGitOutput(repoPath, ["rev-parse", `origin/${branch}`])).trim();
787
+ if (!headSha || !baseSha || headSha === baseSha) {
788
+ return false;
789
+ }
790
+ // exit 0 → HEAD is an ancestor of the base tip → already merged.
791
+ await execGit(repoPath, ["merge-base", "--is-ancestor", "HEAD", `origin/${branch}`]);
792
+ return true;
793
+ } catch {
794
+ return false;
795
+ }
796
+ }
797
+
773
798
  async function resolveRemoteDefaultBranch(repoPath) {
774
799
  try {
775
800
  const output = await execGitOutput(repoPath, ["symbolic-ref", "refs/remotes/origin/HEAD"]);
@@ -2,11 +2,12 @@ import { mkdir, readFile } from "node:fs/promises";
2
2
  import path from "node:path";
3
3
  import { atomicWriteFile } from "../fs-atomic.js";
4
4
 
5
- // Durable map of Claude session ids keyed by repo + PR branch, so a PR comment
5
+ // Durable map of Claude session ids keyed by repo + PR branch + PR scope, so a PR comment
6
6
  // or reopen can resume the prior conversation instead of rebuilding context
7
7
  // from memory (which previously dropped diffs / decisions and caused the model
8
- // to "forget" earlier work). Stored under the same memory dir as issue memory
9
- // so it survives worker restarts.
8
+ // to "forget" earlier work). The PR scope prevents separate PRs that reuse a
9
+ // generic branch name from inheriting each other's Claude conversation.
10
+ // Stored under the same memory dir as issue memory so it survives worker restarts.
10
11
  const SESSIONS_FILE = "pr-sessions.json";
11
12
  const MAX_ENTRIES = 1000;
12
13
 
@@ -14,8 +15,13 @@ function sessionStorePath(projectRoot) {
14
15
  return path.join(projectRoot, "memory", SESSIONS_FILE);
15
16
  }
16
17
 
17
- function sessionKey(repoUrl, branchName) {
18
- return `${String(repoUrl || "").trim()}#${String(branchName || "").trim()}`;
18
+ function sessionKey(repoUrl, branchName, scope = "") {
19
+ const parts = [String(repoUrl || "").trim(), String(branchName || "").trim()];
20
+ const normalizedScope = String(scope || "").trim();
21
+ if (normalizedScope) {
22
+ parts.push(normalizedScope);
23
+ }
24
+ return parts.join("#");
19
25
  }
20
26
 
21
27
  async function readStore(projectRoot) {
@@ -29,12 +35,12 @@ async function readStore(projectRoot) {
29
35
  }
30
36
  }
31
37
 
32
- export async function loadPrSessionId(projectRoot, repoUrl, branchName) {
38
+ export async function loadPrSessionId(projectRoot, repoUrl, branchName, scope = "") {
33
39
  if (!projectRoot || !branchName) {
34
40
  return "";
35
41
  }
36
42
  const store = await readStore(projectRoot);
37
- const entry = store[sessionKey(repoUrl, branchName)];
43
+ const entry = store[sessionKey(repoUrl, branchName, scope)];
38
44
  return entry && typeof entry.session_id === "string" ? entry.session_id : "";
39
45
  }
40
46
 
@@ -42,14 +48,17 @@ export async function loadPrSessionId(projectRoot, repoUrl, branchName) {
42
48
  // never lose each other's entries.
43
49
  let writeChain = Promise.resolve();
44
50
 
45
- export async function savePrSessionId(projectRoot, repoUrl, branchName, sessionId) {
51
+ export async function savePrSessionId(projectRoot, repoUrl, branchName, scopeOrSessionId, maybeSessionId) {
52
+ const hasScope = maybeSessionId !== undefined;
53
+ const scope = hasScope ? scopeOrSessionId : "";
54
+ const sessionId = hasScope ? maybeSessionId : scopeOrSessionId;
46
55
  if (!projectRoot || !branchName || !sessionId) {
47
56
  return;
48
57
  }
49
58
  writeChain = writeChain
50
59
  .then(async () => {
51
60
  const store = await readStore(projectRoot);
52
- store[sessionKey(repoUrl, branchName)] = {
61
+ store[sessionKey(repoUrl, branchName, scope)] = {
53
62
  session_id: String(sessionId).trim(),
54
63
  updated_at: new Date().toISOString()
55
64
  };
@@ -2,11 +2,11 @@ import {
2
2
  isClaudeCliError,
3
3
  runClaudeImplementation,
4
4
  runClaudeImplementationPlanBreakdown,
5
- runClaudeImplementationStep,
6
- runClaudeStepBreakdown
5
+ runClaudeImplementationStep
7
6
  } from "../claude.js";
8
7
  import {
9
8
  branchHasChangesAgainstMain,
9
+ branchMergedIntoBase,
10
10
  commitAndPushRepoChanges,
11
11
  ensureWorktreeForBranch,
12
12
  getBranchDiffAgainstBase,
@@ -30,13 +30,23 @@ import {
30
30
  formatPullRequestOutcomeComment,
31
31
  getNextPlanStep,
32
32
  hasImplementationPlan,
33
- replaceStepWithSubSteps,
34
33
  setImplementationPlan,
35
34
  setPullRequestStatus,
35
+ stripImplementationPlan,
36
36
  tickPlanStep
37
37
  } from "./pull-request.js";
38
38
  import { extractJiraKey, parseOptionalInt, summarizeForLog, toErrorMessage } from "./utils.js";
39
39
 
40
+ const STEP_BRANCH_DIFF_MAX_CHARS = 3000;
41
+
42
+ function truncateForPrompt(value, maxChars, label) {
43
+ const text = String(value || "");
44
+ if (text.length <= maxChars) {
45
+ return text;
46
+ }
47
+ return `${text.slice(0, maxChars)}\n\n[${label}: ${text.length - maxChars} chars omitted]`;
48
+ }
49
+
40
50
  async function commitPartialPullRequestChanges(repo, detail, logger) {
41
51
  try {
42
52
  const commitResult = await commitAndPushRepoChanges(
@@ -81,7 +91,6 @@ export function createPullRequestImplementationServices(overrides = {}) {
81
91
  runClaudeImplementation,
82
92
  runClaudeImplementationPlanBreakdown,
83
93
  runClaudeImplementationStep,
84
- runClaudeStepBreakdown,
85
94
  commitAndPushRepoChanges,
86
95
  prepareRepoForBranch,
87
96
  persistEpicImplementationKnowledge,
@@ -186,9 +195,13 @@ export async function processPullRequestImplementation({
186
195
  // it from memory (which dropped diffs/decisions and caused re-work). The
187
196
  // branchDiff + latestComment below remain as the fallback when no session
188
197
  // id is stored or the stored id is stale.
189
- const resumeSessionId = await loadPrSessionId(projectRoot, repo.url, detail.headRef);
198
+ const prSessionScope =
199
+ [githubIssueMemory?.jira_key, detail.number, detail.url].filter(Boolean).map(String).join("#") ||
200
+ detail.headRef ||
201
+ issueKey;
202
+ const resumeSessionId = await loadPrSessionId(projectRoot, repo.url, detail.headRef, prSessionScope);
190
203
  const onSessionId = (sessionId) => {
191
- void savePrSessionId(projectRoot, repo.url, detail.headRef, sessionId);
204
+ void savePrSessionId(projectRoot, repo.url, detail.headRef, prSessionScope, sessionId);
192
205
  };
193
206
 
194
207
  // Re-ground the run on any work earlier runs already committed to this
@@ -198,12 +211,17 @@ export async function processPullRequestImplementation({
198
211
  typeof services.getBranchDiffAgainstBase === "function"
199
212
  ? await services.getBranchDiffAgainstBase(repoPath, detail.baseRef)
200
213
  : "";
214
+ const stepBranchDiff = truncateForPrompt(
215
+ branchDiff,
216
+ STEP_BRANCH_DIFF_MAX_CHARS,
217
+ "branch diff truncated for step prompt"
218
+ );
201
219
 
202
220
  // Proactively break down large PRs before attempting full implementation
203
221
  if (!inStepMode && nextBody.length > 5000) {
204
222
  try {
205
223
  const proactiveBreakdown = await services.runClaudeImplementationPlanBreakdown({
206
- pullRequest: { ...detail, body: nextBody.slice(0, 8000) },
224
+ pullRequest: { ...detail, body: stripImplementationPlan(nextBody).slice(0, 8000) },
207
225
  memory: { epic: epicSnapshot },
208
226
  issueKey,
209
227
  logger
@@ -269,7 +287,7 @@ export async function processPullRequestImplementation({
269
287
  step: stepText,
270
288
  memory: { epic: epicSnapshot },
271
289
  epicContext: epicMemory,
272
- branchDiff,
290
+ branchDiff: stepBranchDiff,
273
291
  repoPath,
274
292
  repoPaths: repoAccess.repoPaths,
275
293
  branchName: detail.headRef,
@@ -314,6 +332,16 @@ export async function processPullRequestImplementation({
314
332
  pr: detail.number
315
333
  });
316
334
  result = { result: "implemented", summary: "Implementation already committed on previous run." };
335
+ } else if (!latestComment && (await branchMergedIntoBase(repoPath, detail.baseRef).catch(() => false))) {
336
+ // The branch's work already landed on the base (merged via another MR or
337
+ // by a human). There is nothing left to implement, so skip Claude
338
+ // entirely — running it would only report "no changes" and loop. A human
339
+ // comment overrides this so feedback can still be addressed.
340
+ await logger.info("Head branch already merged into base; skipping Claude (nothing to implement)", {
341
+ repo: repo.url,
342
+ pr: detail.number
343
+ });
344
+ result = { result: "implemented", summary: "Changes already merged into the base branch." };
317
345
  } else {
318
346
  result = await services.runClaudeImplementation({
319
347
  pullRequest: { ...detail, body: nextBody },
@@ -423,7 +451,53 @@ export async function processPullRequestImplementation({
423
451
  );
424
452
 
425
453
  if (!commitResult.committed && !commitResult.branchHasImplementation) {
426
- throw new Error("Claude reported success but no repository changes were available to commit.");
454
+ // No diff to commit. If the branch's work already landed on the base
455
+ // (merged via another MR / by a human), re-running Claude can never
456
+ // produce changes — treat it as terminally implemented instead of
457
+ // looping. Otherwise stop without preserving STUCK_AT so the bot waits
458
+ // for human guidance rather than machine-gun retrying an unchangeable
459
+ // no-op every retry window.
460
+ const mergedIntoBase = await branchMergedIntoBase(repoPath, detail.baseRef).catch(() => false);
461
+ if (mergedIntoBase) {
462
+ const mergedBody = setPullRequestStatus(nextBody, "IMPLEMENTED");
463
+ await github.updatePullRequest(repo.url, detail.number, { title: detail.title, body: mergedBody });
464
+ await services.clearForgePullRequestBlocked(github, { ...detail, repoUrl: repo.url });
465
+ if (markReadyOnSuccess) {
466
+ await github.markPullRequestReady(repo.url, detail.number);
467
+ await services.transitionLinkedJiraIssueToReview(detail.title, jira, logger, detail.number);
468
+ }
469
+ const mergedComment =
470
+ successCommentBody ||
471
+ formatPullRequestOutcomeComment(
472
+ result.summary,
473
+ "The requested changes are already merged into the base branch; nothing left to implement."
474
+ );
475
+ await postPullRequestComment(github, repo.url, detail.number, mergedComment);
476
+ await postLinkedJiraComment(extractJiraKey(detail.title), mergedComment, jira, logger);
477
+ await logger.info("Branch already merged into base; PR marked IMPLEMENTED", {
478
+ repo: repo.url,
479
+ pr: detail.number
480
+ });
481
+ return buildDraftPrState(
482
+ { ...detail, body: mergedBody, draft: markReadyOnSuccess ? false : detail.draft },
483
+ "IMPLEMENTED",
484
+ "implemented"
485
+ );
486
+ }
487
+ const noChangeBody = clearStuckAt(setPullRequestStatus(nextBody, "STUCK"));
488
+ await github.updatePullRequest(repo.url, detail.number, { title: detail.title, body: noChangeBody });
489
+ await services.setForgePullRequestBlocked(github, { ...detail, repoUrl: repo.url });
490
+ const noChangeComment = formatPullRequestOutcomeComment(
491
+ result.summary,
492
+ "Claude reported success but produced no repository changes, so there was nothing to commit. Halting automatic retries until there is new guidance."
493
+ );
494
+ await postPullRequestComment(github, repo.url, detail.number, noChangeComment);
495
+ await postLinkedJiraComment(extractJiraKey(detail.title), noChangeComment, jira, logger);
496
+ await logger.error("Pull request produced no repository changes; halting auto-retry", {
497
+ repo: repo.url,
498
+ pr: detail.number
499
+ });
500
+ return buildDraftPrState({ ...detail, body: noChangeBody }, "STUCK", "stuck");
427
501
  }
428
502
 
429
503
  const implementedBody = setPullRequestStatus(nextBody, "IMPLEMENTED");
@@ -504,7 +578,7 @@ export async function processPullRequestImplementation({
504
578
  const issueKey = githubIssueMemory?.jira_key || `PR-${detail.number}`;
505
579
  const epicSnapshot = buildEpicMemoryPromptSnapshot(epicMemory);
506
580
  const breakdown = await services.runClaudeImplementationPlanBreakdown({
507
- pullRequest: { ...detail, body: nextBody.slice(0, 8000) },
581
+ pullRequest: { ...detail, body: stripImplementationPlan(nextBody).slice(0, 8000) },
508
582
  memory: { epic: epicSnapshot },
509
583
  issueKey,
510
584
  logger
@@ -527,38 +601,15 @@ export async function processPullRequestImplementation({
527
601
  });
528
602
  }
529
603
  }
530
- // Try to split the failing step into sub-steps before giving up permanently.
531
604
  if (inStepMode && stepText) {
532
- try {
533
- const issueKey = githubIssueMemory?.jira_key || `PR-${detail.number}`;
534
- const subBreakdown = await services.runClaudeStepBreakdown({
535
- pullRequest: { ...detail },
536
- step: stepText,
537
- issueKey,
538
- logger
539
- });
540
- if (subBreakdown?.steps?.length > 1) {
541
- const splitBody = setPullRequestStatus(
542
- replaceStepWithSubSteps(nextBody, stepText, subBreakdown.steps),
543
- "IMPLEMENTING"
544
- );
545
- await github.updatePullRequest(repo.url, detail.number, { title: detail.title, body: splitBody });
546
- await logger.info("Split too-large step into sub-steps", {
547
- repo: repo.url,
548
- pr: detail.number,
549
- subSteps: subBreakdown.steps.length
550
- });
551
- return buildDraftPrState({ ...detail, body: splitBody }, "IMPLEMENTING", "implementing");
552
- }
553
- } catch (stepBreakdownError) {
554
- await logger.error("Failed to split stuck step into sub-steps", {
555
- repo: repo.url,
556
- pr: detail.number,
557
- error: stepBreakdownError
558
- });
559
- }
605
+ await logger.error("Pull request implementation step is too large; stopping without recursive breakdown", {
606
+ repo: repo.url,
607
+ pr: detail.number,
608
+ step: summarizeForLog(stepText)
609
+ });
560
610
  }
561
- // Step could not be subdivided further permanent stuck.
611
+ // A planned step that still exceeds the prompt budget should stop here
612
+ // instead of recursively expanding the PR body with more generated steps.
562
613
  // Otherwise (no plan yet, breakdown failed) → allow auto-retry by preserving STUCK_AT.
563
614
  const promptTooLongComment = inStepMode
564
615
  ? "Even a single implementation step is too large for Claude to process. Please split this PR into smaller, more focused changes."
@@ -132,15 +132,30 @@ const PLAN_START = "<!-- tm8:steps -->";
132
132
  const PLAN_END = "<!-- /tm8:steps -->";
133
133
  const UNCHECKED_STEP_RE = /^- \[ \] (.+)$/mu;
134
134
  const CHECKED_STEP_RE = /^- \[x\] /mu;
135
+ const MAX_IMPLEMENTATION_PLAN_STEPS = 20;
136
+ const MAX_STEP_BREAKDOWN_STEPS = 4;
137
+ const MAX_IMPLEMENTATION_STEP_CHARS = 1000;
135
138
 
136
139
  export function hasImplementationPlan(body) {
137
140
  const s = String(body || "");
138
141
  return s.includes(PLAN_START) && s.includes(PLAN_END);
139
142
  }
140
143
 
144
+ export function stripImplementationPlan(body) {
145
+ const normalized = String(body || "");
146
+ if (!hasImplementationPlan(normalized)) {
147
+ return normalized;
148
+ }
149
+ return normalized
150
+ .replace(new RegExp(`\\n*${escapeRegExpStr(PLAN_START)}[\\s\\S]*?${escapeRegExpStr(PLAN_END)}`, "u"), "")
151
+ .trimEnd();
152
+ }
153
+
141
154
  export function setImplementationPlan(body, steps) {
142
155
  const normalized = String(body || "");
143
- const planLines = steps.map((step) => `- [ ] ${step}`).join("\n");
156
+ const planLines = normalizeImplementationSteps(steps, MAX_IMPLEMENTATION_PLAN_STEPS)
157
+ .map((step) => `- [ ] ${step}`)
158
+ .join("\n");
144
159
  const planBlock = `${PLAN_START}\n${planLines}\n${PLAN_END}`;
145
160
  if (hasImplementationPlan(normalized)) {
146
161
  return normalized.replace(
@@ -173,7 +188,9 @@ export function replaceStepWithSubSteps(body, stepText, subSteps) {
173
188
  return body;
174
189
  }
175
190
  const escaped = stepText.replace(/[.*+?^${}()|[\]\\]/gu, "\\$&");
176
- const subLines = subSteps.map((s) => `- [ ] ${s}`).join("\n");
191
+ const subLines = normalizeImplementationSteps(subSteps, MAX_STEP_BREAKDOWN_STEPS)
192
+ .map((s) => `- [ ] ${s}`)
193
+ .join("\n");
177
194
  return String(body || "").replace(new RegExp(`^- \\[ \\] ${escaped}$`, "mu"), subLines);
178
195
  }
179
196
 
@@ -192,6 +209,25 @@ function escapeRegExpStr(value) {
192
209
  return String(value).replace(/[.*+?^${}()|[\]\\]/gu, "\\$&");
193
210
  }
194
211
 
212
+ function normalizeImplementationSteps(steps, maxSteps) {
213
+ if (!Array.isArray(steps)) {
214
+ return [];
215
+ }
216
+ return steps
217
+ .map((step) =>
218
+ String(step || "")
219
+ .replace(/\s+/gu, " ")
220
+ .trim()
221
+ )
222
+ .filter(Boolean)
223
+ .slice(0, maxSteps)
224
+ .map((step) =>
225
+ step.length > MAX_IMPLEMENTATION_STEP_CHARS
226
+ ? `${step.slice(0, MAX_IMPLEMENTATION_STEP_CHARS - 3).trimEnd()}...`
227
+ : step
228
+ );
229
+ }
230
+
195
231
  export function formatPullRequestOutcomeComment(summary, fallback) {
196
232
  const normalized = String(summary || "").trim();
197
233
  return normalized || fallback;