diff --git a/cmd/clawgo/cmd_status.go b/cmd/clawgo/cmd_status.go index 55d9e43..3062641 100644 --- a/cmd/clawgo/cmd_status.go +++ b/cmd/clawgo/cmd_status.go @@ -129,9 +129,12 @@ func statusCmd() { fmt.Printf(" - %s\n", key) } } - if summary, prio, nextRetry, dedupeHits, err := collectAutonomyTaskSummary(filepath.Join(workspace, "memory", "tasks.json")); err == nil { + if summary, prio, reasons, nextRetry, dedupeHits, err := collectAutonomyTaskSummary(filepath.Join(workspace, "memory", "tasks.json")); err == nil { fmt.Printf("Autonomy Tasks: todo=%d doing=%d waiting=%d blocked=%d done=%d dedupe_hits=%d\n", summary["todo"], summary["doing"], summary["waiting"], summary["blocked"], summary["done"], dedupeHits) fmt.Printf("Autonomy Priority: high=%d normal=%d low=%d\n", prio["high"], prio["normal"], prio["low"]) + if reasons["active_user"] > 0 || reasons["manual_pause"] > 0 || reasons["max_consecutive_stalls"] > 0 { + fmt.Printf("Autonomy Block Reasons: active_user=%d manual_pause=%d max_stalls=%d\n", reasons["active_user"], reasons["manual_pause"], reasons["max_consecutive_stalls"]) + } if nextRetry != "" { fmt.Printf("Autonomy Next Retry: %s\n", nextRetry) } @@ -249,25 +252,27 @@ func collectTriggerErrorCounts(path string) (map[string]int, error) { return counts, nil } -func collectAutonomyTaskSummary(path string) (map[string]int, map[string]int, string, int, error) { +func collectAutonomyTaskSummary(path string) (map[string]int, map[string]int, map[string]int, string, int, error) { data, err := os.ReadFile(path) if err != nil { if os.IsNotExist(err) { - return map[string]int{"todo": 0, "doing": 0, "waiting": 0, "blocked": 0, "done": 0}, map[string]int{"high": 0, "normal": 0, "low": 0}, "", 0, nil + return map[string]int{"todo": 0, "doing": 0, "waiting": 0, "blocked": 0, "done": 0}, map[string]int{"high": 0, "normal": 0, "low": 0}, map[string]int{"active_user": 0, "manual_pause": 0, "max_consecutive_stalls": 0}, "", 0, nil } - return nil, nil, "", 0, err + return nil, nil, nil, "", 0, err } var items []struct { - Status string `json:"status"` - Priority string `json:"priority"` - RetryAfter string `json:"retry_after"` - DedupeHits int `json:"dedupe_hits"` + Status string `json:"status"` + Priority string `json:"priority"` + BlockReason string `json:"block_reason"` + RetryAfter string `json:"retry_after"` + DedupeHits int `json:"dedupe_hits"` } if err := json.Unmarshal(data, &items); err != nil { - return nil, nil, "", 0, err + return nil, nil, nil, "", 0, err } summary := map[string]int{"todo": 0, "doing": 0, "waiting": 0, "blocked": 0, "done": 0} priorities := map[string]int{"high": 0, "normal": 0, "low": 0} + reasons := map[string]int{"active_user": 0, "manual_pause": 0, "max_consecutive_stalls": 0} nextRetry := "" nextRetryAt := time.Time{} totalDedupe := 0 @@ -277,6 +282,10 @@ func collectAutonomyTaskSummary(path string) (map[string]int, map[string]int, st summary[s]++ } totalDedupe += it.DedupeHits + r := strings.ToLower(strings.TrimSpace(it.BlockReason)) + if _, ok := reasons[r]; ok { + reasons[r]++ + } p := strings.ToLower(strings.TrimSpace(it.Priority)) if _, ok := priorities[p]; ok { priorities[p]++ @@ -292,7 +301,7 @@ func collectAutonomyTaskSummary(path string) (map[string]int, map[string]int, st } } } - return summary, priorities, nextRetry, totalDedupe, nil + return summary, priorities, reasons, nextRetry, totalDedupe, nil } func collectRecentSubagentSessions(sessionsDir string, limit int) ([]string, error) { diff --git a/pkg/autonomy/engine.go b/pkg/autonomy/engine.go index cb31e5b..13fe124 100644 --- a/pkg/autonomy/engine.go +++ b/pkg/autonomy/engine.go @@ -127,6 +127,7 @@ func (e *Engine) tick() { st.Status = "waiting" st.BlockReason = "manual_pause" e.writeReflectLog("waiting", st, "paused by manual switch") + e.writeTriggerAudit("waiting", st, "manual_pause") } } e.persistStateLocked() @@ -137,7 +138,9 @@ func (e *Engine) tick() { for _, st := range e.state { if st.Status == "running" { st.Status = "waiting" + st.BlockReason = "active_user" e.writeReflectLog("waiting", st, "paused due to active user conversation") + e.writeTriggerAudit("waiting", st, "active_user") } } e.persistStateLocked() @@ -220,9 +223,11 @@ func (e *Engine) tick() { continue } if st.Status == "waiting" { + reason := st.BlockReason st.Status = "idle" st.BlockReason = "" - e.writeReflectLog("resume", st, "user conversation idle, autonomy resumed") + e.writeReflectLog("resume", st, "autonomy resumed from waiting") + e.writeTriggerAudit("resume", st, reason) } if st.Status == "blocked" { if !st.RetryAfter.IsZero() && now.Before(st.RetryAfter) { @@ -578,13 +583,37 @@ func (e *Engine) pauseFilePath() string { return filepath.Join(e.opts.Workspace, "memory", "autonomy.pause") } +func (e *Engine) controlFilePath() string { + if strings.TrimSpace(e.opts.Workspace) == "" { + return "" + } + return filepath.Join(e.opts.Workspace, "memory", "autonomy.control.json") +} + func (e *Engine) hasManualPause() bool { p := e.pauseFilePath() if p == "" { return false } _, err := os.Stat(p) - return err == nil + if err == nil { + return true + } + ctrl := e.controlFilePath() + if ctrl == "" { + return false + } + data, rErr := os.ReadFile(ctrl) + if rErr != nil { + return false + } + var c struct { + Enabled bool `json:"enabled"` + } + if jErr := json.Unmarshal(data, &c); jErr != nil { + return false + } + return !c.Enabled } func (e *Engine) hasRecentUserActivity(now time.Time) bool {