claude-teammate 0.1.313 → 0.1.315
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
package/src/claude/prompts.js
CHANGED
|
@@ -23,6 +23,16 @@ function formatPriorWorkContext(input) {
|
|
|
23
23
|
return `${sections.join("\n")}\n`;
|
|
24
24
|
}
|
|
25
25
|
|
|
26
|
+
// Pre-baked code-graph grounding (symbols + call paths) injected by the worker.
|
|
27
|
+
// Empty when codegraph is unavailable, so prompts are unchanged in that case.
|
|
28
|
+
function formatCodegraphContext(input) {
|
|
29
|
+
const ctx = String(input.codegraphContext || "").trim();
|
|
30
|
+
if (!ctx) {
|
|
31
|
+
return "";
|
|
32
|
+
}
|
|
33
|
+
return `\nCode graph context (symbols + call paths relevant to this task — use it to locate code precisely and avoid hallucinating APIs that do not exist in this repository):\n${ctx}\n`;
|
|
34
|
+
}
|
|
35
|
+
|
|
26
36
|
const INCLUDE_RESOURCE_LINKS_RULE =
|
|
27
37
|
"When your response is posted as a comment on a Jira issue, GitHub issue, or GitHub PR, include the URL of every resource you created (e.g. Jira task, spreadsheet, test design, test cases, document, Confluence page) so the reader can navigate directly to it.";
|
|
28
38
|
|
|
@@ -400,7 +410,7 @@ ${input.latestComment?.body || "(none)"}
|
|
|
400
410
|
|
|
401
411
|
Recent PR comments:
|
|
402
412
|
${recentComments || "(none)"}
|
|
403
|
-
${formatPriorWorkContext(input)}
|
|
413
|
+
${formatPriorWorkContext(input)}${formatCodegraphContext(input)}
|
|
404
414
|
Instructions:
|
|
405
415
|
- Checkout the branch above in this repository.
|
|
406
416
|
- Implement the plan described in the pull request body.
|
|
@@ -522,7 +532,7 @@ ${JSON.stringify(input.memory?.epic || {}, null, 2)}
|
|
|
522
532
|
|
|
523
533
|
Step to implement:
|
|
524
534
|
${input.step}
|
|
525
|
-
${formatPriorWorkContext(input)}
|
|
535
|
+
${formatPriorWorkContext(input)}${formatCodegraphContext(input)}
|
|
526
536
|
Instructions:
|
|
527
537
|
- Checkout the branch above in this repository.
|
|
528
538
|
- Implement only the step described above. Do not implement other parts of the plan.
|
package/src/repo.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { execFile } from "node:child_process";
|
|
2
|
-
import { access, mkdir, rename, rm } from "node:fs/promises";
|
|
2
|
+
import { access, mkdir, readFile, rename, rm, writeFile } from "node:fs/promises";
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import { promisify } from "node:util";
|
|
5
5
|
import { buildGitEnvForRepoUrl, parseGitHubRepoUrl, parseRepoUrl } from "./forge/repo-host.js";
|
|
@@ -24,6 +24,38 @@ export class RepoCheckoutNotReadyError extends Error {
|
|
|
24
24
|
}
|
|
25
25
|
}
|
|
26
26
|
|
|
27
|
+
// Idempotently add ignore patterns to this checkout's local exclude file
|
|
28
|
+
// (`.git/info/exclude`). Worktree-safe: resolves the shared common git dir so a
|
|
29
|
+
// single exclude file covers every worktree. Used to keep tooling artifacts
|
|
30
|
+
// (e.g. `.codegraph/`) out of `git add -A` and `git clean -fd` without touching
|
|
31
|
+
// the repository's tracked `.gitignore`.
|
|
32
|
+
export async function ensureGitExcludes(repoPath, patterns) {
|
|
33
|
+
let commonDir;
|
|
34
|
+
try {
|
|
35
|
+
commonDir = (await execGitOutput(repoPath, ["rev-parse", "--git-common-dir"])).trim();
|
|
36
|
+
} catch {
|
|
37
|
+
return; // not a git checkout yet — nothing to exclude
|
|
38
|
+
}
|
|
39
|
+
const excludePath = path.isAbsolute(commonDir)
|
|
40
|
+
? path.join(commonDir, "info", "exclude")
|
|
41
|
+
: path.join(repoPath, commonDir, "info", "exclude");
|
|
42
|
+
|
|
43
|
+
let current = "";
|
|
44
|
+
try {
|
|
45
|
+
current = await readFile(excludePath, "utf8");
|
|
46
|
+
} catch {
|
|
47
|
+
// exclude file may not exist yet — treated as empty
|
|
48
|
+
}
|
|
49
|
+
const present = new Set(current.split("\n").map((line) => line.trim()));
|
|
50
|
+
const missing = patterns.filter((pattern) => !present.has(pattern));
|
|
51
|
+
if (missing.length === 0) {
|
|
52
|
+
return; // idempotent: all patterns already excluded
|
|
53
|
+
}
|
|
54
|
+
const next = `${current.replace(/\n*$/u, "")}\n${missing.join("\n")}\n`;
|
|
55
|
+
await mkdir(path.dirname(excludePath), { recursive: true });
|
|
56
|
+
await writeFile(excludePath, next, "utf8");
|
|
57
|
+
}
|
|
58
|
+
|
|
27
59
|
export async function ensureLocalRepo(repoUrl, reposDir) {
|
|
28
60
|
const repo = parseRepoUrl(repoUrl);
|
|
29
61
|
const checkoutPath = path.join(reposDir, repo.owner, repo.name);
|
|
@@ -247,7 +279,9 @@ export async function prepareRepoForDefaultBranch(repoPath) {
|
|
|
247
279
|
|
|
248
280
|
export async function commitAndPushRepoChanges(repoPath, branchName, commitMessage, baseRef) {
|
|
249
281
|
await execGit(repoPath, ["checkout", branchName]);
|
|
250
|
-
|
|
282
|
+
// Defense-in-depth: never stage the codegraph index even if the local
|
|
283
|
+
// exclude file was not applied to this checkout.
|
|
284
|
+
await execGit(repoPath, ["add", "-A", "--", ".", ":(exclude).codegraph"]);
|
|
251
285
|
const statusOutput = await execGitOutput(repoPath, ["status", "--porcelain", "--ignore-submodules=dirty"]);
|
|
252
286
|
if (!statusOutput.trim()) {
|
|
253
287
|
const branchHasImplementation = await branchHasChangesAgainstMain(repoPath, baseRef);
|
|
@@ -770,6 +804,31 @@ export async function branchHasChangesAgainstMain(repoPath, baseRef) {
|
|
|
770
804
|
return Number.isFinite(count) && count > 0;
|
|
771
805
|
}
|
|
772
806
|
|
|
807
|
+
/**
|
|
808
|
+
* True when HEAD's own work has already landed on the base branch (e.g. the
|
|
809
|
+
* branch was merged via another MR / a human merged it directly). In that case
|
|
810
|
+
* HEAD is an ancestor of the base tip while still being a distinct commit, so a
|
|
811
|
+
* branch-vs-base diff is empty and re-running Claude can never produce changes.
|
|
812
|
+
* Callers use this to terminate instead of looping on "no changes to commit".
|
|
813
|
+
* A brand-new branch that merely points at the base tip (HEAD === base) returns
|
|
814
|
+
* false — that is an unstarted PR, not a merged one. Best-effort; never throws.
|
|
815
|
+
*/
|
|
816
|
+
export async function branchMergedIntoBase(repoPath, baseRef) {
|
|
817
|
+
const branch = baseRef || (await resolveRemoteDefaultBranch(repoPath));
|
|
818
|
+
try {
|
|
819
|
+
const headSha = (await execGitOutput(repoPath, ["rev-parse", "HEAD"])).trim();
|
|
820
|
+
const baseSha = (await execGitOutput(repoPath, ["rev-parse", `origin/${branch}`])).trim();
|
|
821
|
+
if (!headSha || !baseSha || headSha === baseSha) {
|
|
822
|
+
return false;
|
|
823
|
+
}
|
|
824
|
+
// exit 0 → HEAD is an ancestor of the base tip → already merged.
|
|
825
|
+
await execGit(repoPath, ["merge-base", "--is-ancestor", "HEAD", `origin/${branch}`]);
|
|
826
|
+
return true;
|
|
827
|
+
} catch {
|
|
828
|
+
return false;
|
|
829
|
+
}
|
|
830
|
+
}
|
|
831
|
+
|
|
773
832
|
async function resolveRemoteDefaultBranch(repoPath) {
|
|
774
833
|
try {
|
|
775
834
|
const output = await execGitOutput(repoPath, ["symbolic-ref", "refs/remotes/origin/HEAD"]);
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { execFile } from "node:child_process";
|
|
2
|
+
import { existsSync } from "node:fs";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { promisify } from "node:util";
|
|
5
|
+
|
|
6
|
+
const execFileAsync = promisify(execFile);
|
|
7
|
+
|
|
8
|
+
// Resolve binary: env override wins, otherwise rely on PATH (with the
|
|
9
|
+
// user-local install dir prepended in runCg's env).
|
|
10
|
+
function codegraphBin() {
|
|
11
|
+
return process.env.CODEGRAPH_BIN || "codegraph";
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const INDEX_TIMEOUT_MS = 90_000;
|
|
15
|
+
const EXPLORE_TIMEOUT_MS = 30_000;
|
|
16
|
+
const CONTEXT_MAX_CHARS = 4000;
|
|
17
|
+
|
|
18
|
+
async function runCg(args, { cwd, timeout, logger }) {
|
|
19
|
+
try {
|
|
20
|
+
const { stdout } = await execFileAsync(codegraphBin(), args, {
|
|
21
|
+
cwd,
|
|
22
|
+
timeout,
|
|
23
|
+
env: { ...process.env, PATH: `${process.env.HOME}/.local/bin:${process.env.PATH}` }
|
|
24
|
+
});
|
|
25
|
+
return stdout || "";
|
|
26
|
+
} catch (error) {
|
|
27
|
+
// Codegraph failure must never block the task (mirrors the Claude-CLI rule).
|
|
28
|
+
await logger?.error?.("codegraph command failed; continuing without graph context", {
|
|
29
|
+
command: args[0],
|
|
30
|
+
cwd,
|
|
31
|
+
error: error?.message || String(error)
|
|
32
|
+
});
|
|
33
|
+
return null;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// init on first use for this checkout, incremental sync afterwards. Never throws.
|
|
38
|
+
export async function ensureCodegraphIndex(repoPath, logger) {
|
|
39
|
+
const indexed = existsSync(path.join(repoPath, ".codegraph"));
|
|
40
|
+
const args = indexed ? ["sync"] : ["init", "--force"];
|
|
41
|
+
await runCg(args, { cwd: repoPath, timeout: INDEX_TIMEOUT_MS, logger });
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Returns a grounding string (or "" when unavailable). Never throws.
|
|
45
|
+
export async function exploreCodegraph(repoPath, query, logger) {
|
|
46
|
+
if (!query?.trim()) {
|
|
47
|
+
return "";
|
|
48
|
+
}
|
|
49
|
+
const out = await runCg(["explore", query.trim()], {
|
|
50
|
+
cwd: repoPath,
|
|
51
|
+
timeout: EXPLORE_TIMEOUT_MS,
|
|
52
|
+
logger
|
|
53
|
+
});
|
|
54
|
+
if (!out) {
|
|
55
|
+
return "";
|
|
56
|
+
}
|
|
57
|
+
return out.length > CONTEXT_MAX_CHARS ? `${out.slice(0, CONTEXT_MAX_CHARS)}\n[codegraph context truncated]` : out;
|
|
58
|
+
}
|
|
@@ -6,12 +6,15 @@ import {
|
|
|
6
6
|
} from "../claude.js";
|
|
7
7
|
import {
|
|
8
8
|
branchHasChangesAgainstMain,
|
|
9
|
+
branchMergedIntoBase,
|
|
9
10
|
commitAndPushRepoChanges,
|
|
11
|
+
ensureGitExcludes,
|
|
10
12
|
ensureWorktreeForBranch,
|
|
11
13
|
getBranchDiffAgainstBase,
|
|
12
14
|
prepareRepoForBranch,
|
|
13
15
|
removeWorktree
|
|
14
16
|
} from "../repo.js";
|
|
17
|
+
import { ensureCodegraphIndex, exploreCodegraph } from "./codegraph-context.js";
|
|
15
18
|
import { buildEpicMemoryPromptSnapshot, persistEpicImplementationKnowledge } from "./epic-memory.js";
|
|
16
19
|
import {
|
|
17
20
|
clearForgePullRequestBlocked,
|
|
@@ -189,6 +192,21 @@ export async function processPullRequestImplementation({
|
|
|
189
192
|
const issueKey = githubIssueMemory?.jira_key || `PR-${detail.number}`;
|
|
190
193
|
const epicSnapshot = buildEpicMemoryPromptSnapshot(epicMemory);
|
|
191
194
|
|
|
195
|
+
// Codegraph: keep the index local-only (never staged/pushed/cleaned), then
|
|
196
|
+
// (re)index every accessible checkout and pre-bake grounding context for the
|
|
197
|
+
// implementation prompt. All steps fail soft — codegraph is never required.
|
|
198
|
+
const codegraphRepoPaths =
|
|
199
|
+
Array.isArray(repoAccess.repoPaths) && repoAccess.repoPaths.length > 0 ? repoAccess.repoPaths : [repoPath];
|
|
200
|
+
for (const cgRepoPath of codegraphRepoPaths) {
|
|
201
|
+
await ensureGitExcludes(cgRepoPath, [".codegraph/"]);
|
|
202
|
+
await ensureCodegraphIndex(cgRepoPath, logger);
|
|
203
|
+
}
|
|
204
|
+
const codegraphContext = await exploreCodegraph(
|
|
205
|
+
repoPath,
|
|
206
|
+
`${issueKey} ${detail.title} ${stripImplementationPlan(nextBody).slice(0, 500)}`,
|
|
207
|
+
logger
|
|
208
|
+
);
|
|
209
|
+
|
|
192
210
|
// Resume the prior Claude conversation for this PR branch so a follow-up
|
|
193
211
|
// comment / reopen keeps the model's earlier context instead of rebuilding
|
|
194
212
|
// it from memory (which dropped diffs/decisions and caused re-work). The
|
|
@@ -284,6 +302,7 @@ export async function processPullRequestImplementation({
|
|
|
284
302
|
result = await services.runClaudeImplementationStep({
|
|
285
303
|
pullRequest: { ...detail, body: nextBody },
|
|
286
304
|
step: stepText,
|
|
305
|
+
codegraphContext,
|
|
287
306
|
memory: { epic: epicSnapshot },
|
|
288
307
|
epicContext: epicMemory,
|
|
289
308
|
branchDiff: stepBranchDiff,
|
|
@@ -331,6 +350,16 @@ export async function processPullRequestImplementation({
|
|
|
331
350
|
pr: detail.number
|
|
332
351
|
});
|
|
333
352
|
result = { result: "implemented", summary: "Implementation already committed on previous run." };
|
|
353
|
+
} else if (!latestComment && (await branchMergedIntoBase(repoPath, detail.baseRef).catch(() => false))) {
|
|
354
|
+
// The branch's work already landed on the base (merged via another MR or
|
|
355
|
+
// by a human). There is nothing left to implement, so skip Claude
|
|
356
|
+
// entirely — running it would only report "no changes" and loop. A human
|
|
357
|
+
// comment overrides this so feedback can still be addressed.
|
|
358
|
+
await logger.info("Head branch already merged into base; skipping Claude (nothing to implement)", {
|
|
359
|
+
repo: repo.url,
|
|
360
|
+
pr: detail.number
|
|
361
|
+
});
|
|
362
|
+
result = { result: "implemented", summary: "Changes already merged into the base branch." };
|
|
334
363
|
} else {
|
|
335
364
|
result = await services.runClaudeImplementation({
|
|
336
365
|
pullRequest: { ...detail, body: nextBody },
|
|
@@ -338,6 +367,7 @@ export async function processPullRequestImplementation({
|
|
|
338
367
|
epicContext: epicMemory,
|
|
339
368
|
latestComment,
|
|
340
369
|
branchDiff,
|
|
370
|
+
codegraphContext,
|
|
341
371
|
repoPath,
|
|
342
372
|
repoPaths: repoAccess.repoPaths,
|
|
343
373
|
branchName: detail.headRef,
|
|
@@ -440,7 +470,53 @@ export async function processPullRequestImplementation({
|
|
|
440
470
|
);
|
|
441
471
|
|
|
442
472
|
if (!commitResult.committed && !commitResult.branchHasImplementation) {
|
|
443
|
-
|
|
473
|
+
// No diff to commit. If the branch's work already landed on the base
|
|
474
|
+
// (merged via another MR / by a human), re-running Claude can never
|
|
475
|
+
// produce changes — treat it as terminally implemented instead of
|
|
476
|
+
// looping. Otherwise stop without preserving STUCK_AT so the bot waits
|
|
477
|
+
// for human guidance rather than machine-gun retrying an unchangeable
|
|
478
|
+
// no-op every retry window.
|
|
479
|
+
const mergedIntoBase = await branchMergedIntoBase(repoPath, detail.baseRef).catch(() => false);
|
|
480
|
+
if (mergedIntoBase) {
|
|
481
|
+
const mergedBody = setPullRequestStatus(nextBody, "IMPLEMENTED");
|
|
482
|
+
await github.updatePullRequest(repo.url, detail.number, { title: detail.title, body: mergedBody });
|
|
483
|
+
await services.clearForgePullRequestBlocked(github, { ...detail, repoUrl: repo.url });
|
|
484
|
+
if (markReadyOnSuccess) {
|
|
485
|
+
await github.markPullRequestReady(repo.url, detail.number);
|
|
486
|
+
await services.transitionLinkedJiraIssueToReview(detail.title, jira, logger, detail.number);
|
|
487
|
+
}
|
|
488
|
+
const mergedComment =
|
|
489
|
+
successCommentBody ||
|
|
490
|
+
formatPullRequestOutcomeComment(
|
|
491
|
+
result.summary,
|
|
492
|
+
"The requested changes are already merged into the base branch; nothing left to implement."
|
|
493
|
+
);
|
|
494
|
+
await postPullRequestComment(github, repo.url, detail.number, mergedComment);
|
|
495
|
+
await postLinkedJiraComment(extractJiraKey(detail.title), mergedComment, jira, logger);
|
|
496
|
+
await logger.info("Branch already merged into base; PR marked IMPLEMENTED", {
|
|
497
|
+
repo: repo.url,
|
|
498
|
+
pr: detail.number
|
|
499
|
+
});
|
|
500
|
+
return buildDraftPrState(
|
|
501
|
+
{ ...detail, body: mergedBody, draft: markReadyOnSuccess ? false : detail.draft },
|
|
502
|
+
"IMPLEMENTED",
|
|
503
|
+
"implemented"
|
|
504
|
+
);
|
|
505
|
+
}
|
|
506
|
+
const noChangeBody = clearStuckAt(setPullRequestStatus(nextBody, "STUCK"));
|
|
507
|
+
await github.updatePullRequest(repo.url, detail.number, { title: detail.title, body: noChangeBody });
|
|
508
|
+
await services.setForgePullRequestBlocked(github, { ...detail, repoUrl: repo.url });
|
|
509
|
+
const noChangeComment = formatPullRequestOutcomeComment(
|
|
510
|
+
result.summary,
|
|
511
|
+
"Claude reported success but produced no repository changes, so there was nothing to commit. Halting automatic retries until there is new guidance."
|
|
512
|
+
);
|
|
513
|
+
await postPullRequestComment(github, repo.url, detail.number, noChangeComment);
|
|
514
|
+
await postLinkedJiraComment(extractJiraKey(detail.title), noChangeComment, jira, logger);
|
|
515
|
+
await logger.error("Pull request produced no repository changes; halting auto-retry", {
|
|
516
|
+
repo: repo.url,
|
|
517
|
+
pr: detail.number
|
|
518
|
+
});
|
|
519
|
+
return buildDraftPrState({ ...detail, body: noChangeBody }, "STUCK", "stuck");
|
|
444
520
|
}
|
|
445
521
|
|
|
446
522
|
const implementedBody = setPullRequestStatus(nextBody, "IMPLEMENTED");
|