claude-teammate 0.1.270 → 0.1.271
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
|
@@ -143,6 +143,26 @@ export interface StatusResponse {
|
|
|
143
143
|
state: WorkerState;
|
|
144
144
|
}
|
|
145
145
|
|
|
146
|
+
export interface SkillFixEvent {
|
|
147
|
+
ts: string;
|
|
148
|
+
skill: string;
|
|
149
|
+
location?: "repo" | "global";
|
|
150
|
+
errorType?: string;
|
|
151
|
+
status:
|
|
152
|
+
| "generating"
|
|
153
|
+
| "pr-created"
|
|
154
|
+
| "patched"
|
|
155
|
+
| "patched-fallback"
|
|
156
|
+
| "pr-exists"
|
|
157
|
+
| "no-fix"
|
|
158
|
+
| "error"
|
|
159
|
+
| "lock-skipped"
|
|
160
|
+
| "not-found";
|
|
161
|
+
prUrl?: string;
|
|
162
|
+
files?: number;
|
|
163
|
+
error?: string;
|
|
164
|
+
}
|
|
165
|
+
|
|
146
166
|
// Singleton state — shared across all useStatus() calls
|
|
147
167
|
const _status = ref<StatusResponse | null>(null);
|
|
148
168
|
const _loading = ref(false);
|
|
@@ -179,3 +199,20 @@ export function useStatus() {
|
|
|
179
199
|
|
|
180
200
|
return { status: _status, loading: _loading, error: _error, loadStatus, startPolling, stopPolling };
|
|
181
201
|
}
|
|
202
|
+
|
|
203
|
+
const _skillFixes = ref<SkillFixEvent[]>([]);
|
|
204
|
+
|
|
205
|
+
export function useSkillFixes() {
|
|
206
|
+
const { apiFetch } = useApi();
|
|
207
|
+
|
|
208
|
+
async function loadSkillFixes() {
|
|
209
|
+
try {
|
|
210
|
+
const data = await apiFetch<{ events: SkillFixEvent[] }>("/api/skill-fixes");
|
|
211
|
+
_skillFixes.value = data.events ?? [];
|
|
212
|
+
} catch {
|
|
213
|
+
// ignore — endpoint may not exist yet
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
return { skillFixes: _skillFixes, loadSkillFixes };
|
|
218
|
+
}
|
|
@@ -276,6 +276,56 @@
|
|
|
276
276
|
</div>
|
|
277
277
|
</template>
|
|
278
278
|
|
|
279
|
+
<!-- SKILL SELF-REPAIR -->
|
|
280
|
+
<div class="card" v-if="skillFixes.length > 0">
|
|
281
|
+
<div class="card-header">
|
|
282
|
+
<span class="card-title">⚙ Skill Self-Repair</span>
|
|
283
|
+
<span style="font-family:var(--f-mono);font-size:.7rem;color:var(--ink-3)">{{ activeSkillFixes.length > 0 ? `${activeSkillFixes.length} active` : '' }} {{ skillFixes.length }} events</span>
|
|
284
|
+
</div>
|
|
285
|
+
<!-- Active fixes banner -->
|
|
286
|
+
<div v-if="activeSkillFixes.length > 0" style="padding:8px 12px;background:rgba(234,179,8,.08);border-bottom:1px solid rgba(234,179,8,.2);font-size:.8rem;color:var(--ink-2)">
|
|
287
|
+
<span style="color:#ca8a04;font-weight:600">Fixing:</span>
|
|
288
|
+
<span v-for="f in activeSkillFixes" :key="f.skill + f.ts" style="margin-left:8px">
|
|
289
|
+
<span style="font-family:var(--f-mono);color:var(--ink-1)">{{ f.skill }}</span>
|
|
290
|
+
<span style="color:var(--ink-3);margin-left:4px">({{ f.location || '?' }})</span>
|
|
291
|
+
</span>
|
|
292
|
+
</div>
|
|
293
|
+
<div class="table-wrap">
|
|
294
|
+
<table>
|
|
295
|
+
<thead>
|
|
296
|
+
<tr>
|
|
297
|
+
<th>Time</th>
|
|
298
|
+
<th>Skill</th>
|
|
299
|
+
<th>Location</th>
|
|
300
|
+
<th>Trigger</th>
|
|
301
|
+
<th>Status</th>
|
|
302
|
+
<th>Details</th>
|
|
303
|
+
</tr>
|
|
304
|
+
</thead>
|
|
305
|
+
<tbody>
|
|
306
|
+
<tr v-for="ev in skillFixes.slice(0, 20)" :key="ev.skill + ev.ts">
|
|
307
|
+
<td style="font-family:var(--f-mono);font-size:.75rem;color:var(--ink-3);white-space:nowrap">{{ formatRelative(ev.ts) }}</td>
|
|
308
|
+
<td style="font-family:var(--f-mono);font-weight:600">{{ ev.skill }}</td>
|
|
309
|
+
<td>
|
|
310
|
+
<span v-if="ev.location" :class="['tag', ev.location === 'repo' ? 'sky' : 'violet']">{{ ev.location }}</span>
|
|
311
|
+
<span v-else style="color:var(--ink-3)">—</span>
|
|
312
|
+
</td>
|
|
313
|
+
<td style="font-size:.75rem;color:var(--ink-2)">{{ skillFixTriggerLabel(ev.errorType) }}</td>
|
|
314
|
+
<td>
|
|
315
|
+
<span :class="['tag', skillFixStatusClass(ev.status)]">{{ ev.status }}</span>
|
|
316
|
+
</td>
|
|
317
|
+
<td style="font-size:.75rem">
|
|
318
|
+
<a v-if="ev.prUrl" :href="ev.prUrl" target="_blank" style="color:var(--sky);text-decoration:none">PR →</a>
|
|
319
|
+
<span v-else-if="ev.error" style="color:var(--red);font-family:var(--f-mono)" :title="ev.error">{{ ev.error.slice(0, 40) }}…</span>
|
|
320
|
+
<span v-else-if="ev.files" style="color:var(--ink-3)">{{ ev.files }} file{{ ev.files !== 1 ? 's' : '' }}</span>
|
|
321
|
+
<span v-else style="color:var(--ink-3)">—</span>
|
|
322
|
+
</td>
|
|
323
|
+
</tr>
|
|
324
|
+
</tbody>
|
|
325
|
+
</table>
|
|
326
|
+
</div>
|
|
327
|
+
</div>
|
|
328
|
+
|
|
279
329
|
<!-- ISSUE DRAWER (always mounted) -->
|
|
280
330
|
<IssueDrawer
|
|
281
331
|
:issue="drawerIssue"
|
|
@@ -288,9 +338,10 @@
|
|
|
288
338
|
</template>
|
|
289
339
|
|
|
290
340
|
<script setup lang="ts">
|
|
291
|
-
import type { DraftPr, ReviewPr, StuckTask, WorkflowIssue } from "~/composables/useStatus";
|
|
341
|
+
import type { DraftPr, ReviewPr, SkillFixEvent, StuckTask, WorkflowIssue } from "~/composables/useStatus";
|
|
292
342
|
|
|
293
343
|
const { status, loadStatus } = useStatus();
|
|
344
|
+
const { skillFixes, loadSkillFixes } = useSkillFixes();
|
|
294
345
|
const { apiFetch } = useApi();
|
|
295
346
|
// biome-ignore lint/correctness/noUnusedVariables: used in template
|
|
296
347
|
const { formatTime, formatRelative, formatDuration, stateLabel, shortenUrl } = useHelpers();
|
|
@@ -461,11 +512,33 @@ function _closeDrawer() {
|
|
|
461
512
|
drawerOpen.value = false;
|
|
462
513
|
}
|
|
463
514
|
|
|
515
|
+
// biome-ignore lint/correctness/noUnusedVariables: used in template
|
|
516
|
+
const activeSkillFixes = computed(() => (skillFixes.value as SkillFixEvent[]).filter((e) => e.status === "generating"));
|
|
517
|
+
|
|
518
|
+
// biome-ignore lint/correctness/noUnusedVariables: used in template
|
|
519
|
+
function skillFixStatusClass(status: SkillFixEvent["status"]): string {
|
|
520
|
+
if (status === "pr-created" || status === "patched" || status === "patched-fallback") return "green";
|
|
521
|
+
if (status === "generating") return "yellow";
|
|
522
|
+
if (status === "error" || status === "no-fix") return "red";
|
|
523
|
+
return "";
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
// biome-ignore lint/correctness/noUnusedVariables: used in template
|
|
527
|
+
function skillFixTriggerLabel(errorType?: string): string {
|
|
528
|
+
if (!errorType) return "—";
|
|
529
|
+
if (errorType === "user-feedback") return "user feedback";
|
|
530
|
+
if (errorType === "bash-error-in-skill") return "bash error";
|
|
531
|
+
if (errorType === "tool-error-in-skill") return "mcp error";
|
|
532
|
+
if (errorType === "skill-load-failed") return "load failed";
|
|
533
|
+
return errorType;
|
|
534
|
+
}
|
|
535
|
+
|
|
464
536
|
async function _refresh() {
|
|
465
|
-
await loadStatus();
|
|
537
|
+
await Promise.all([loadStatus(), loadSkillFixes()]);
|
|
466
538
|
}
|
|
467
539
|
|
|
468
540
|
onMounted(async () => {
|
|
541
|
+
loadSkillFixes();
|
|
469
542
|
try {
|
|
470
543
|
const config = await apiFetch<{ config: Record<string, string> }>("/api/config");
|
|
471
544
|
const url = config?.config?.JIRA_BASE_URL || "";
|
|
@@ -42,6 +42,7 @@ const TABS = [
|
|
|
42
42
|
{ id: "draft-pr", label: "Draft PR", tag: "draft-pr" },
|
|
43
43
|
{ id: "review-pr", label: "PR Review", tag: "review-pr" },
|
|
44
44
|
{ id: "review-discussion", label: "Discussions", tag: "review-discussion" },
|
|
45
|
+
{ id: "skill-fix", label: "Skill Fix", tag: "skill-fix" },
|
|
45
46
|
{ id: "system", label: "System", tag: "" }
|
|
46
47
|
] as const;
|
|
47
48
|
|
|
@@ -66,6 +67,7 @@ function getTag(line: string): string | null {
|
|
|
66
67
|
if (/^(?:Draft PR\s|Draft pull request\s|Pull request\s)/i.test(body)) return "draft-pr";
|
|
67
68
|
if (/^PR\s+review\s/i.test(body)) return "review-pr";
|
|
68
69
|
if (/^PR\s+discussion\s/i.test(body)) return "review-discussion";
|
|
70
|
+
if (/skill-fix:/i.test(body)) return "skill-fix";
|
|
69
71
|
|
|
70
72
|
return null;
|
|
71
73
|
}
|
package/src/dashboard/server.js
CHANGED
|
@@ -139,6 +139,10 @@ async function handleRequest(req, res, projectRoot, runtimePaths) {
|
|
|
139
139
|
return handleGetUsage(res, runtimePaths);
|
|
140
140
|
}
|
|
141
141
|
|
|
142
|
+
if (pathname === "/api/skill-fixes" && req.method === "GET") {
|
|
143
|
+
return handleGetSkillFixes(res, projectRoot);
|
|
144
|
+
}
|
|
145
|
+
|
|
142
146
|
const resetMatch = pathname.match(/^\/api\/issues\/([^/]+)\/reset$/u);
|
|
143
147
|
if (resetMatch && req.method === "POST") {
|
|
144
148
|
return handleResetIssue(res, projectRoot, resetMatch[1]);
|
|
@@ -838,6 +842,21 @@ async function parseJsonlFile(filePath, sessionIdToMeta, tokensBySession) {
|
|
|
838
842
|
}
|
|
839
843
|
}
|
|
840
844
|
|
|
845
|
+
async function handleGetSkillFixes(res, projectRoot) {
|
|
846
|
+
const file = path.join(projectRoot, "memory", "skill-fixes.json");
|
|
847
|
+
try {
|
|
848
|
+
const content = await readFile(file, "utf8");
|
|
849
|
+
const events = JSON.parse(content);
|
|
850
|
+
sendJson(res, 200, { events: Array.isArray(events) ? events : [] });
|
|
851
|
+
} catch (err) {
|
|
852
|
+
if (err.code === "ENOENT") {
|
|
853
|
+
sendJson(res, 200, { events: [] });
|
|
854
|
+
return;
|
|
855
|
+
}
|
|
856
|
+
throw err;
|
|
857
|
+
}
|
|
858
|
+
}
|
|
859
|
+
|
|
841
860
|
function sanitizePath(rawPath) {
|
|
842
861
|
const decoded = decodeURIComponent(rawPath);
|
|
843
862
|
|
package/src/skills/index.js
CHANGED
|
@@ -1,7 +1,11 @@
|
|
|
1
|
+
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
1
3
|
import { extractSkillFailures } from "./detector.js";
|
|
2
4
|
import { applySkillFix, generateSkillFix, readSkillFiles } from "./fixer.js";
|
|
3
5
|
import { findSkillLocation } from "./locator.js";
|
|
4
6
|
|
|
7
|
+
const SKILL_FIX_EVENTS_MAX = 50;
|
|
8
|
+
|
|
5
9
|
// Module-level lock: prevents concurrent fix attempts for the same skill
|
|
6
10
|
// (multiple tasks can detect the same failing skill simultaneously)
|
|
7
11
|
const activeFixLocks = new Set();
|
|
@@ -32,19 +36,20 @@ async function fixSkillsAsync(failures, projectRoot, logger, invokeClaudeTask) {
|
|
|
32
36
|
// Deduplicate by skillName — one fix per skill per invocation
|
|
33
37
|
const seen = new Set();
|
|
34
38
|
|
|
35
|
-
for (const { skillName, errorContent } of failures) {
|
|
39
|
+
for (const { skillName, errorContent, errorType } of failures) {
|
|
36
40
|
if (!skillName || skillName === "unknown" || seen.has(skillName)) continue;
|
|
37
41
|
seen.add(skillName);
|
|
38
42
|
|
|
39
43
|
// Skip if another concurrent task is already fixing this skill
|
|
40
44
|
if (activeFixLocks.has(skillName)) {
|
|
41
45
|
logger?.info("skill-fix: fix already in progress, skipping", { skill: skillName });
|
|
46
|
+
await appendSkillFixEvent(projectRoot, { skill: skillName, errorType, status: "lock-skipped" });
|
|
42
47
|
continue;
|
|
43
48
|
}
|
|
44
49
|
|
|
45
50
|
activeFixLocks.add(skillName);
|
|
46
51
|
try {
|
|
47
|
-
await fixSingleSkill({ skillName, errorContent, projectRoot, logger, invokeClaudeTask });
|
|
52
|
+
await fixSingleSkill({ skillName, errorContent, errorType, projectRoot, logger, invokeClaudeTask });
|
|
48
53
|
} finally {
|
|
49
54
|
activeFixLocks.delete(skillName);
|
|
50
55
|
}
|
|
@@ -74,6 +79,7 @@ export function scheduleSkillFixWithFeedback({
|
|
|
74
79
|
fixSingleSkill({
|
|
75
80
|
skillName,
|
|
76
81
|
errorContent: `User correction: ${correctionSummary}`,
|
|
82
|
+
errorType: "user-feedback",
|
|
77
83
|
projectRoot,
|
|
78
84
|
logger,
|
|
79
85
|
invokeClaudeTask,
|
|
@@ -83,10 +89,19 @@ export function scheduleSkillFixWithFeedback({
|
|
|
83
89
|
.finally(() => activeFixLocks.delete(skillName));
|
|
84
90
|
}
|
|
85
91
|
|
|
86
|
-
async function fixSingleSkill({
|
|
92
|
+
async function fixSingleSkill({
|
|
93
|
+
skillName,
|
|
94
|
+
errorContent,
|
|
95
|
+
errorType,
|
|
96
|
+
projectRoot,
|
|
97
|
+
logger,
|
|
98
|
+
invokeClaudeTask,
|
|
99
|
+
epicContext
|
|
100
|
+
}) {
|
|
87
101
|
const location = findSkillLocation(skillName, projectRoot);
|
|
88
102
|
if (!location) {
|
|
89
103
|
logger?.info("skill-fix: skill file not found, skipping", { skill: skillName });
|
|
104
|
+
await appendSkillFixEvent(projectRoot, { skill: skillName, errorType, status: "not-found" });
|
|
90
105
|
return;
|
|
91
106
|
}
|
|
92
107
|
|
|
@@ -94,10 +109,18 @@ async function fixSingleSkill({ skillName, errorContent, projectRoot, logger, in
|
|
|
94
109
|
if (skillFiles.length === 0) return;
|
|
95
110
|
|
|
96
111
|
logger?.info("skill-fix: generating fix", { skill: skillName, location: location.type, files: skillFiles.length });
|
|
112
|
+
await appendSkillFixEvent(projectRoot, {
|
|
113
|
+
skill: skillName,
|
|
114
|
+
location: location.type,
|
|
115
|
+
errorType,
|
|
116
|
+
status: "generating",
|
|
117
|
+
files: skillFiles.length
|
|
118
|
+
});
|
|
97
119
|
|
|
98
120
|
const fix = await generateSkillFix(skillName, skillFiles, errorContent, projectRoot, invokeClaudeTask, epicContext);
|
|
99
121
|
if (!Array.isArray(fix?.files) || fix.files.length === 0) {
|
|
100
122
|
logger?.info("skill-fix: no fix generated", { skill: skillName });
|
|
123
|
+
await appendSkillFixEvent(projectRoot, { skill: skillName, location: location.type, errorType, status: "no-fix" });
|
|
101
124
|
return;
|
|
102
125
|
}
|
|
103
126
|
|
|
@@ -111,4 +134,30 @@ async function fixSingleSkill({ skillName, errorContent, projectRoot, logger, in
|
|
|
111
134
|
});
|
|
112
135
|
|
|
113
136
|
logger?.info(`skill-fix: ${result.status}`, { skill: skillName, ...result });
|
|
137
|
+
await appendSkillFixEvent(projectRoot, {
|
|
138
|
+
skill: skillName,
|
|
139
|
+
location: location.type,
|
|
140
|
+
errorType,
|
|
141
|
+
status: result.status,
|
|
142
|
+
prUrl: result.prUrl,
|
|
143
|
+
error: result.error,
|
|
144
|
+
files: fix.files.length
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
async function appendSkillFixEvent(projectRoot, fields) {
|
|
149
|
+
try {
|
|
150
|
+
const file = path.join(projectRoot, "memory", "skill-fixes.json");
|
|
151
|
+
await mkdir(path.dirname(file), { recursive: true });
|
|
152
|
+
let events = [];
|
|
153
|
+
try {
|
|
154
|
+
events = JSON.parse(await readFile(file, "utf8"));
|
|
155
|
+
} catch {}
|
|
156
|
+
if (!Array.isArray(events)) events = [];
|
|
157
|
+
events.unshift({ ts: new Date().toISOString(), ...fields });
|
|
158
|
+
events = events.slice(0, SKILL_FIX_EVENTS_MAX);
|
|
159
|
+
await writeFile(file, JSON.stringify(events, null, 2), "utf8");
|
|
160
|
+
} catch {
|
|
161
|
+
// never throw — event logging is best-effort
|
|
162
|
+
}
|
|
114
163
|
}
|