package main import ( "context" "encoding/json" "fmt" "io" "os" "time" "github.com/spf13/cobra" "github.com/steveyegge/beads/internal/compact" "github.com/steveyegge/beads/internal/storage/sqlite" ) var ( compactDryRun bool compactTier int compactAll bool compactID string compactForce bool compactBatch int compactWorkers int compactStats bool compactAnalyze bool compactApply bool compactAuto bool compactSummary string compactActor string compactLimit int ) var compactCmd = &cobra.Command{ Use: "compact", Short: "Compact old closed issues to save space", Long: `Compact old closed issues using semantic summarization. Compaction reduces database size by summarizing closed issues that are no longer actively referenced. This is permanent graceful decay - original content is discarded. Modes: - Analyze: Export candidates for agent review (no API key needed) - Apply: Accept agent-provided summary (no API key needed) - Auto: AI-powered compaction (requires ANTHROPIC_API_KEY, legacy) Tiers: - Tier 1: Semantic compression (30 days closed, 70% reduction) - Tier 2: Ultra compression (90 days closed, 95% reduction) Examples: # Agent-driven workflow (recommended) bd compact --analyze --json # Get candidates with full content bd compact --apply --id bd-42 --summary summary.txt bd compact --apply --id bd-42 --summary - < summary.txt # Legacy AI-powered workflow bd compact --auto --dry-run # Preview candidates bd compact --auto --all # Compact all eligible issues bd compact --auto --id bd-42 # Compact specific issue # Statistics bd compact --stats # Show statistics `, Run: func(_ *cobra.Command, _ []string) { ctx := rootCtx // Handle compact stats first if compactStats { if daemonClient != nil { runCompactStatsRPC() } else { sqliteStore, ok := store.(*sqlite.SQLiteStorage) if !ok { fmt.Fprintf(os.Stderr, "Error: compact requires SQLite storage\n") os.Exit(1) } runCompactStats(ctx, sqliteStore) } return } // Count active modes activeModes := 0 if compactAnalyze { activeModes++ } if compactApply { activeModes++ } if compactAuto { activeModes++ } // Check for exactly one mode if activeModes == 0 { fmt.Fprintf(os.Stderr, "Error: must specify one mode: --analyze, --apply, or --auto\n") os.Exit(1) } if activeModes > 1 { fmt.Fprintf(os.Stderr, "Error: cannot use multiple modes together (--analyze, --apply, --auto are mutually exclusive)\n") os.Exit(1) } // Handle analyze mode (requires direct database access) if compactAnalyze { if err := ensureDirectMode("compact --analyze requires direct database access"); err != nil { fmt.Fprintf(os.Stderr, "Error: %v\n", err) fmt.Fprintf(os.Stderr, "Hint: Use --no-daemon flag to bypass daemon and access database directly\n") os.Exit(1) } sqliteStore, ok := store.(*sqlite.SQLiteStorage) if !ok { fmt.Fprintf(os.Stderr, "Error: failed to open database in direct mode\n") fmt.Fprintf(os.Stderr, "Hint: Ensure .beads/beads.db exists and is readable\n") os.Exit(1) } runCompactAnalyze(ctx, sqliteStore) return } // Handle apply mode (requires direct database access) if compactApply { if err := ensureDirectMode("compact --apply requires direct database access"); err != nil { fmt.Fprintf(os.Stderr, "Error: %v\n", err) fmt.Fprintf(os.Stderr, "Hint: Use --no-daemon flag to bypass daemon and access database directly\n") os.Exit(1) } if compactID == "" { fmt.Fprintf(os.Stderr, "Error: --apply requires --id\n") os.Exit(1) } if compactSummary == "" { fmt.Fprintf(os.Stderr, "Error: --apply requires --summary\n") os.Exit(1) } sqliteStore, ok := store.(*sqlite.SQLiteStorage) if !ok { fmt.Fprintf(os.Stderr, "Error: failed to open database in direct mode\n") fmt.Fprintf(os.Stderr, "Hint: Ensure .beads/beads.db exists and is readable\n") os.Exit(1) } runCompactApply(ctx, sqliteStore) return } // Handle auto mode (legacy) if compactAuto { // Validation checks if compactID != "" && compactAll { fmt.Fprintf(os.Stderr, "Error: cannot use --id and --all together\n") os.Exit(1) } if compactForce && compactID == "" { fmt.Fprintf(os.Stderr, "Error: --force requires --id\n") os.Exit(1) } if compactID == "" && !compactAll && !compactDryRun { fmt.Fprintf(os.Stderr, "Error: must specify --all, --id, or --dry-run\n") os.Exit(1) } // Use RPC if daemon available, otherwise direct mode if daemonClient != nil { runCompactRPC(ctx) return } // Fallback to direct mode apiKey := os.Getenv("ANTHROPIC_API_KEY") if apiKey == "" && !compactDryRun { fmt.Fprintf(os.Stderr, "Error: --auto mode requires ANTHROPIC_API_KEY environment variable\n") os.Exit(1) } sqliteStore, ok := store.(*sqlite.SQLiteStorage) if !ok { fmt.Fprintf(os.Stderr, "Error: compact requires SQLite storage\n") os.Exit(1) } config := &compact.Config{ APIKey: apiKey, Concurrency: compactWorkers, DryRun: compactDryRun, } compactor, err := compact.New(sqliteStore, apiKey, config) if err != nil { fmt.Fprintf(os.Stderr, "Error: failed to create compactor: %v\n", err) os.Exit(1) } if compactID != "" { runCompactSingle(ctx, compactor, sqliteStore, compactID) return } runCompactAll(ctx, compactor, sqliteStore) } }, } func runCompactSingle(ctx context.Context, compactor *compact.Compactor, store *sqlite.SQLiteStorage, issueID string) { start := time.Now() if !compactForce { eligible, reason, err := store.CheckEligibility(ctx, issueID, compactTier) if err != nil { fmt.Fprintf(os.Stderr, "Error: failed to check eligibility: %v\n", err) os.Exit(1) } if !eligible { fmt.Fprintf(os.Stderr, "Error: %s is not eligible for Tier %d compaction: %s\n", issueID, compactTier, reason) os.Exit(1) } } issue, err := store.GetIssue(ctx, issueID) if err != nil { fmt.Fprintf(os.Stderr, "Error: failed to get issue: %v\n", err) os.Exit(1) } originalSize := len(issue.Description) + len(issue.Design) + len(issue.Notes) + len(issue.AcceptanceCriteria) if compactDryRun { if jsonOutput { output := map[string]interface{}{ "dry_run": true, "tier": compactTier, "issue_id": issueID, "original_size": originalSize, "estimated_reduction": "70-80%", } outputJSON(output) return } fmt.Printf("DRY RUN - Tier %d compaction\n\n", compactTier) fmt.Printf("Issue: %s\n", issueID) fmt.Printf("Original size: %d bytes\n", originalSize) fmt.Printf("Estimated reduction: 70-80%%\n") return } var compactErr error if compactTier == 1 { compactErr = compactor.CompactTier1(ctx, issueID) } else { fmt.Fprintf(os.Stderr, "Error: Tier 2 compaction not yet implemented\n") os.Exit(1) } if compactErr != nil { fmt.Fprintf(os.Stderr, "Error: %v\n", compactErr) os.Exit(1) } issue, err = store.GetIssue(ctx, issueID) if err != nil { fmt.Fprintf(os.Stderr, "Error: failed to get updated issue: %v\n", err) os.Exit(1) } compactedSize := len(issue.Description) savingBytes := originalSize - compactedSize elapsed := time.Since(start) if jsonOutput { output := map[string]interface{}{ "success": true, "tier": compactTier, "issue_id": issueID, "original_size": originalSize, "compacted_size": compactedSize, "saved_bytes": savingBytes, "reduction_pct": float64(savingBytes) / float64(originalSize) * 100, "elapsed_ms": elapsed.Milliseconds(), } outputJSON(output) return } fmt.Printf("✓ Compacted %s (Tier %d)\n", issueID, compactTier) fmt.Printf(" %d → %d bytes (saved %d, %.1f%%)\n", originalSize, compactedSize, savingBytes, float64(savingBytes)/float64(originalSize)*100) fmt.Printf(" Time: %v\n", elapsed) // Schedule auto-flush to export changes markDirtyAndScheduleFlush() } func runCompactAll(ctx context.Context, compactor *compact.Compactor, store *sqlite.SQLiteStorage) { start := time.Now() var candidates []string if compactTier == 1 { tier1, err := store.GetTier1Candidates(ctx) if err != nil { fmt.Fprintf(os.Stderr, "Error: failed to get candidates: %v\n", err) os.Exit(1) } for _, c := range tier1 { candidates = append(candidates, c.IssueID) } } else { tier2, err := store.GetTier2Candidates(ctx) if err != nil { fmt.Fprintf(os.Stderr, "Error: failed to get candidates: %v\n", err) os.Exit(1) } for _, c := range tier2 { candidates = append(candidates, c.IssueID) } } if len(candidates) == 0 { if jsonOutput { outputJSON(map[string]interface{}{ "success": true, "count": 0, "message": "No eligible candidates", }) return } fmt.Println("No eligible candidates for compaction") return } if compactDryRun { totalSize := 0 for _, id := range candidates { issue, err := store.GetIssue(ctx, id) if err != nil { continue } totalSize += len(issue.Description) + len(issue.Design) + len(issue.Notes) + len(issue.AcceptanceCriteria) } if jsonOutput { output := map[string]interface{}{ "dry_run": true, "tier": compactTier, "candidate_count": len(candidates), "total_size_bytes": totalSize, "estimated_reduction": "70-80%", } outputJSON(output) return } fmt.Printf("DRY RUN - Tier %d compaction\n\n", compactTier) fmt.Printf("Candidates: %d issues\n", len(candidates)) fmt.Printf("Total size: %d bytes\n", totalSize) fmt.Printf("Estimated reduction: 70-80%%\n") return } if !jsonOutput { fmt.Printf("Compacting %d issues (Tier %d)...\n\n", len(candidates), compactTier) } results, err := compactor.CompactTier1Batch(ctx, candidates) if err != nil { fmt.Fprintf(os.Stderr, "Error: batch compaction failed: %v\n", err) os.Exit(1) } successCount := 0 failCount := 0 totalSaved := 0 totalOriginal := 0 for i, result := range results { if !jsonOutput { fmt.Printf("[%s] %d/%d\r", progressBar(i+1, len(results)), i+1, len(results)) } if result.Err != nil { failCount++ } else { successCount++ totalOriginal += result.OriginalSize totalSaved += (result.OriginalSize - result.CompactedSize) } } elapsed := time.Since(start) if jsonOutput { output := map[string]interface{}{ "success": true, "tier": compactTier, "total": len(results), "succeeded": successCount, "failed": failCount, "saved_bytes": totalSaved, "original_size": totalOriginal, "elapsed_ms": elapsed.Milliseconds(), } outputJSON(output) return } fmt.Printf("\n\nCompleted in %v\n\n", elapsed) fmt.Printf("Summary:\n") fmt.Printf(" Succeeded: %d\n", successCount) fmt.Printf(" Failed: %d\n", failCount) if totalOriginal > 0 { fmt.Printf(" Saved: %d bytes (%.1f%%)\n", totalSaved, float64(totalSaved)/float64(totalOriginal)*100) } // Schedule auto-flush to export changes if successCount > 0 { markDirtyAndScheduleFlush() } } func runCompactStats(ctx context.Context, store *sqlite.SQLiteStorage) { tier1, err := store.GetTier1Candidates(ctx) if err != nil { fmt.Fprintf(os.Stderr, "Error: failed to get Tier 1 candidates: %v\n", err) os.Exit(1) } tier2, err := store.GetTier2Candidates(ctx) if err != nil { fmt.Fprintf(os.Stderr, "Error: failed to get Tier 2 candidates: %v\n", err) os.Exit(1) } tier1Size := 0 for _, c := range tier1 { tier1Size += c.OriginalSize } tier2Size := 0 for _, c := range tier2 { tier2Size += c.OriginalSize } if jsonOutput { output := map[string]interface{}{ "tier1": map[string]interface{}{ "candidates": len(tier1), "total_size": tier1Size, }, "tier2": map[string]interface{}{ "candidates": len(tier2), "total_size": tier2Size, }, } outputJSON(output) return } fmt.Println("Compaction Statistics") fmt.Printf("Tier 1 (30+ days closed):\n") fmt.Printf(" Candidates: %d\n", len(tier1)) fmt.Printf(" Total size: %d bytes\n", tier1Size) if tier1Size > 0 { fmt.Printf(" Estimated savings: %d bytes (70%%)\n\n", tier1Size*7/10) } fmt.Printf("Tier 2 (90+ days closed, Tier 1 compacted):\n") fmt.Printf(" Candidates: %d\n", len(tier2)) fmt.Printf(" Total size: %d bytes\n", tier2Size) if tier2Size > 0 { fmt.Printf(" Estimated savings: %d bytes (95%%)\n", tier2Size*95/100) } } func progressBar(current, total int) string { const width = 40 if total == 0 { return "[" + string(make([]byte, width)) + "]" } filled := (current * width) / total bar := "" for i := 0; i < width; i++ { if i < filled { bar += "█" } else { bar += " " } } return "[" + bar + "]" } //nolint:unparam // ctx may be used in future for cancellation func runCompactRPC(_ context.Context) { if compactID != "" && compactAll { fmt.Fprintf(os.Stderr, "Error: cannot use --id and --all together\n") os.Exit(1) } if compactForce && compactID == "" { fmt.Fprintf(os.Stderr, "Error: --force requires --id\n") os.Exit(1) } if compactID == "" && !compactAll && !compactDryRun { fmt.Fprintf(os.Stderr, "Error: must specify --all, --id, or --dry-run\n") os.Exit(1) } apiKey := os.Getenv("ANTHROPIC_API_KEY") if apiKey == "" && !compactDryRun { fmt.Fprintf(os.Stderr, "Error: ANTHROPIC_API_KEY environment variable not set\n") os.Exit(1) } args := map[string]interface{}{ "tier": compactTier, "dry_run": compactDryRun, "force": compactForce, "all": compactAll, "api_key": apiKey, "workers": compactWorkers, "batch_size": compactBatch, } if compactID != "" { args["issue_id"] = compactID } resp, err := daemonClient.Execute("compact", args) if err != nil { fmt.Fprintf(os.Stderr, "Error: %v\n", err) os.Exit(1) } if !resp.Success { fmt.Fprintf(os.Stderr, "Error: %s\n", resp.Error) os.Exit(1) } if jsonOutput { fmt.Println(string(resp.Data)) return } var result struct { Success bool `json:"success"` IssueID string `json:"issue_id,omitempty"` OriginalSize int `json:"original_size,omitempty"` CompactedSize int `json:"compacted_size,omitempty"` Reduction string `json:"reduction,omitempty"` Duration string `json:"duration,omitempty"` DryRun bool `json:"dry_run,omitempty"` Results []struct { IssueID string `json:"issue_id"` Success bool `json:"success"` Error string `json:"error,omitempty"` OriginalSize int `json:"original_size,omitempty"` CompactedSize int `json:"compacted_size,omitempty"` Reduction string `json:"reduction,omitempty"` } `json:"results,omitempty"` } if err := json.Unmarshal(resp.Data, &result); err != nil { fmt.Fprintf(os.Stderr, "Error parsing response: %v\n", err) os.Exit(1) } if compactID != "" { if result.DryRun { fmt.Printf("DRY RUN - Tier %d compaction\n\n", compactTier) fmt.Printf("Issue: %s\n", compactID) fmt.Printf("Original size: %d bytes\n", result.OriginalSize) fmt.Printf("Estimated reduction: %s\n", result.Reduction) } else { fmt.Printf("Successfully compacted %s\n", result.IssueID) fmt.Printf("Original size: %d bytes\n", result.OriginalSize) fmt.Printf("Compacted size: %d bytes\n", result.CompactedSize) fmt.Printf("Reduction: %s\n", result.Reduction) fmt.Printf("Duration: %s\n", result.Duration) } } else if compactAll { if result.DryRun { fmt.Printf("DRY RUN - Found %d candidates for Tier %d compaction\n", len(result.Results), compactTier) } else { successCount := 0 for _, r := range result.Results { if r.Success { successCount++ } } fmt.Printf("Compacted %d/%d issues in %s\n", successCount, len(result.Results), result.Duration) for _, r := range result.Results { if r.Success { fmt.Printf(" ✓ %s: %d → %d bytes (%s)\n", r.IssueID, r.OriginalSize, r.CompactedSize, r.Reduction) } else { fmt.Printf(" ✗ %s: %s\n", r.IssueID, r.Error) } } } } } func runCompactStatsRPC() { args := map[string]interface{}{ "tier": compactTier, } resp, err := daemonClient.Execute("compact_stats", args) if err != nil { fmt.Fprintf(os.Stderr, "Error: %v\n", err) os.Exit(1) } if !resp.Success { fmt.Fprintf(os.Stderr, "Error: %s\n", resp.Error) os.Exit(1) } if jsonOutput { fmt.Println(string(resp.Data)) return } var result struct { Success bool `json:"success"` Stats struct { Tier1Candidates int `json:"tier1_candidates"` Tier2Candidates int `json:"tier2_candidates"` TotalClosed int `json:"total_closed"` Tier1MinAge string `json:"tier1_min_age"` Tier2MinAge string `json:"tier2_min_age"` EstimatedSavings string `json:"estimated_savings,omitempty"` } `json:"stats"` } if err := json.Unmarshal(resp.Data, &result); err != nil { fmt.Fprintf(os.Stderr, "Error parsing response: %v\n", err) os.Exit(1) } fmt.Printf("\nCompaction Statistics\n") fmt.Printf("=====================\n\n") fmt.Printf("Total closed issues: %d\n\n", result.Stats.TotalClosed) fmt.Printf("Tier 1 (30+ days closed, not compacted):\n") fmt.Printf(" Candidates: %d\n", result.Stats.Tier1Candidates) fmt.Printf(" Min age: %s\n\n", result.Stats.Tier1MinAge) fmt.Printf("Tier 2 (90+ days closed, Tier 1 compacted):\n") fmt.Printf(" Candidates: %d\n", result.Stats.Tier2Candidates) fmt.Printf(" Min age: %s\n", result.Stats.Tier2MinAge) } func runCompactAnalyze(ctx context.Context, store *sqlite.SQLiteStorage) { type Candidate struct { ID string `json:"id"` Title string `json:"title"` Description string `json:"description"` Design string `json:"design"` Notes string `json:"notes"` AcceptanceCriteria string `json:"acceptance_criteria"` SizeBytes int `json:"size_bytes"` AgeDays int `json:"age_days"` Tier int `json:"tier"` Compacted bool `json:"compacted"` } var candidates []Candidate // Single issue mode if compactID != "" { issue, err := store.GetIssue(ctx, compactID) if err != nil { fmt.Fprintf(os.Stderr, "Error: failed to get issue: %v\n", err) os.Exit(1) } sizeBytes := len(issue.Description) + len(issue.Design) + len(issue.Notes) + len(issue.AcceptanceCriteria) ageDays := 0 if issue.ClosedAt != nil { ageDays = int(time.Since(*issue.ClosedAt).Hours() / 24) } candidates = append(candidates, Candidate{ ID: issue.ID, Title: issue.Title, Description: issue.Description, Design: issue.Design, Notes: issue.Notes, AcceptanceCriteria: issue.AcceptanceCriteria, SizeBytes: sizeBytes, AgeDays: ageDays, Tier: compactTier, Compacted: issue.CompactionLevel > 0, }) } else { // Get tier candidates var tierCandidates []*sqlite.CompactionCandidate var err error if compactTier == 1 { tierCandidates, err = store.GetTier1Candidates(ctx) } else { tierCandidates, err = store.GetTier2Candidates(ctx) } if err != nil { fmt.Fprintf(os.Stderr, "Error: failed to get candidates: %v\n", err) os.Exit(1) } // Apply limit if specified if compactLimit > 0 && len(tierCandidates) > compactLimit { tierCandidates = tierCandidates[:compactLimit] } // Fetch full details for each candidate for _, c := range tierCandidates { issue, err := store.GetIssue(ctx, c.IssueID) if err != nil { continue // Skip issues we can't fetch } ageDays := int(time.Since(c.ClosedAt).Hours() / 24) candidates = append(candidates, Candidate{ ID: issue.ID, Title: issue.Title, Description: issue.Description, Design: issue.Design, Notes: issue.Notes, AcceptanceCriteria: issue.AcceptanceCriteria, SizeBytes: c.OriginalSize, AgeDays: ageDays, Tier: compactTier, Compacted: issue.CompactionLevel > 0, }) } } if jsonOutput { outputJSON(candidates) return } // Human-readable output fmt.Printf("Compaction Candidates (Tier %d)\n\n", compactTier) for _, c := range candidates { compactStatus := "" if c.Compacted { compactStatus = " (already compacted)" } fmt.Printf("ID: %s%s\n", c.ID, compactStatus) fmt.Printf(" Title: %s\n", c.Title) fmt.Printf(" Size: %d bytes\n", c.SizeBytes) fmt.Printf(" Age: %d days\n\n", c.AgeDays) } fmt.Printf("Total: %d candidates\n", len(candidates)) } func runCompactApply(ctx context.Context, store *sqlite.SQLiteStorage) { start := time.Now() // Read summary var summaryBytes []byte var err error if compactSummary == "-" { // Read from stdin summaryBytes, err = io.ReadAll(os.Stdin) if err != nil { fmt.Fprintf(os.Stderr, "Error: failed to read summary from stdin: %v\n", err) os.Exit(1) } } else { // #nosec G304 -- summary file path provided explicitly by operator summaryBytes, err = os.ReadFile(compactSummary) if err != nil { fmt.Fprintf(os.Stderr, "Error: failed to read summary file: %v\n", err) os.Exit(1) } } summary := string(summaryBytes) // Get issue issue, err := store.GetIssue(ctx, compactID) if err != nil { fmt.Fprintf(os.Stderr, "Error: failed to get issue: %v\n", err) os.Exit(1) } // Calculate sizes originalSize := len(issue.Description) + len(issue.Design) + len(issue.Notes) + len(issue.AcceptanceCriteria) compactedSize := len(summary) // Check eligibility unless --force if !compactForce { eligible, reason, err := store.CheckEligibility(ctx, compactID, compactTier) if err != nil { fmt.Fprintf(os.Stderr, "Error: failed to check eligibility: %v\n", err) os.Exit(1) } if !eligible { fmt.Fprintf(os.Stderr, "Error: %s is not eligible for Tier %d compaction: %s\n", compactID, compactTier, reason) fmt.Fprintf(os.Stderr, "Hint: use --force to bypass eligibility checks\n") os.Exit(1) } // Enforce size reduction unless --force if compactedSize >= originalSize { fmt.Fprintf(os.Stderr, "Error: summary (%d bytes) is not shorter than original (%d bytes)\n", compactedSize, originalSize) fmt.Fprintf(os.Stderr, "Hint: use --force to bypass size validation\n") os.Exit(1) } } // Apply compaction actor := compactActor if actor == "" { actor = "agent" } updates := map[string]interface{}{ "description": summary, "design": "", "notes": "", "acceptance_criteria": "", } if err := store.UpdateIssue(ctx, compactID, updates, actor); err != nil { fmt.Fprintf(os.Stderr, "Error: failed to update issue: %v\n", err) os.Exit(1) } commitHash := compact.GetCurrentCommitHash() if err := store.ApplyCompaction(ctx, compactID, compactTier, originalSize, compactedSize, commitHash); err != nil { fmt.Fprintf(os.Stderr, "Error: failed to apply compaction: %v\n", err) os.Exit(1) } savingBytes := originalSize - compactedSize reductionPct := float64(savingBytes) / float64(originalSize) * 100 eventData := fmt.Sprintf("Tier %d compaction: %d → %d bytes (saved %d, %.1f%%)", compactTier, originalSize, compactedSize, savingBytes, reductionPct) if err := store.AddComment(ctx, compactID, actor, eventData); err != nil { fmt.Fprintf(os.Stderr, "Error: failed to record event: %v\n", err) os.Exit(1) } if err := store.MarkIssueDirty(ctx, compactID); err != nil { fmt.Fprintf(os.Stderr, "Error: failed to mark dirty: %v\n", err) os.Exit(1) } elapsed := time.Since(start) if jsonOutput { output := map[string]interface{}{ "success": true, "issue_id": compactID, "tier": compactTier, "original_size": originalSize, "compacted_size": compactedSize, "saved_bytes": savingBytes, "reduction_pct": reductionPct, "elapsed_ms": elapsed.Milliseconds(), } outputJSON(output) return } fmt.Printf("✓ Compacted %s (Tier %d)\n", compactID, compactTier) fmt.Printf(" %d → %d bytes (saved %d, %.1f%%)\n", originalSize, compactedSize, savingBytes, reductionPct) fmt.Printf(" Time: %v\n", elapsed) // Schedule auto-flush to export changes markDirtyAndScheduleFlush() } func init() { compactCmd.Flags().BoolVar(&compactDryRun, "dry-run", false, "Preview without compacting") compactCmd.Flags().IntVar(&compactTier, "tier", 1, "Compaction tier (1 or 2)") compactCmd.Flags().BoolVar(&compactAll, "all", false, "Process all candidates") compactCmd.Flags().StringVar(&compactID, "id", "", "Compact specific issue") compactCmd.Flags().BoolVar(&compactForce, "force", false, "Force compact (bypass checks, requires --id)") compactCmd.Flags().IntVar(&compactBatch, "batch-size", 10, "Issues per batch") compactCmd.Flags().IntVar(&compactWorkers, "workers", 5, "Parallel workers") compactCmd.Flags().BoolVar(&compactStats, "stats", false, "Show compaction statistics") compactCmd.Flags().BoolVar(&jsonOutput, "json", false, "Output JSON format") // New mode flags compactCmd.Flags().BoolVar(&compactAnalyze, "analyze", false, "Analyze mode: export candidates for agent review") compactCmd.Flags().BoolVar(&compactApply, "apply", false, "Apply mode: accept agent-provided summary") compactCmd.Flags().BoolVar(&compactAuto, "auto", false, "Auto mode: AI-powered compaction (legacy)") compactCmd.Flags().StringVar(&compactSummary, "summary", "", "Path to summary file (use '-' for stdin)") compactCmd.Flags().StringVar(&compactActor, "actor", "agent", "Actor name for audit trail") compactCmd.Flags().IntVar(&compactLimit, "limit", 0, "Limit number of candidates (0 = no limit)") rootCmd.AddCommand(compactCmd) }