add skill execution audit and status metrics for openclaw alignment

This commit is contained in:
DBT
2026-02-24 05:52:18 +00:00
parent 9ae8de20ce
commit 4e15f06d31
2 changed files with 91 additions and 3 deletions

View File

@@ -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)

View File

@@ -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),