diff --git a/cmd/clawgo/cmd_status.go b/cmd/clawgo/cmd_status.go index 985338d..4be69ae 100644 --- a/cmd/clawgo/cmd_status.go +++ b/cmd/clawgo/cmd_status.go @@ -116,6 +116,12 @@ func statusCmd() { fmt.Printf(" %s: %d\n", trigger, agg[trigger]) } } + if total, okCnt, failCnt, top, err := collectSkillExecStats(filepath.Join(workspace, "memory", "skill-audit.jsonl")); err == nil && total > 0 { + fmt.Printf("Skill Exec: total=%d ok=%d fail=%d\n", total, okCnt, failCnt) + if top != "" { + fmt.Printf("Skill Exec Top: %s\n", top) + } + } sessionsDir := filepath.Join(filepath.Dir(configPath), "sessions") if kinds, err := collectSessionKindCounts(sessionsDir); err == nil && len(kinds) > 0 { @@ -389,6 +395,52 @@ func collectAutonomyTaskSummary(path string) (map[string]int, map[string]int, ma return summary, priorities, reasons, nextRetry, totalDedupe, nil } +func collectSkillExecStats(path string) (int, int, int, string, error) { + data, err := os.ReadFile(path) + if err != nil { + return 0, 0, 0, "", err + } + lines := strings.Split(strings.TrimSpace(string(data)), "\n") + total, okCnt, failCnt := 0, 0, 0 + skillCounts := map[string]int{} + for _, line := range lines { + line = strings.TrimSpace(line) + if line == "" { + continue + } + var row struct { + Skill string `json:"skill"` + OK bool `json:"ok"` + } + if err := json.Unmarshal([]byte(line), &row); err != nil { + continue + } + total++ + if row.OK { + okCnt++ + } else { + failCnt++ + } + s := strings.TrimSpace(row.Skill) + if s == "" { + s = "unknown" + } + skillCounts[s]++ + } + topSkill := "" + topN := 0 + for k, v := range skillCounts { + if v > topN { + topN = v + topSkill = k + } + } + if topSkill != "" { + topSkill = fmt.Sprintf("%s(%d)", topSkill, topN) + } + return total, okCnt, failCnt, topSkill, nil +} + func collectRecentSubagentSessions(sessionsDir string, limit int) ([]string, error) { indexPath := filepath.Join(sessionsDir, "sessions.json") data, err := os.ReadFile(indexPath) diff --git a/pkg/tools/skill_exec.go b/pkg/tools/skill_exec.go index 9e6b2f3..61814dd 100644 --- a/pkg/tools/skill_exec.go +++ b/pkg/tools/skill_exec.go @@ -67,20 +67,32 @@ func (t *SkillExecTool) Execute(ctx context.Context, args map[string]interface{} skillDir, err := t.resolveSkillDir(skill) if err != nil { + t.writeAudit(skill, script, false, err.Error()) + return "", err + } + if _, err := os.Stat(filepath.Join(skillDir, "SKILL.md")); err != nil { + err = fmt.Errorf("SKILL.md missing for skill: %s", skill) + t.writeAudit(skill, script, false, err.Error()) return "", err } relScript := filepath.Clean(script) if strings.Contains(relScript, "..") || filepath.IsAbs(relScript) { - return "", fmt.Errorf("script must be relative path inside skill directory") + err := fmt.Errorf("script must be relative path inside skill directory") + t.writeAudit(skill, script, false, err.Error()) + return "", err } if !strings.HasPrefix(relScript, "scripts"+string(os.PathSeparator)) { - return "", fmt.Errorf("script must be under scripts/ directory") + err := fmt.Errorf("script must be under scripts/ directory") + t.writeAudit(skill, script, false, err.Error()) + return "", err } scriptPath := filepath.Join(skillDir, relScript) if _, err := os.Stat(scriptPath); err != nil { - return "", fmt.Errorf("script not found: %s", scriptPath) + err = fmt.Errorf("script not found: %s", scriptPath) + t.writeAudit(skill, script, false, err.Error()) + return "", err } cmdArgs := []string{} @@ -97,11 +109,13 @@ func (t *SkillExecTool) Execute(ctx context.Context, args map[string]interface{} cmd, err := buildSkillCommand(runCtx, scriptPath, cmdArgs) if err != nil { + t.writeAudit(skill, script, false, err.Error()) return "", err } cmd.Dir = skillDir output, err := cmd.CombinedOutput() if err != nil { + t.writeAudit(skill, script, false, err.Error()) return "", fmt.Errorf("skill execution failed: %w\n%s", err, string(output)) } @@ -109,9 +123,31 @@ func (t *SkillExecTool) Execute(ctx context.Context, args map[string]interface{} if out == "" { out = "(no output)" } + t.writeAudit(skill, script, true, "") return out, nil } +func (t *SkillExecTool) writeAudit(skill, script string, ok bool, errText string) { + if strings.TrimSpace(t.workspace) == "" { + return + } + memDir := filepath.Join(t.workspace, "memory") + _ = os.MkdirAll(memDir, 0755) + row := fmt.Sprintf("{\"time\":%q,\"skill\":%q,\"script\":%q,\"ok\":%t,\"error\":%q}\n", + time.Now().UTC().Format(time.RFC3339), + strings.TrimSpace(skill), + strings.TrimSpace(script), + ok, + strings.TrimSpace(errText), + ) + f, err := os.OpenFile(filepath.Join(memDir, "skill-audit.jsonl"), os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0644) + if err != nil { + return + } + defer f.Close() + _, _ = f.WriteString(row) +} + func (t *SkillExecTool) resolveSkillDir(skill string) (string, error) { candidates := []string{ filepath.Join(t.workspace, "skills", skill),