/** * Feedback Logger — writes structured feedback entries to FEEDBACK.md * alongside each skill's SKILL.md when failures are detected. * * Entries are appended automatically by the dojo when: * - A tool call fails after a skill was loaded (retries) * - The user corrects the agent after a skill was loaded * - A skill invocation exceeds the retry threshold * * The forge can read these entries to suggest SKILL.md improvements. */ import { existsSync, readFileSync, appendFileSync, statSync } from "node:fs"; import { join } from "node:path"; const SKILLS_DIR = join(process.env.HOME || "~", ".pi", "agent", "skills"); const MAX_FEEDBACK_SIZE = 50_000; // 50KB — rotate after this export interface FeedbackEntry { /** Skill name */ skill: string; /** ISO timestamp */ timestamp: string; /** What went wrong */ symptom: string; /** Error message or user correction text */ detail: string; /** Category */ category: "tool_error" | "user_correction" | "excessive_retries" | "timeout"; /** Session ID for traceability */ sessionId?: string; } /** * Append a feedback entry to a skill's FEEDBACK.md. * Creates the file if it doesn't exist. Rotates if too large. */ export function appendFeedback(entry: FeedbackEntry): boolean { const feedbackPath = join(SKILLS_DIR, entry.skill, "FEEDBACK.md"); const skillDir = join(SKILLS_DIR, entry.skill); // Only write feedback for skills that exist if (!existsSync(join(skillDir, "SKILL.md"))) return false; // Rotate if file is too large if (existsSync(feedbackPath)) { try { const size = statSync(feedbackPath).size; if (size > MAX_FEEDBACK_SIZE) { // Keep only the last half const content = readFileSync(feedbackPath, "utf8"); const lines = content.split("\n"); const half = Math.floor(lines.length / 2); const trimmed = "\n\n" + lines.slice(half).join("\n"); // Use writeFileSync to replace const { writeFileSync } = require("node:fs"); writeFileSync(feedbackPath, trimmed, "utf8"); } } catch { // Best effort } } const header = existsSync(feedbackPath) ? "" : `# Feedback Log Auto-generated by skill-evolution. Records failures and corrections for this skill. Used by the Skill Dojo to suggest improvements. --- `; const dateStr = entry.timestamp.slice(0, 10); const timeStr = entry.timestamp.slice(11, 16); const sessionRef = entry.sessionId ? ` (session: \`${entry.sessionId.slice(0, 8)}\`)` : ""; const block = `## ${dateStr} ${timeStr} — ${entry.category}${sessionRef} - **Symptom:** ${entry.symptom} - **Detail:** ${entry.detail} `; try { appendFileSync(feedbackPath, header + block, "utf8"); return true; } catch { return false; } } /** * Read recent feedback entries for a skill (last N entries). */ export function readFeedback(skill: string, maxEntries = 10): string { const feedbackPath = join(SKILLS_DIR, skill, "FEEDBACK.md"); if (!existsSync(feedbackPath)) return ""; try { const content = readFileSync(feedbackPath, "utf8"); // Extract entries (each starts with ## date) const entries = content.split(/(?=^## \d{4}-)/m).filter(e => e.startsWith("## ")); return entries.slice(-maxEntries).join("\n"); } catch { return ""; } } /** * Count feedback entries for a skill. */ export function countFeedback(skill: string): number { const feedbackPath = join(SKILLS_DIR, skill, "FEEDBACK.md"); if (!existsSync(feedbackPath)) return 0; try { const content = readFileSync(feedbackPath, "utf8"); return (content.match(/^## \d{4}-/gm) || []).length; } catch { return 0; } }