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 +1 -1
- package/src/claude/stream.js +10 -0
- package/src/claude.js +4 -0
- package/src/skills/detector.js +5 -0
- package/src/skills/fixer.js +32 -8
- package/src/skills/index.js +23 -7
- package/src/skills/locator.js +15 -7
- package/src/worker/jira-issue-workflow.js +7 -2
package/package.json
CHANGED
package/src/claude/stream.js
CHANGED
|
@@ -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,
|
package/src/skills/detector.js
CHANGED
|
@@ -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) {
|
package/src/skills/fixer.js
CHANGED
|
@@ -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
|
|
241
|
-
|
|
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], {
|
|
331
|
-
|
|
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 });
|
package/src/skills/index.js
CHANGED
|
@@ -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 {
|
|
6
|
+
import { findSkillLocationInRoots } from "./locator.js";
|
|
6
7
|
|
|
7
|
-
const SKILL_FIX_EVENTS_MAX =
|
|
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
|
-
|
|
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
|
-
|
|
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", {
|
|
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(
|
|
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
|
}
|
package/src/skills/locator.js
CHANGED
|
@@ -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
|
|
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(
|
|
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
|
|
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
|
-
|
|
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,
|