claude-teammate 0.1.199 → 0.1.201
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/claude/prompts.js +8 -1
- package/src/claude/validators.js +7 -1
- package/src/claude.js +14 -5
- package/src/memory.js +7 -3
- package/src/worker/epic-memory.js +28 -2
- package/src/worker/github-issue-workflow.js +7 -4
- package/src/worker/jira-helpers.js +66 -21
- package/src/worker/jira-issue-support.js +2 -1
- package/src/worker/jira-issue-workflow.js +12 -2
package/package.json
CHANGED
package/src/claude/prompts.js
CHANGED
|
@@ -585,11 +585,18 @@ export function buildEpicMemoryCleanupSystemPrompt() {
|
|
|
585
585
|
"Remove obvious, redundant, contradicted, or one-off ticket details.",
|
|
586
586
|
LANGUAGE_PRESERVATION_RULES,
|
|
587
587
|
"Do not invent knowledge that is not grounded in the provided memory snapshot.",
|
|
588
|
+
"Also extract repo_branches: for each repository URL in the known repos list (or mentioned in the candidate updates), if the candidate updates specify the TARGET/BASE branch — the branch the PR or MR should merge INTO (e.g. 'tạo MR vào nhánh main', 'merge into develop', 'PR target is staging', 'nhánh chính là develop') — emit {url, branch} for it. Do NOT capture implementation/feature branch names (the short-lived branch being created for this task). Only include repos where a base/target branch is explicitly mentioned.",
|
|
589
|
+
"Also extract pr_repo_url: if the candidate updates indicate which specific repository should receive the pull request or merge request (e.g. 'tạo PR vào org/backend', 'create PR in org/frontend', 'MR goes to repo X'), set this to that repository's full URL from the known repos list. Set to empty string if not mentioned or ambiguous.",
|
|
588
590
|
"Return only structured output matching the schema."
|
|
589
591
|
].join(" ");
|
|
590
592
|
}
|
|
591
593
|
|
|
592
594
|
export function buildEpicMemoryCleanupUserPrompt(input) {
|
|
595
|
+
const knownRepos = (input.epicMemory?.repos || []).map((r) => r.url).filter(Boolean);
|
|
596
|
+
const reposLine = knownRepos.length > 0
|
|
597
|
+
? `\nKnown repos:\n${knownRepos.map((u) => `- ${u}`).join("\n")}`
|
|
598
|
+
: "";
|
|
599
|
+
|
|
593
600
|
return `Update this epic memory facts and guardrails and clean it up.
|
|
594
601
|
|
|
595
602
|
Reason:
|
|
@@ -597,7 +604,7 @@ ${input.reason || "(none)"}
|
|
|
597
604
|
|
|
598
605
|
Current epic memory:
|
|
599
606
|
${JSON.stringify(input.epicMemory || {}, null, 2)}
|
|
600
|
-
|
|
607
|
+
${reposLine}
|
|
601
608
|
Latest candidate updates:
|
|
602
609
|
${JSON.stringify(input.candidateUpdates || {}, null, 2)}
|
|
603
610
|
|
package/src/claude/validators.js
CHANGED
|
@@ -121,7 +121,13 @@ export function validateEpicMemoryCleanupResult(result) {
|
|
|
121
121
|
: [],
|
|
122
122
|
guardrails: Array.isArray(result.guardrails)
|
|
123
123
|
? result.guardrails.map((item) => String(item).trim()).filter(Boolean)
|
|
124
|
-
: []
|
|
124
|
+
: [],
|
|
125
|
+
repo_branches: Array.isArray(result.repo_branches)
|
|
126
|
+
? result.repo_branches
|
|
127
|
+
.filter((rb) => rb && typeof rb.url === "string" && rb.url.trim())
|
|
128
|
+
.map((rb) => ({ url: String(rb.url).trim(), branch: String(rb.branch || "").trim() }))
|
|
129
|
+
: [],
|
|
130
|
+
pr_repo_url: typeof result.pr_repo_url === "string" ? result.pr_repo_url.trim() : ""
|
|
125
131
|
};
|
|
126
132
|
}
|
|
127
133
|
|
package/src/claude.js
CHANGED
|
@@ -247,16 +247,25 @@ const EPIC_MEMORY_CLEANUP_SCHEMA = {
|
|
|
247
247
|
properties: {
|
|
248
248
|
facts: {
|
|
249
249
|
type: "array",
|
|
250
|
-
items: {
|
|
251
|
-
type: "string"
|
|
252
|
-
}
|
|
250
|
+
items: { type: "string" }
|
|
253
251
|
},
|
|
254
252
|
guardrails: {
|
|
253
|
+
type: "array",
|
|
254
|
+
items: { type: "string" }
|
|
255
|
+
},
|
|
256
|
+
repo_branches: {
|
|
255
257
|
type: "array",
|
|
256
258
|
items: {
|
|
257
|
-
type: "
|
|
259
|
+
type: "object",
|
|
260
|
+
additionalProperties: false,
|
|
261
|
+
properties: {
|
|
262
|
+
url: { type: "string" },
|
|
263
|
+
branch: { type: "string" }
|
|
264
|
+
},
|
|
265
|
+
required: ["url", "branch"]
|
|
258
266
|
}
|
|
259
|
-
}
|
|
267
|
+
},
|
|
268
|
+
pr_repo_url: { type: "string" }
|
|
260
269
|
},
|
|
261
270
|
required: ["facts", "guardrails"]
|
|
262
271
|
};
|
package/src/memory.js
CHANGED
|
@@ -190,10 +190,12 @@ function normalizeEpicMemoryData(data) {
|
|
|
190
190
|
|
|
191
191
|
normalized.repos = uniqueRepos(normalized.repos).map((repo) => ({
|
|
192
192
|
url: repo.url,
|
|
193
|
-
local_path: repo.local_path || ""
|
|
193
|
+
local_path: repo.local_path || "",
|
|
194
|
+
working_branch: repo.working_branch || ""
|
|
194
195
|
}));
|
|
195
196
|
normalized.facts = [...new Set(normalized.facts.map((fact) => String(fact).trim()).filter(Boolean))];
|
|
196
197
|
normalized.guardrails = [...new Set(normalized.guardrails.map((guardrail) => String(guardrail).trim()).filter(Boolean))];
|
|
198
|
+
normalized.pr_repo_url = String(normalized.pr_repo_url || "").trim();
|
|
197
199
|
|
|
198
200
|
delete normalized.repo_url;
|
|
199
201
|
delete normalized.local_repo_path;
|
|
@@ -386,13 +388,15 @@ function uniqueRepos(repos) {
|
|
|
386
388
|
|
|
387
389
|
const existing = seen.get(repo.url) ?? {
|
|
388
390
|
url: repo.url,
|
|
389
|
-
local_path: ""
|
|
391
|
+
local_path: "",
|
|
392
|
+
working_branch: ""
|
|
390
393
|
};
|
|
391
394
|
|
|
392
395
|
seen.set(repo.url, {
|
|
393
396
|
...existing,
|
|
394
397
|
...repo,
|
|
395
|
-
local_path: repo.local_path || existing.local_path || ""
|
|
398
|
+
local_path: repo.local_path || existing.local_path || "",
|
|
399
|
+
working_branch: repo.working_branch || existing.working_branch || ""
|
|
396
400
|
});
|
|
397
401
|
}
|
|
398
402
|
|
|
@@ -121,10 +121,16 @@ export async function saveMaintainedEpicMemory({
|
|
|
121
121
|
issueKey: issue.key,
|
|
122
122
|
logger
|
|
123
123
|
});
|
|
124
|
+
const updatedRepos = applyRepoBranchUpdates(
|
|
125
|
+
startingEpicMemory.repos || [],
|
|
126
|
+
cleaned.repo_branches || []
|
|
127
|
+
);
|
|
124
128
|
nextEpicMemory = normalizeEpicMemoryContent({
|
|
125
129
|
...startingEpicMemory,
|
|
126
130
|
facts: cleaned.facts || [],
|
|
127
|
-
guardrails: cleaned.guardrails || []
|
|
131
|
+
guardrails: cleaned.guardrails || [],
|
|
132
|
+
repos: updatedRepos,
|
|
133
|
+
pr_repo_url: cleaned.pr_repo_url || startingEpicMemory.pr_repo_url || ""
|
|
128
134
|
});
|
|
129
135
|
if (logger) {
|
|
130
136
|
await logger.info("Updated epic memory", {
|
|
@@ -230,10 +236,30 @@ function hasEpicMemoryContentChanges(current, next) {
|
|
|
230
236
|
JSON.stringify(dedupeTextValues(current?.facts || [])) !==
|
|
231
237
|
JSON.stringify(dedupeTextValues(next?.facts || [])) ||
|
|
232
238
|
JSON.stringify(dedupeTextValues(current?.guardrails || [])) !==
|
|
233
|
-
JSON.stringify(dedupeTextValues(next?.guardrails || []))
|
|
239
|
+
JSON.stringify(dedupeTextValues(next?.guardrails || [])) ||
|
|
240
|
+
(current?.pr_repo_url || "") !== (next?.pr_repo_url || "") ||
|
|
241
|
+
JSON.stringify(current?.repos || []) !== JSON.stringify(next?.repos || [])
|
|
234
242
|
);
|
|
235
243
|
}
|
|
236
244
|
|
|
245
|
+
function applyRepoBranchUpdates(existingRepos, repoBranches) {
|
|
246
|
+
if (!Array.isArray(repoBranches) || repoBranches.length === 0) {
|
|
247
|
+
return existingRepos;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
let repos = [...existingRepos];
|
|
251
|
+
for (const rb of repoBranches) {
|
|
252
|
+
if (!rb.url) continue;
|
|
253
|
+
const idx = repos.findIndex((r) => r.url === rb.url);
|
|
254
|
+
if (idx === -1) {
|
|
255
|
+
repos.push({ url: rb.url, local_path: "", working_branch: rb.branch });
|
|
256
|
+
} else if (rb.branch) {
|
|
257
|
+
repos[idx] = { ...repos[idx], working_branch: rb.branch };
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
return repos;
|
|
261
|
+
}
|
|
262
|
+
|
|
237
263
|
export function buildReopenContext({ reopenedFromInReview, issueMemoryPath, epicMemoryPath, workerLogPath }) {
|
|
238
264
|
if (!reopenedFromInReview) {
|
|
239
265
|
return null;
|
|
@@ -256,7 +256,10 @@ export async function processGitHubIssue({
|
|
|
256
256
|
return buildGitHubIssueState(detail, githubIssueMemory, "blocked");
|
|
257
257
|
}
|
|
258
258
|
|
|
259
|
-
const
|
|
259
|
+
const repoEpicEntry = (approvalEpicMemory?.repos || []).find(
|
|
260
|
+
(r) => r.url === githubIssueMemory.repo_url
|
|
261
|
+
);
|
|
262
|
+
const baseBranch = repoEpicEntry?.working_branch || await github.getDefaultBranch(githubIssueMemory.repo_url);
|
|
260
263
|
await services.pullLatest(githubIssueMemory.local_path);
|
|
261
264
|
|
|
262
265
|
// Use a short-lived worktree to create the branch so the primary clone
|
|
@@ -270,9 +273,9 @@ export async function processGitHubIssue({
|
|
|
270
273
|
githubIssueMemory.local_path,
|
|
271
274
|
nextBranchName
|
|
272
275
|
);
|
|
273
|
-
await services.ensureBranchFromDefault(branchWorktree, nextBranchName,
|
|
276
|
+
await services.ensureBranchFromDefault(branchWorktree, nextBranchName, baseBranch);
|
|
274
277
|
} else {
|
|
275
|
-
await services.ensureBranchFromDefault(githubIssueMemory.local_path, nextBranchName,
|
|
278
|
+
await services.ensureBranchFromDefault(githubIssueMemory.local_path, nextBranchName, baseBranch);
|
|
276
279
|
}
|
|
277
280
|
} finally {
|
|
278
281
|
if (branchWorktree) {
|
|
@@ -283,7 +286,7 @@ export async function processGitHubIssue({
|
|
|
283
286
|
title: detail.title,
|
|
284
287
|
body: buildInitialPullRequestBody(detail.number, detail.body),
|
|
285
288
|
head: nextBranchName,
|
|
286
|
-
base:
|
|
289
|
+
base: baseBranch,
|
|
287
290
|
draft: true
|
|
288
291
|
});
|
|
289
292
|
githubIssueMemory.pr_url = pullRequest.url;
|
|
@@ -206,14 +206,15 @@ export function resetIssueMemoryForCommentRestart(issueMemory, commentId) {
|
|
|
206
206
|
|
|
207
207
|
export function deriveTaskRepos(detail, epicMemory, forgeRegistry) {
|
|
208
208
|
const rememberedRepos = Array.isArray(epicMemory?.repos) ? epicMemory.repos : [];
|
|
209
|
-
const
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
209
|
+
const knownRepoUrls = rememberedRepos.map((r) => r.url).filter(Boolean);
|
|
210
|
+
const descriptionPairs = extractRepoBranchPairs(detail?.descriptionText || "");
|
|
211
|
+
const commentPairs = findRepoBranchPairsInComments(
|
|
212
|
+
Array.isArray(detail?.comments) ? detail.comments : [],
|
|
213
|
+
knownRepoUrls
|
|
214
|
+
);
|
|
214
215
|
|
|
215
216
|
return filterReposForActiveForge(
|
|
216
|
-
mergeRepoUrls(rememberedRepos,
|
|
217
|
+
mergeRepoUrls(rememberedRepos, [...descriptionPairs, ...commentPairs]),
|
|
217
218
|
forgeRegistry
|
|
218
219
|
);
|
|
219
220
|
}
|
|
@@ -233,7 +234,7 @@ export function filterReposForActiveForge(repos, forgeRegistry) {
|
|
|
233
234
|
});
|
|
234
235
|
}
|
|
235
236
|
|
|
236
|
-
export function selectIssueCreationRepo({ detail, issueMemory, repos }) {
|
|
237
|
+
export function selectIssueCreationRepo({ detail, issueMemory, repos, epicMemory }) {
|
|
237
238
|
if (!Array.isArray(repos) || repos.length === 0) {
|
|
238
239
|
return null;
|
|
239
240
|
}
|
|
@@ -243,6 +244,14 @@ export function selectIssueCreationRepo({ detail, issueMemory, repos }) {
|
|
|
243
244
|
}
|
|
244
245
|
|
|
245
246
|
const repoByUrl = new Map(repos.map((repo) => [String(repo.url || "").trim(), repo]));
|
|
247
|
+
|
|
248
|
+
if (epicMemory?.pr_repo_url) {
|
|
249
|
+
const prRepo = repoByUrl.get(String(epicMemory.pr_repo_url).trim());
|
|
250
|
+
if (prRepo) {
|
|
251
|
+
return prRepo;
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
246
255
|
const latestExplicitRepo = getLatestExplicitIssueCreationRepo(detail, repoByUrl);
|
|
247
256
|
if (latestExplicitRepo) {
|
|
248
257
|
return latestExplicitRepo;
|
|
@@ -414,19 +423,53 @@ function ensureImplementationInstruction(body) {
|
|
|
414
423
|
return `${normalizedBody}\n\n---\n\n${IMPLEMENTATION_INSTRUCTION}`.trim();
|
|
415
424
|
}
|
|
416
425
|
|
|
417
|
-
|
|
418
|
-
|
|
426
|
+
const BRANCH_KEYWORD_RE = /\b(?:branch|nhánh)\s*[:=]\s*([a-zA-Z0-9][a-zA-Z0-9_\-./]*)/iu;
|
|
427
|
+
|
|
428
|
+
function extractRepoBranchPairs(text) {
|
|
429
|
+
const str = String(text || "");
|
|
430
|
+
const urls = extractRepoUrls(str);
|
|
431
|
+
if (urls.length === 0) {
|
|
432
|
+
return [];
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
if (urls.length === 1) {
|
|
436
|
+
const branchMatch = str.match(BRANCH_KEYWORD_RE);
|
|
437
|
+
return [{ url: urls[0], working_branch: branchMatch ? branchMatch[1] : "" }];
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
return urls.map((url) => {
|
|
441
|
+
const lineWithUrl = str.split("\n").find((line) => line.includes(url)) || "";
|
|
442
|
+
const lineBranchMatch = lineWithUrl.match(BRANCH_KEYWORD_RE);
|
|
443
|
+
return { url, working_branch: lineBranchMatch ? lineBranchMatch[1] : "" };
|
|
444
|
+
});
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
function findRepoBranchPairsInComments(comments, knownRepoUrls = []) {
|
|
448
|
+
const seen = new Map();
|
|
419
449
|
|
|
420
450
|
for (const comment of [...comments].reverse()) {
|
|
421
|
-
const
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
451
|
+
const pairs = extractRepoBranchPairs(comment.bodyText || "");
|
|
452
|
+
|
|
453
|
+
for (const pair of pairs) {
|
|
454
|
+
if (!seen.has(pair.url)) {
|
|
455
|
+
seen.set(pair.url, pair.working_branch);
|
|
456
|
+
} else if (pair.working_branch && !seen.get(pair.url)) {
|
|
457
|
+
seen.set(pair.url, pair.working_branch);
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
// Branch-only comment (no URL): when there's exactly one known repo, apply the branch to it.
|
|
462
|
+
// Newest-first iteration ensures the latest explicit branch wins.
|
|
463
|
+
if (knownRepoUrls.length === 1 && pairs.length === 0) {
|
|
464
|
+
const url = knownRepoUrls[0];
|
|
465
|
+
const branchMatch = String(comment.bodyText || "").match(BRANCH_KEYWORD_RE);
|
|
466
|
+
if (branchMatch && (!seen.has(url) || !seen.get(url))) {
|
|
467
|
+
seen.set(url, branchMatch[1]);
|
|
425
468
|
}
|
|
426
469
|
}
|
|
427
470
|
}
|
|
428
471
|
|
|
429
|
-
return
|
|
472
|
+
return [...seen.entries()].map(([url, working_branch]) => ({ url, working_branch }));
|
|
430
473
|
}
|
|
431
474
|
|
|
432
475
|
function getLatestComment(comments) {
|
|
@@ -489,15 +532,17 @@ function isBlockedOrFailedIssueState(detail, issueMemory, blockedLabel) {
|
|
|
489
532
|
);
|
|
490
533
|
}
|
|
491
534
|
|
|
492
|
-
function mergeRepoUrls(existingRepos,
|
|
535
|
+
function mergeRepoUrls(existingRepos, repoPairs) {
|
|
493
536
|
const merged = Array.isArray(existingRepos) ? [...existingRepos] : [];
|
|
494
537
|
|
|
495
|
-
for (const
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
});
|
|
538
|
+
for (const pair of repoPairs) {
|
|
539
|
+
const url = pair.url;
|
|
540
|
+
const workingBranch = pair.working_branch || "";
|
|
541
|
+
const existingIdx = merged.findIndex((repo) => normalizeRepoIdentity(repo.url) === normalizeRepoIdentity(url));
|
|
542
|
+
if (existingIdx === -1) {
|
|
543
|
+
merged.push({ url, local_path: "", working_branch: workingBranch });
|
|
544
|
+
} else if (workingBranch) {
|
|
545
|
+
merged[existingIdx] = { ...merged[existingIdx], working_branch: workingBranch };
|
|
501
546
|
}
|
|
502
547
|
}
|
|
503
548
|
|
|
@@ -159,6 +159,7 @@ export async function continueGitHubIssueCreation({
|
|
|
159
159
|
issueMemoryRecord,
|
|
160
160
|
issueMemory,
|
|
161
161
|
repos,
|
|
162
|
+
epicMemory = null,
|
|
162
163
|
logger,
|
|
163
164
|
claudeResult = null,
|
|
164
165
|
services
|
|
@@ -168,7 +169,7 @@ export async function continueGitHubIssueCreation({
|
|
|
168
169
|
}
|
|
169
170
|
|
|
170
171
|
const latestRepoSelectionReply = getLatestHumanRepoSelectionReply(detail, botUser, jira);
|
|
171
|
-
let selectedRepo = selectIssueCreationRepo({ detail, issueMemory, repos });
|
|
172
|
+
let selectedRepo = selectIssueCreationRepo({ detail, issueMemory, repos, epicMemory });
|
|
172
173
|
const latestRepoPrompt = getLatestRepoSelectionPromptComment(detail, botUser, jira);
|
|
173
174
|
if (!selectedRepo && latestRepoSelectionReply) {
|
|
174
175
|
const fallback = await services.runClaudeRepoSelection({
|
|
@@ -170,6 +170,10 @@ export async function processJiraIssue({
|
|
|
170
170
|
services
|
|
171
171
|
}));
|
|
172
172
|
|
|
173
|
+
// Re-derive after LLM may have updated epicMemory (repo branches, pr_repo_url).
|
|
174
|
+
// Regex derivation uses epicMemory.repos as base so LLM-set branches are preserved.
|
|
175
|
+
liveRepos = deriveTaskRepos(detail, epicMemory, forgeRegistry);
|
|
176
|
+
|
|
173
177
|
let reopenedFromInReview = false;
|
|
174
178
|
if (isInReviewStatus(detail.status) && hasNewHumanReplyWhileWaiting(detail, botUser, jira)) {
|
|
175
179
|
const transition = await jira.transitionIssueToStatus(detail.key, "In Progress");
|
|
@@ -197,9 +201,14 @@ export async function processJiraIssue({
|
|
|
197
201
|
await pickUpToDoJiraIssue({ detail, jira, logger, saveProgress });
|
|
198
202
|
}
|
|
199
203
|
|
|
200
|
-
const
|
|
204
|
+
const previousRepos = Array.isArray(epicMemory?.repos) ? epicMemory.repos : [];
|
|
205
|
+
const previousRepoUrls = previousRepos.map((repo) => repo.url);
|
|
201
206
|
const nextRepoUrls = liveRepos.map((repo) => repo.url);
|
|
202
|
-
|
|
207
|
+
const branchChanged = liveRepos.some((repo) => {
|
|
208
|
+
const prev = previousRepos.find((r) => r.url === repo.url);
|
|
209
|
+
return (repo.working_branch || "") !== (prev?.working_branch || "");
|
|
210
|
+
});
|
|
211
|
+
if (JSON.stringify(previousRepoUrls) !== JSON.stringify(nextRepoUrls) || branchChanged) {
|
|
203
212
|
epicMemory.repos = liveRepos;
|
|
204
213
|
epicMemory = await services.saveEpicMemory(epicMemoryRecord.filePath, detail, epicMemory);
|
|
205
214
|
await logger.info("Updated epic repo knowledge", {
|
|
@@ -455,6 +464,7 @@ export async function processJiraIssue({
|
|
|
455
464
|
issueMemoryRecord,
|
|
456
465
|
issueMemory,
|
|
457
466
|
repos: liveRepos,
|
|
467
|
+
epicMemory,
|
|
458
468
|
logger,
|
|
459
469
|
claudeResult,
|
|
460
470
|
services
|