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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-teammate",
3
- "version": "0.1.270",
3
+ "version": "0.1.271",
4
4
  "description": "CLI bootstrapper for Claude Teammate.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -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
  }
@@ -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
 
@@ -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({ skillName, errorContent, projectRoot, logger, invokeClaudeTask, epicContext }) {
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
  }