claude-teammate 0.1.281 → 0.1.282

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.281",
3
+ "version": "0.1.282",
4
4
  "description": "CLI bootstrapper for Claude Teammate.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -72,6 +72,16 @@ export function parseClaudeOutput(output) {
72
72
  return direct;
73
73
  }
74
74
 
75
+ // Skill-fix schema: { analysis, files, reason }
76
+ if ("analysis" in direct && Array.isArray(direct.files)) {
77
+ return direct;
78
+ }
79
+
80
+ // Skill-correction schema: { isCorrection, skillName, correctionSummary }
81
+ if ("isCorrection" in direct) {
82
+ return direct;
83
+ }
84
+
75
85
  if ("result" in direct && typeof direct.result === "string") {
76
86
  return JSON.parse(direct.result);
77
87
  }
package/src/claude.js CHANGED
@@ -538,6 +538,8 @@ export async function runClaudeSkillCorrectionCheck({
538
538
  SKILL_CORRECTION_SYSTEM,
539
539
  `Previous bot response:\n<previous>\n${String(previousBotResponse || "").slice(0, 2500)}\n</previous>\n\nUser message:\n<user>\n${humanMessage}\n</user>\n\nIs the user correcting specific output behavior from a skill?`,
540
540
  {
541
+ model: "haiku",
542
+ effort: "low",
541
543
  runOpts: {
542
544
  cwd,
543
545
  timeout: timeoutMs || 30_000,
@@ -564,6 +566,7 @@ export function triggerSkillFeedbackFix({
564
566
  skillName,
565
567
  correctionSummary,
566
568
  projectRoot,
569
+ projectRoots,
567
570
  eventsRoot,
568
571
  logger,
569
572
  epicContext,
@@ -573,6 +576,7 @@ export function triggerSkillFeedbackFix({
573
576
  skillName,
574
577
  correctionSummary,
575
578
  projectRoot,
579
+ projectRoots,
576
580
  eventsRoot: eventsRoot || CLAUDE_TEAMMATE_ROOT,
577
581
  logger,
578
582
  invokeClaudeTask,
@@ -18,6 +18,9 @@ import { findSkillLocation } from "./locator.js";
18
18
  // load, close the attribution window. Prevents errors from unrelated work done
19
19
  // long after the skill completed being wrongly attributed to it.
20
20
  const MAX_ATTRIBUTION_DEPTH = 20;
21
+ // Cap the trail in inferSilentRecovery to prevent unbounded memory growth
22
+ // in long tasks that call many tools under one active skill.
23
+ const MAX_TRAIL_SIZE = 100;
21
24
 
22
25
  export function extractSkillFailures(streamLines) {
23
26
  const failures = [];
@@ -194,6 +197,8 @@ export function inferSilentRecovery(streamLines) {
194
197
  if (!isAttributable) continue;
195
198
  const isError = event.is_error === true || (toolName === "Bash" && looksLikeBashError(contentText));
196
199
  trail.push({ name: toolName, isError, contentText });
200
+ // Evict oldest entries once trail grows past the cap so memory stays bounded.
201
+ if (trail.length > MAX_TRAIL_SIZE) trail.splice(0, trail.length - MAX_TRAIL_SIZE);
197
202
 
198
203
  // Retry pattern: same tool seen earlier with isError=true, now called again.
199
204
  if (trail.length >= 2) {
@@ -5,7 +5,7 @@ import { tmpdir } from "node:os";
5
5
  import path from "node:path";
6
6
  import process from "node:process";
7
7
  import { promisify } from "node:util";
8
- import { parseRepoUrl } from "../forge/repo-host.js";
8
+ import { buildGitEnvForRepoUrl, parseRepoUrl } from "../forge/repo-host.js";
9
9
 
10
10
  const execFileAsync = promisify(execFile);
11
11
 
@@ -237,10 +237,11 @@ async function detectProvider(projectRoot) {
237
237
  encoding: "utf8",
238
238
  timeout: 5000
239
239
  });
240
- const repo = parseRepoUrl(stdout.trim());
241
- return { provider: repo.provider, repo };
240
+ const remoteUrl = stdout.trim();
241
+ const repo = parseRepoUrl(remoteUrl);
242
+ return { provider: repo.provider, repo, remoteUrl };
242
243
  } catch {
243
- return { provider: "github", repo: null };
244
+ return { provider: "github", repo: null, remoteUrl: null };
244
245
  }
245
246
  }
246
247
 
@@ -287,7 +288,7 @@ async function getDefaultBranch(projectRoot) {
287
288
 
288
289
  async function createSkillFixPR({ skillName, files, reason, location, projectRoot, logger }) {
289
290
  const branch = `fix/skill-${sanitizeBranchSegment(skillName)}`;
290
- const { provider, repo } = await detectProvider(projectRoot);
291
+ const { provider, repo, remoteUrl } = await detectProvider(projectRoot);
291
292
 
292
293
  // Fail fast for GitLab if token missing — without an MR API call we cannot
293
294
  // ship the fix upstream, and writing into the active working tree would
@@ -298,6 +299,19 @@ async function createSkillFixPR({ skillName, files, reason, location, projectRoo
298
299
  return { status: "error", error: msg };
299
300
  }
300
301
 
302
+ // Warn for GitHub if PAT is missing — git push will fall back to system
303
+ // credential helpers which may not be configured in containers / CI.
304
+ if (provider === "github" && !process.env.GITHUB_PAT) {
305
+ logger?.warn?.(
306
+ "skill-fix: GITHUB_PAT not set — git push will rely on system credential helper (may fail in CI/container)",
307
+ { skill: skillName }
308
+ );
309
+ }
310
+
311
+ // Build auth env so git fetch + push work in environments without a system
312
+ // credential helper (containers, CI). Mirrors how repo.js authenticates.
313
+ const gitAuthEnv = remoteUrl ? buildGitEnvForRepoUrl(remoteUrl) : undefined;
314
+
301
315
  // Idempotency: skip if PR/MR already open.
302
316
  // null = API unreachable; log and proceed rather than silently skipping (risk of
303
317
  // duplicate is low; failing to fix would be worse than a duplicate PR).
@@ -327,8 +341,17 @@ async function createSkillFixPR({ skillName, files, reason, location, projectRoo
327
341
  try {
328
342
  // Pull latest default branch so the fix is based on current HEAD, not stale local state.
329
343
  try {
330
- await execFileAsync("git", ["fetch", "origin", defaultBranch], { cwd: projectRoot, timeout: 60000 });
331
- } catch {}
344
+ await execFileAsync("git", ["fetch", "origin", defaultBranch], {
345
+ cwd: projectRoot,
346
+ timeout: 60000,
347
+ ...(gitAuthEnv && { env: gitAuthEnv })
348
+ });
349
+ } catch (fetchErr) {
350
+ logger?.warn?.("skill-fix: fetch failed, proceeding with stale base — fix PR may conflict on merge", {
351
+ skill: skillName,
352
+ error: fetchErr?.message
353
+ });
354
+ }
332
355
 
333
356
  // Prune stale worktree registrations left by prior SIGKILL — otherwise
334
357
  // `git worktree add -B branch` fails if the branch is still "checked out"
@@ -384,7 +407,8 @@ async function createSkillFixPR({ skillName, files, reason, location, projectRoo
384
407
  // --force-with-lease: safe retry after partial failure — only overwrites if remote matches expected
385
408
  await execFileAsync("git", ["push", "--force-with-lease", "-u", "origin", branch], {
386
409
  cwd: worktreePath,
387
- timeout: 60000
410
+ timeout: 60000,
411
+ ...(gitAuthEnv && { env: gitAuthEnv })
388
412
  });
389
413
 
390
414
  const prUrl = await openPR({ branch, prTitle, safeReason, defaultBranch, provider, repo, projectRoot });
@@ -1,10 +1,11 @@
1
1
  import { mkdir, readFile, writeFile } from "node:fs/promises";
2
2
  import path from "node:path";
3
+ import process from "node:process";
3
4
  import { extractSkillFailures, inferBypassedSkills, inferSilentRecovery } from "./detector.js";
4
5
  import { applySkillFix, generateSkillFix, readSkillFiles } from "./fixer.js";
5
- import { findSkillLocation } from "./locator.js";
6
+ import { findSkillLocationInRoots } from "./locator.js";
6
7
 
7
- const SKILL_FIX_EVENTS_MAX = 50;
8
+ const SKILL_FIX_EVENTS_MAX = parseInt(process.env.SKILL_FIX_EVENTS_MAX || "200", 10);
8
9
 
9
10
  // Module-level lock: prevents concurrent fix attempts for the same skill
10
11
  // (multiple tasks can detect the same failing skill simultaneously)
@@ -113,7 +114,8 @@ async function fixSkillsAsync(failures, projectRoot, eventsRoot, logger, invokeC
113
114
  if (!existing) {
114
115
  grouped.set(f.skillName, { ...f });
115
116
  } else {
116
- existing.errorContent = `${existing.errorContent}\n---\n[${f.errorType}]\n${f.errorContent || ""}`.slice(0, 1800);
117
+ const combined = `${existing.errorContent}\n---\n[${f.errorType}]\n${f.errorContent || ""}`;
118
+ existing.errorContent = combined.length > 1800 ? `${combined.slice(0, 1797)}…` : combined;
117
119
  // Keep the first errorType as the primary classification for telemetry,
118
120
  // but tag composite when more than one mode contributed.
119
121
  if (existing.errorType !== f.errorType && !existing.errorType.endsWith("+")) {
@@ -156,13 +158,17 @@ async function fixSkillsAsync(failures, projectRoot, eventsRoot, logger, invokeC
156
158
  * Haiku already classified it — this applies the fix using user correction as context.
157
159
  * Fire-and-forget safe: never throws.
158
160
  *
159
- * projectRoot: cloned repo path (skill lookup + git PR).
161
+ * projectRoot: primary cloned repo path (skill lookup + git PR).
162
+ * projectRoots: optional array of all cloned repo paths to search for the skill.
163
+ * Handles multi-repo projects where a skill may live in any repo.
164
+ * Falls back to [projectRoot] when omitted.
160
165
  * eventsRoot: claude-teammate root (event log destination).
161
166
  */
162
167
  export function scheduleSkillFixWithFeedback({
163
168
  skillName,
164
169
  correctionSummary,
165
170
  projectRoot,
171
+ projectRoots,
166
172
  eventsRoot,
167
173
  logger,
168
174
  invokeClaudeTask,
@@ -188,6 +194,7 @@ export function scheduleSkillFixWithFeedback({
188
194
  errorContent: `User correction: ${correctionSummary}`,
189
195
  errorType: "user-feedback",
190
196
  projectRoot,
197
+ projectRoots,
191
198
  eventsRoot: eventsRoot || projectRoot,
192
199
  logger,
193
200
  invokeClaudeTask,
@@ -203,20 +210,29 @@ async function fixSingleSkill({
203
210
  errorContent,
204
211
  errorType,
205
212
  projectRoot,
213
+ projectRoots,
206
214
  eventsRoot,
207
215
  logger,
208
216
  invokeClaudeTask,
209
217
  epicContext,
210
218
  issueKey
211
219
  }) {
212
- const location = findSkillLocation(skillName, projectRoot);
220
+ // Search all provided repo roots so multi-repo projects can find skills in any repo.
221
+ const roots = Array.isArray(projectRoots) && projectRoots.length > 0 ? projectRoots : [projectRoot];
222
+ const location = findSkillLocationInRoots(skillName, roots);
213
223
  if (!location) {
214
- logger?.info("skill-fix: skill file not found, skipping", { skill: skillName, projectRoot });
224
+ logger?.info("skill-fix: skill file not found in any project root, skipping", {
225
+ skill: skillName,
226
+ roots
227
+ });
215
228
  await appendSkillFixEvent(eventsRoot, { skill: skillName, errorType, status: "not-found" });
216
229
  return;
217
230
  }
218
231
  if (location.type === "plugin-cache") {
219
- logger?.info("skill-fix: plugin-cache skill is out of scope for auto-fix", { skill: skillName });
232
+ logger?.info(
233
+ "skill-fix: plugin-cache skill is out of scope for auto-fix (owned by plugin author; update the plugin to fix)",
234
+ { skill: skillName }
235
+ );
220
236
  await appendSkillFixEvent(eventsRoot, { skill: skillName, errorType, status: "scope-excluded" });
221
237
  return;
222
238
  }
@@ -3,29 +3,33 @@ import { homedir } from "node:os";
3
3
  import path from "node:path";
4
4
 
5
5
  /**
6
- * Find where a skill's SKILL.md lives.
7
- * Returns { path, type: 'repo' | 'global' } or null.
6
+ * Find where a skill's SKILL.md lives, searching multiple project roots.
7
+ * Returns { path, type: 'repo' | 'global', dir } or null.
8
8
  *
9
9
  * Skill structure on disk: {root}/skills/{skillName}/SKILL.md
10
10
  *
11
11
  * Repo skills → fix via PR.
12
12
  * Global skills → fix in-place, notify user.
13
+ * Plugin-cache → out of scope for auto-fix (owned by plugin authors).
14
+ *
15
+ * Checks all repo-level skill dirs first (in order), then global.
16
+ * This handles multi-repo projects where a skill may live in any cloned repo.
13
17
  */
14
- export function findSkillLocation(skillName, projectRoot) {
15
- // Normalize: strip leading slash, plugin prefix ("plugin:skill" → "skill")
18
+ export function findSkillLocationInRoots(skillName, projectRoots) {
16
19
  const raw = String(skillName || "")
17
20
  .replace(/^\//, "")
18
21
  .trim();
19
22
  const isPluginPrefixed = raw.includes(":");
20
23
  const base = isPluginPrefixed ? raw.split(":").pop() : raw;
21
- // Reject empty, dotfiles, or path traversal attempts
22
24
  if (!base || base.startsWith(".") || base.includes("/") || base.includes("\\")) return null;
23
25
 
24
26
  const home = homedir();
27
+ const roots = Array.isArray(projectRoots) ? projectRoots.filter(Boolean) : [];
28
+
25
29
  // Only repo-level and user-level skill dirs are managed; plugin cache skills
26
30
  // are owned by their plugin authors and intentionally out of scope.
27
31
  const candidates = [
28
- { root: path.join(projectRoot, ".claude", "skills"), type: "repo" },
32
+ ...roots.map((root) => ({ root: path.join(root, ".claude", "skills"), type: "repo" })),
29
33
  { root: path.join(home, ".claude", "skills"), type: "global" }
30
34
  ].map((c) => ({ ...c, path: path.join(c.root, base, "SKILL.md") }));
31
35
 
@@ -40,6 +44,10 @@ export function findSkillLocation(skillName, projectRoot) {
40
44
  // log "scope-excluded" instead of the misleading "not-found".
41
45
  return isPluginPrefixed ? { type: "plugin-cache", path: null, dir: null } : null;
42
46
  }
43
- // dir: skill directory containing SKILL.md and any companion scripts
44
47
  return { ...found, dir: path.dirname(found.path) };
45
48
  }
49
+
50
+ /** Single-root convenience wrapper — backward-compatible. */
51
+ export function findSkillLocation(skillName, projectRoot) {
52
+ return findSkillLocationInRoots(skillName, projectRoot ? [projectRoot] : []);
53
+ }
@@ -255,7 +255,9 @@ export async function processJiraIssue({
255
255
  botUser,
256
256
  jira,
257
257
  projectRoot,
258
- eventsRoot: projectRoot,
258
+ // eventsRoot intentionally omitted — triggerSkillFeedbackFix defaults to
259
+ // CLAUDE_TEAMMATE_ROOT (the stable teammate process root), which is correct.
260
+ // Passing projectRoot here would write events into the cloned repo dir instead.
259
261
  liveRepos,
260
262
  issueMemory,
261
263
  issueMemoryRecord,
@@ -769,7 +771,9 @@ export async function maybeCheckSkillCorrection({
769
771
  .sort(compareCommentsNewestFirst)
770
772
  .find((c) => jira.isBotAuthor(c.author, botUser) && getCommentTimestamp(c) < humanTs);
771
773
 
772
- const repoCwd = liveRepos?.find((r) => r?.local_path)?.local_path || projectRoot;
774
+ // Collect all repo local paths so the skill fixer can search every cloned repo.
775
+ const allRepoPaths = (liveRepos || []).map((r) => r?.local_path).filter(Boolean);
776
+ const repoCwd = allRepoPaths[0] || projectRoot;
773
777
 
774
778
  // Fire-and-forget by design: run-1 already produced the broken output, run-2
775
779
  // (next poll cycle) will pick up the fixed skill. We do not block the main
@@ -791,6 +795,7 @@ export async function maybeCheckSkillCorrection({
791
795
  skillName: result.skillName,
792
796
  correctionSummary: result.correctionSummary,
793
797
  projectRoot: repoCwd,
798
+ projectRoots: allRepoPaths.length > 0 ? allRepoPaths : undefined,
794
799
  eventsRoot,
795
800
  logger,
796
801
  epicContext,