claude-teammate 0.1.200 → 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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-teammate",
3
- "version": "0.1.200",
3
+ "version": "0.1.201",
4
4
  "description": "CLI bootstrapper for Claude Teammate.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -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
 
@@ -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: "string"
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
@@ -195,6 +195,7 @@ function normalizeEpicMemoryData(data) {
195
195
  }));
196
196
  normalized.facts = [...new Set(normalized.facts.map((fact) => String(fact).trim()).filter(Boolean))];
197
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();
198
199
 
199
200
  delete normalized.repo_url;
200
201
  delete normalized.local_repo_path;
@@ -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;
@@ -234,7 +234,7 @@ export function filterReposForActiveForge(repos, forgeRegistry) {
234
234
  });
235
235
  }
236
236
 
237
- export function selectIssueCreationRepo({ detail, issueMemory, repos }) {
237
+ export function selectIssueCreationRepo({ detail, issueMemory, repos, epicMemory }) {
238
238
  if (!Array.isArray(repos) || repos.length === 0) {
239
239
  return null;
240
240
  }
@@ -244,6 +244,14 @@ export function selectIssueCreationRepo({ detail, issueMemory, repos }) {
244
244
  }
245
245
 
246
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
+
247
255
  const latestExplicitRepo = getLatestExplicitIssueCreationRepo(detail, repoByUrl);
248
256
  if (latestExplicitRepo) {
249
257
  return latestExplicitRepo;
@@ -415,7 +423,7 @@ function ensureImplementationInstruction(body) {
415
423
  return `${normalizedBody}\n\n---\n\n${IMPLEMENTATION_INSTRUCTION}`.trim();
416
424
  }
417
425
 
418
- const BRANCH_KEYWORD_RE = /\b(?:branch|nhánh)\s*[:\s=]\s*([a-zA-Z0-9][a-zA-Z0-9_\-./]*)/iu;
426
+ const BRANCH_KEYWORD_RE = /\b(?:branch|nhánh)\s*[:=]\s*([a-zA-Z0-9][a-zA-Z0-9_\-./]*)/iu;
419
427
 
420
428
  function extractRepoBranchPairs(text) {
421
429
  const str = String(text || "");
@@ -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");
@@ -460,6 +464,7 @@ export async function processJiraIssue({
460
464
  issueMemoryRecord,
461
465
  issueMemory,
462
466
  repos: liveRepos,
467
+ epicMemory,
463
468
  logger,
464
469
  claudeResult,
465
470
  services