diff --git a/.beads/issues.jsonl b/.beads/issues.jsonl index 178114c..4c60727 100644 --- a/.beads/issues.jsonl +++ b/.beads/issues.jsonl @@ -1,5 +1,6 @@ {"id":"bd-18h","title":"src/layout/justify.rs: Spread/space-between gap distribution","description":"Distribute extra space evenly across gaps. Center separator core within each gap. Remainder chars distributed left-to-right.","status":"closed","priority":2,"issue_type":"task","created_at":"2026-02-06T18:11:04.287537Z","created_by":"tayloreernisse","updated_at":"2026-02-06T19:14:32.967113Z","closed_at":"2026-02-06T19:14:32.967066Z","close_reason":"Justify: space-between gap distribution with centered separator core","compaction_level":0,"original_size":0,"labels":["layout","phase-2"],"dependencies":[{"issue_id":"bd-18h","depends_on_id":"bd-7nx","type":"blocks","created_at":"2026-02-06T18:11:35.764576Z","created_by":"tayloreernisse"}]} {"id":"bd-1ac","title":"src/lib.rs: Module declarations and public API","description":"Declare all pub mod: error, config, input, theme, color, glyph, width, cache, trend, format, shell, metrics, section, layout. Re-export key types.","status":"closed","priority":0,"issue_type":"task","created_at":"2026-02-06T18:10:48.931855Z","created_by":"tayloreernisse","updated_at":"2026-02-06T19:08:29.000390Z","closed_at":"2026-02-06T19:08:29.000345Z","close_reason":"Fully implemented during scaffold: all pub mod declarations, Error re-export","compaction_level":0,"original_size":0,"labels":["core","phase-1"],"dependencies":[{"issue_id":"bd-1ac","depends_on_id":"bd-1m2","type":"blocks","created_at":"2026-02-06T18:11:30.510508Z","created_by":"tayloreernisse"}]} +{"id":"bd-1if","title":"Implement cache TTL jitter (cache_ttl_jitter_pct)","description":"## Background\nPRD specifies cache_ttl_jitter_pct (default 10): add random +/- jitter to TTL to desynchronize cache expiration across concurrent renders. Without jitter, all cached values expire simultaneously causing a thundering herd of shell-outs on the same render cycle.\n\n## Approach\n\n**Step 1: Add jitter_pct to Cache struct**\n```rust\npub struct Cache {\n dir: Option,\n jitter_pct: u8, // from config.global.cache_ttl_jitter_pct\n}\n```\n\n**Step 2: Deterministic per-key jitter using key hash**\nUse a simple hash of the cache key to produce deterministic jitter per key. This ensures the same key always gets the same jitter (no oscillation between cache hit/miss on successive renders).\n\n```rust\nfn jittered_ttl(&self, key: &str, base_ttl: Duration) -> Duration {\n if self.jitter_pct == 0 { return base_ttl; }\n\n // Simple FNV-1a hash of key for deterministic per-key jitter\n let mut hash: u64 = 0xcbf29ce484222325;\n for byte in key.bytes() {\n hash ^= byte as u64;\n hash = hash.wrapping_mul(0x100000001b3);\n }\n\n // Map hash to range [-jitter_pct, +jitter_pct]\n let jitter_range = self.jitter_pct as f64 / 100.0;\n let normalized = (hash % 2001) as f64 / 1000.0 - 1.0; // [-1.0, 1.0]\n let multiplier = 1.0 + (normalized * jitter_range);\n\n let jittered_ms = (base_ttl.as_millis() as f64 * multiplier) as u64;\n // Clamp: minimum 100ms to avoid zero TTL\n Duration::from_millis(jittered_ms.max(100))\n}\n```\n\n**Step 3: Apply in Cache::get()**\nReplace `if age < ttl` with `if age < self.jittered_ttl(key, ttl)`.\n\n**Step 4: Pass jitter_pct from config during Cache construction**\nIn src/bin/claude-statusline.rs: `Cache::new(&template, &session, config.global.cache_ttl_jitter_pct)`\n\n## Acceptance Criteria\n- [ ] Cache TTL varies by +/- jitter_pct for each key\n- [ ] Jitter is deterministic per key (same key = same jitter every time)\n- [ ] jitter_pct = 0 disables jitter entirely (exact TTL)\n- [ ] Minimum jittered TTL is 100ms (never zero)\n- [ ] jitter_pct = 100 at most doubles or halves the TTL\n- [ ] Different keys get different jitter values\n- [ ] No external crate needed (FNV-1a is 5 lines)\n- [ ] cargo test; cargo clippy --all-targets -- -D warnings\n\n## TDD Loop\nRED: test_jitter_zero_returns_exact_ttl, test_jitter_deterministic_per_key, test_jitter_different_keys_differ, test_jitter_clamps_minimum, test_jitter_100_pct_range\nGREEN: Implement jittered_ttl + wire into get()\nVERIFY: cargo test -- jitter\n\n## Files\n- src/cache.rs (add jitter_pct field, jittered_ttl method, apply in get())\n- src/bin/claude-statusline.rs (pass jitter_pct from config to Cache::new)\n\n## Edge Cases\n- jitter_pct = 0: TTL unchanged (fast path, no hash computation)\n- jitter_pct = 100: TTL ranges from 0.5x to 2.0x base (clamped to 100ms min)\n- Very short TTL (1s) with 10% jitter: ranges from 900ms to 1100ms\n- Key with only ASCII chars vs unicode: FNV-1a works on bytes, handles both","status":"open","priority":3,"issue_type":"task","created_at":"2026-02-06T20:20:01.522601Z","created_by":"tayloreernisse","updated_at":"2026-02-06T20:24:26.763244Z","compaction_level":0,"original_size":0} {"id":"bd-1m2","title":"Project scaffold: Cargo.toml, build.rs, src/ structure","description":"Create Cargo.toml with all deps per PRD. Create build.rs for defaults.json/schema.json rerun-if-changed. Create src/ directory structure with empty module files. Verify cargo check passes.","status":"closed","priority":0,"issue_type":"task","created_at":"2026-02-06T18:10:14.965071Z","created_by":"tayloreernisse","updated_at":"2026-02-06T19:07:44.162081Z","closed_at":"2026-02-06T19:07:44.162012Z","close_reason":"Scaffold complete: Cargo.toml, build.rs, all modules, CLI, stubs compile clean","compaction_level":0,"original_size":0,"labels":["phase-1","scaffold"]} {"id":"bd-1pp","title":"src/format.rs: display_width, human_tokens, human_duration, truncation, apply_formatting","description":"display_width via unicode-width. human_tokens (k/M). human_duration (s/m/h). Grapheme-safe truncation (right/middle/left). apply_formatting: prefix/suffix, color override, pad+align.","status":"closed","priority":1,"issue_type":"task","created_at":"2026-02-06T18:10:32.654555Z","created_by":"tayloreernisse","updated_at":"2026-02-06T19:08:28.906312Z","closed_at":"2026-02-06T19:08:28.906260Z","close_reason":"Fully implemented during scaffold: display_width, human_tokens, human_duration, truncate, apply_formatting","compaction_level":0,"original_size":0,"labels":["core","phase-1"],"dependencies":[{"issue_id":"bd-1pp","depends_on_id":"bd-2ck","type":"blocks","created_at":"2026-02-06T18:11:33.078938Z","created_by":"tayloreernisse"}]} {"id":"bd-1rm","title":"src/layout/priority.rs: Priority drop algorithm","description":"Drop tier 3 all at once, then tier 2 if still overflowing. Priority 1 never drops. line_width calculation with separator suppression at spacers.","status":"closed","priority":1,"issue_type":"task","created_at":"2026-02-06T18:10:59.275122Z","created_by":"tayloreernisse","updated_at":"2026-02-06T19:14:32.924870Z","closed_at":"2026-02-06T19:14:32.924822Z","close_reason":"Priority drop: tier 3 all-at-once, then tier 2, P1 never drops","compaction_level":0,"original_size":0,"labels":["layout","phase-2"],"dependencies":[{"issue_id":"bd-1rm","depends_on_id":"bd-7nx","type":"blocks","created_at":"2026-02-06T18:11:35.728197Z","created_by":"tayloreernisse"}]} @@ -9,15 +10,27 @@ {"id":"bd-2ck","title":"src/color.rs: ANSI constants, resolve_color, should_use_color","description":"10 ANSI const strings. resolve_color with palette refs (p:key), compound styles (red bold), single names. should_use_color: NO_COLOR, cli flag, config, atty check.","status":"closed","priority":1,"issue_type":"task","created_at":"2026-02-06T18:10:25.055555Z","created_by":"tayloreernisse","updated_at":"2026-02-06T19:08:28.861365Z","closed_at":"2026-02-06T19:08:28.861316Z","close_reason":"Fully implemented during scaffold: ANSI consts, resolve_color, should_use_color, atty","compaction_level":0,"original_size":0,"labels":["core","phase-1"],"dependencies":[{"issue_id":"bd-2ck","depends_on_id":"bd-1m2","type":"blocks","created_at":"2026-02-06T18:11:30.471417Z","created_by":"tayloreernisse"}]} {"id":"bd-2or","title":"src/glyph.rs: Nerd Font + ASCII fallback","description":"glyph() function: lookup named glyph, return Nerd Font icon when enabled, ASCII fallback otherwise.","status":"closed","priority":2,"issue_type":"task","created_at":"2026-02-06T18:10:34.834222Z","created_by":"tayloreernisse","updated_at":"2026-02-06T19:08:28.928218Z","closed_at":"2026-02-06T19:08:28.928171Z","close_reason":"Fully implemented during scaffold: glyph function with Nerd Font lookup + ASCII fallback","compaction_level":0,"original_size":0,"labels":["core","phase-1"]} {"id":"bd-2ov","title":"src/error.rs: Error enum with From impls","description":"Error enum: Io, Json, ConfigNotFound, EmptyStdin. Display impl, std::error::Error impl, From and From.","status":"closed","priority":1,"issue_type":"task","created_at":"2026-02-06T18:10:16.662422Z","created_by":"tayloreernisse","updated_at":"2026-02-06T19:08:28.790207Z","closed_at":"2026-02-06T19:08:28.790Z","close_reason":"Fully implemented during scaffold: Error enum, Display, From impls","compaction_level":0,"original_size":0,"labels":["core","phase-1"],"dependencies":[{"issue_id":"bd-2ov","depends_on_id":"bd-1m2","type":"blocks","created_at":"2026-02-06T18:11:30.411272Z","created_by":"tayloreernisse"}]} +{"id":"bd-2qr","title":"Implement breakpoint hysteresis for responsive layout","description":"## Background\nPRD specifies breakpoint_hysteresis (default 2): avoid toggling responsive presets if width fluctuates within +-hysteresis columns of a breakpoint. Without this, a terminal at exactly 60 cols might flip between dense and standard on alternate renders due to measurement noise.\n\n## Approach\n1. Store last responsive preset choice in a static Mutex (similar to width memoization)\n2. In src/layout/mod.rs responsive_preset(), if the current width is within hysteresis of a breakpoint AND the previous preset is still valid, keep the previous preset\n3. Only change preset when width moves beyond breakpoint + hysteresis\n\nExample: breakpoints narrow=60, medium=100, hysteresis=2\n- At 61 cols, switches to standard (>60)\n- At 59 cols: within 60-2=58..60, if was standard, STAY standard\n- At 57 cols: below 58, switch to dense\n\n## Acceptance Criteria\n- [ ] Width at breakpoint +/- hysteresis maintains previous preset\n- [ ] Width clearly beyond breakpoint changes preset\n- [ ] hysteresis = 0 disables (every width change triggers preset change)\n- [ ] First render (no previous state) uses simple threshold\n- [ ] cargo test with edge case tests; cargo clippy --all-targets -- -D warnings\n\n## Files\n- src/layout/mod.rs (add hysteresis state and logic to responsive_preset)\n\n## Edge Cases\n- First render: no previous state, use simple thresholds\n- hysteresis = 0: equivalent to current behavior\n- Width oscillates exactly at breakpoint: should stabilize on one preset","status":"open","priority":3,"issue_type":"task","created_at":"2026-02-06T20:19:53.754463Z","created_by":"tayloreernisse","updated_at":"2026-02-06T20:19:53.754463Z","compaction_level":0,"original_size":0} {"id":"bd-2u8","title":"17 pure sections (no shell-out)","description":"Implement all pure sections: model, provider, project, context_bar, context_usage, context_remaining, tokens_raw, cache_efficiency, cost, cost_velocity, token_velocity, lines_changed, duration, tools, turns, version, output_style. Each returns Option.","status":"closed","priority":1,"issue_type":"task","created_at":"2026-02-06T18:10:40.742273Z","created_by":"tayloreernisse","updated_at":"2026-02-06T19:12:26.989673Z","closed_at":"2026-02-06T19:12:26.989485Z","close_reason":"All 17 pure sections implemented: model, provider, project, context_bar, context_usage, context_remaining, tokens_raw, cache_efficiency, cost, cost_velocity, token_velocity, lines_changed, duration, tools, turns, version, output_style","compaction_level":0,"original_size":0,"labels":["phase-1","sections"],"dependencies":[{"issue_id":"bd-2u8","depends_on_id":"bd-1pp","type":"blocks","created_at":"2026-02-06T18:11:33.180547Z","created_by":"tayloreernisse"},{"issue_id":"bd-2u8","depends_on_id":"bd-1ze","type":"blocks","created_at":"2026-02-06T18:11:33.162497Z","created_by":"tayloreernisse"},{"issue_id":"bd-2u8","depends_on_id":"bd-4z1","type":"blocks","created_at":"2026-02-06T18:11:33.200843Z","created_by":"tayloreernisse"}]} +{"id":"bd-2vm","title":"Implement shell circuit breaker (failure tracking + cooldown)","description":"## Background\nPRD specifies a circuit breaker pattern for shell-outs: track consecutive failures per command key in-memory. If failures exceed shell_failure_threshold (default 3), open a cooldown window (shell_cooldown_ms, default 30s) and serve stale cache without executing the command. Resets on success or cooldown expiry. This prevents hammering failing commands (e.g., git lock contention).\n\n## Approach\nAdd a static CircuitBreaker struct in src/shell.rs:\n```rust\nuse std::sync::Mutex;\nuse std::collections::HashMap;\nuse std::time::Instant;\n\nstruct CircuitState {\n failures: u8,\n cooldown_until: Option,\n}\n\nstatic BREAKER: Mutex> = Mutex::new(HashMap::new());\n```\n\nWrap exec_with_timeout in a new function exec_with_circuit_breaker(program, args, dir, timeout, threshold, cooldown_ms) that:\n1. Checks if command key is in cooldown -> return None\n2. Calls exec_with_timeout\n3. On success: reset failure count\n4. On failure: increment count, if >= threshold set cooldown_until\n\nConfig values shell_failure_threshold and shell_cooldown_ms should be passed from RenderContext or a ShellConfig struct.\n\n## Acceptance Criteria\n- [ ] After 3 consecutive failures, command is not executed for 30s (configurable)\n- [ ] Success resets failure count to 0\n- [ ] Cooldown expiry allows retry\n- [ ] Failure tracking is per-command-key (git != br != sysctl)\n- [ ] State is in-memory only (resets on process restart as PRD specifies)\n- [ ] cargo test with unit tests for circuit breaker logic\n- [ ] cargo clippy --all-targets -- -D warnings\n\n## Files\n- src/shell.rs (add CircuitBreaker, wrap exec_with_timeout)\n- src/section/mod.rs (pass shell config to context or make available)\n- src/bin/claude-statusline.rs (pass config values)\n\n## Edge Cases\n- Circuit opens mid-render for one command but not others\n- Concurrent renders: Mutex is fine since renders are sequential per process\n- shell_failure_threshold = 0 means disabled (never trip)","status":"open","priority":2,"issue_type":"task","created_at":"2026-02-06T20:19:02.633385Z","created_by":"tayloreernisse","updated_at":"2026-02-06T20:20:43.664128Z","compaction_level":0,"original_size":0,"dependencies":[{"issue_id":"bd-2vm","depends_on_id":"bd-ywx","type":"blocks","created_at":"2026-02-06T20:20:43.664112Z","created_by":"tayloreernisse"}]} {"id":"bd-2z0","title":"Shell-out sections: vcs, beads, load, hostname, time, custom","description":"Port remaining 7 shell-out sections. Combined git status, jj support, beads br status, sysctl/proc load, hostname, date, custom command exec. All use cache + stale fallback.","status":"closed","priority":1,"issue_type":"task","created_at":"2026-02-06T18:11:17.544497Z","created_by":"tayloreernisse","updated_at":"2026-02-06T19:21:20.470989Z","closed_at":"2026-02-06T19:21:20.470915Z","close_reason":"All 8 shell-out sections implemented: vcs, beads, load, hostname, time, custom, cost_trend, context_trend. Compiles clean, clippy clean, --test output verified.","compaction_level":0,"original_size":0,"labels":["phase-3","sections"],"dependencies":[{"issue_id":"bd-2z0","depends_on_id":"bd-3hb","type":"blocks","created_at":"2026-02-06T18:11:35.826685Z","created_by":"tayloreernisse"},{"issue_id":"bd-2z0","depends_on_id":"bd-7nx","type":"blocks","created_at":"2026-02-06T18:11:35.785781Z","created_by":"tayloreernisse"},{"issue_id":"bd-2z0","depends_on_id":"bd-s6n","type":"blocks","created_at":"2026-02-06T18:11:35.805900Z","created_by":"tayloreernisse"}]} +{"id":"bd-30o","title":"Implement gradual drop strategy in priority.rs","description":"## Background\nPRD specifies two drop strategies: tiered (implemented) and gradual (not implemented). Gradual drops sections one-by-one by (priority desc, width cost desc, rightmost first), recomputing after each removal. Priority 1 sections never drop. Config field drop_strategy selects which.\n\n## Approach\nIn src/layout/priority.rs, add a gradual_drop function alongside existing priority_drop:\n1. Sort candidates by (priority DESC, display_width DESC, position DESC)\n2. Remove one at a time, recompute line_width after each\n3. Stop when line fits or only priority 1 sections remain\n4. In the caller (src/layout/mod.rs render_line), check config.global.drop_strategy and dispatch to tiered or gradual.\n\n## Acceptance Criteria\n- [ ] drop_strategy = \"gradual\" drops sections one at a time\n- [ ] Drop order: highest priority number first, then widest, then rightmost\n- [ ] Priority 1 sections never drop in either strategy\n- [ ] Recalculate width after each individual drop\n- [ ] drop_strategy = \"tiered\" behavior unchanged\n- [ ] cargo test with tests for both strategies\n- [ ] cargo clippy --all-targets -- -D warnings\n\n## Files\n- src/layout/priority.rs (add gradual_drop function)\n- src/layout/mod.rs (dispatch based on config.global.drop_strategy)\n\n## Edge Cases\n- All sections are priority 1: nothing drops even if overflowing\n- Single section: no drops needed\n- Two sections with same priority and width: rightmost drops first","status":"open","priority":3,"issue_type":"task","created_at":"2026-02-06T20:19:34.219695Z","created_by":"tayloreernisse","updated_at":"2026-02-06T20:19:34.219695Z","compaction_level":0,"original_size":0} +{"id":"bd-3ax","title":"Implement render budget timer with graceful degradation","description":"## Background\nPRD specifies render_budget_ms (default 8ms): if exceeded during rendering, drop remaining lowest-priority sections and emit partial output. Prevents UI stalls from cascading shell-out delays. Critical requirement: partial output must still be valid ANSI (no unclosed escape sequences).\n\n## Approach\n\n**Step 1: Add budget tracking to RenderContext**\n```rust\npub struct RenderContext<'a> {\n // ...existing fields...\n pub budget_start: Option, // None = unlimited\n pub budget_ms: u64,\n}\n\nimpl RenderContext<'_> {\n pub fn budget_remaining(&self) -> Option {\n let start = self.budget_start?;\n let budget = Duration::from_millis(self.budget_ms);\n budget.checked_sub(start.elapsed())\n }\n pub fn budget_exceeded(&self) -> bool {\n self.budget_remaining().map_or(false, |r| r.is_zero())\n }\n}\n```\n\n**Step 2: Check budget in render_line()**\nIn src/layout/mod.rs render_line(), before each section render:\n```rust\nfor id in section_ids {\n if ctx.budget_exceeded() {\n let (priority, _) = section_meta(id, ctx.config);\n if priority > 1 {\n continue; // Skip non-essential sections when over budget\n }\n // Priority 1 sections always render regardless of budget\n }\n let output = section::render_section(id, ctx);\n // ...\n}\n```\n\n**Step 3: ANSI validity guarantee**\nEach SectionOutput.ansi already ends with RESET (convention enforced by all section renderers). For safety, in assemble_left(), append a final RESET at the end of each line:\n```rust\nfn assemble_left(active: &[ActiveSection], ...) -> String {\n let mut output = String::new();\n // ...existing assembly...\n output.push_str(color::RESET); // Ensure no unclosed sequences\n output\n}\n```\nThis is idempotent (double RESET is harmless) and guarantees ANSI validity.\n\n**Step 4: Skip flex expansion when over budget**\nIn render_line(), if budget_exceeded before flex phase, skip flex expansion entirely - emit what we have without flex padding.\n\n**Step 5: Wire budget from config**\nbudget_ms = 0 means unlimited (budget_start = None).\n\n## Acceptance Criteria\n- [ ] Sections are skipped when render budget is exceeded\n- [ ] Priority 1 sections ALWAYS render regardless of budget\n- [ ] Non-priority-1 sections are skipped in layout order (leftmost remaining skip first)\n- [ ] Partial output always has valid ANSI (RESET appended to every line)\n- [ ] render_budget_ms = 0 disables budget (no timing overhead)\n- [ ] Budget timer starts after config load, not including stdin read\n- [ ] Flex expansion skipped when over budget\n- [ ] cargo test; cargo clippy --all-targets -- -D warnings\n\n## TDD Loop\nRED: test_budget_zero_renders_all, test_budget_skips_low_priority, test_budget_always_renders_priority1, test_partial_output_ends_with_reset\nGREEN: Add budget_start/budget_ms to RenderContext, check in render_line\nVERIFY: cargo test -- budget\n\n## Files\n- src/section/mod.rs (add budget_start, budget_ms, helper methods to RenderContext)\n- src/layout/mod.rs (check budget before each section render, skip flex when over budget)\n- src/layout/mod.rs assemble_left (append RESET for ANSI safety)\n- src/bin/claude-statusline.rs (set budget_start = Instant::now() after config load, budget_ms from config)\n\n## Edge Cases\n- All sections are priority 1: budget has no effect (all rendered)\n- Budget exceeded during flex expansion: emit without flex\n- Budget = 0: unlimited, budget_start set to None (no Instant::now call)\n- Budget exceeded before first section: still render priority 1 sections\n- Single-line layout: budget applies per line, not per render","status":"open","priority":3,"issue_type":"task","created_at":"2026-02-06T20:19:43.552342Z","created_by":"tayloreernisse","updated_at":"2026-02-06T20:24:46.124093Z","compaction_level":0,"original_size":0,"dependencies":[{"issue_id":"bd-3ax","depends_on_id":"bd-62g","type":"blocks","created_at":"2026-02-06T20:20:43.735777Z","created_by":"tayloreernisse"}]} {"id":"bd-3hb","title":"src/shell.rs: exec_with_timeout, GIT_ENV, parse_git_status_v2","description":"Polling try_wait with 5ms sleep. GIT_ENV constants. Combined git status --porcelain=v2 --branch parsing. Circuit breaker tracking.","status":"closed","priority":1,"issue_type":"task","created_at":"2026-02-06T18:11:12.323223Z","created_by":"tayloreernisse","updated_at":"2026-02-06T19:08:29.022076Z","closed_at":"2026-02-06T19:08:29.022033Z","close_reason":"Fully implemented during scaffold: exec_with_timeout, parse_git_status_v2","compaction_level":0,"original_size":0,"labels":["core","phase-3"]} {"id":"bd-3hc","title":"src/bin/claude-statusline.rs: CLI entry point with --test and --help","description":"Manual CLI flag parsing (no clap). Stdin read, config load, theme detect, render, stdout. Support --help, --test, --config-schema, --print-defaults, --list-sections, --validate-config, --dump-state, --config, --width, --color, --no-cache, --no-shell, --clear-cache.","status":"closed","priority":1,"issue_type":"task","created_at":"2026-02-06T18:10:45.407479Z","created_by":"tayloreernisse","updated_at":"2026-02-06T19:12:33.169152Z","closed_at":"2026-02-06T19:12:33.169044Z","close_reason":"CLI fully implemented during scaffold, all dependencies now closed","compaction_level":0,"original_size":0,"labels":["cli","phase-1"],"dependencies":[{"issue_id":"bd-3hc","depends_on_id":"bd-1ac","type":"blocks","created_at":"2026-02-06T18:11:33.241844Z","created_by":"tayloreernisse"},{"issue_id":"bd-3hc","depends_on_id":"bd-2u8","type":"blocks","created_at":"2026-02-06T18:11:33.260886Z","created_by":"tayloreernisse"}]} +{"id":"bd-3l2","title":"Implement cache GC (garbage collection of old cache dirs)","description":"## Background\nPRD specifies cache garbage collection: at most once per cache_gc_interval_hours (default 24), scan /tmp/claude-sl-* and delete dirs older than cache_gc_days (default 7), owned by current user, not symlinks. GC runs asynchronously AFTER render output is written, never blocking the status line. Uses a lock file in /tmp to avoid concurrent GC.\n\n## Approach\n1. Add a gc() function in src/cache.rs\n2. Check gc eligibility: read mtime of /tmp/claude-sl-gc.lock. If younger than gc_interval_hours, skip.\n3. Try flock on gc.lock (non-blocking). If contention, skip.\n4. Scan /tmp/claude-sl-* directories. For each: verify owned by current user, not symlink, mtime older than gc_days -> remove_dir_all.\n5. Call gc() AFTER print!() in main, so output is not delayed.\n6. Consider spawning a detached thread for GC so the process exits immediately.\n\n## Acceptance Criteria\n- [ ] Old cache dirs (>7 days) are cleaned up\n- [ ] GC runs at most once per 24 hours (configurable)\n- [ ] GC only deletes dirs owned by current user\n- [ ] GC does not follow symlinks\n- [ ] GC does not block status line output\n- [ ] GC uses lock file to prevent concurrent runs\n- [ ] cargo test; cargo clippy --all-targets -- -D warnings\n\n## Files\n- src/cache.rs (add gc function)\n- src/bin/claude-statusline.rs (call gc after render output)\n\n## Edge Cases\n- /tmp/claude-sl-* dirs from other users: skip (ownership check)\n- Symlinked cache dirs: skip (symlink check)\n- No old dirs to clean: exit GC early\n- Lock contention: skip GC silently","status":"open","priority":2,"issue_type":"task","created_at":"2026-02-06T20:19:14.041202Z","created_by":"tayloreernisse","updated_at":"2026-02-06T20:19:14.041202Z","compaction_level":0,"original_size":0} +{"id":"bd-3oy","title":"Fix cache namespace to include cache_version and config_hash","description":"## Background\nPRD specifies cache directory template: /tmp/claude-sl-{session_id}-{cache_version}-{config_hash}/\nCurrent implementation only uses {session_id}. This means config changes dont invalidate cache, and binary upgrades with new cache_version dont clear stale data.\n\n## Approach\n1. Compute config_hash: MD5 of the merged config JSON (post-merge, pre-deserialize), truncated to 8 hex chars.\n2. Update cache dir template in defaults.json from /tmp/claude-sl-{session_id} to /tmp/claude-sl-{session_id}-{cache_version}-{config_hash}\n3. In Cache::new(), also replace {cache_version} and {config_hash} placeholders.\n4. Pass config_hash and cache_version when constructing Cache.\n\n## Acceptance Criteria\n- [ ] Cache dir includes {session_id}-{cache_version}-{config_hash} in path\n- [ ] Changing any config value produces a different cache dir\n- [ ] Bumping cache_version in config produces a different cache dir\n- [ ] Old cache dirs are not cleaned up (thats Cache GCs job)\n- [ ] cargo test; cargo clippy --all-targets -- -D warnings\n\n## Files\n- defaults.json (update cache_dir template)\n- src/cache.rs (add config_hash and cache_version replacement in new())\n- src/bin/claude-statusline.rs (compute config_hash from merged JSON, pass to Cache::new)\n\n## Edge Cases\n- User overrides cache_dir in config without {cache_version}/{config_hash}: works fine, placeholders just arent present\n- Empty config produces consistent hash","status":"open","priority":1,"issue_type":"bug","created_at":"2026-02-06T20:18:46.944106Z","created_by":"tayloreernisse","updated_at":"2026-02-06T20:18:46.944106Z","compaction_level":0,"original_size":0} +{"id":"bd-3q1","title":"Implement CLAUDE_STATUSLINE_NO_CACHE, _NO_SHELL, _COLOR env vars","description":"## Background\nPRD Contract section specifies three env vars: CLAUDE_STATUSLINE_NO_CACHE, CLAUDE_STATUSLINE_NO_SHELL, CLAUDE_STATUSLINE_COLOR. Only NO_COLOR (the standard) is currently checked. The CLAUDE_STATUSLINE_COLOR var should override config color mode (auto|always|never), similar to --color= CLI flag but at env var precedence.\n\n## Approach\nIn src/bin/claude-statusline.rs, check env vars early:\n1. CLAUDE_STATUSLINE_NO_CACHE: set no_cache = true (same effect as --no-cache flag)\n2. CLAUDE_STATUSLINE_NO_SHELL: set no_shell = true (same effect as --no-shell flag)\n3. CLAUDE_STATUSLINE_COLOR: in src/color.rs should_use_color(), check this env var with same precedence as --color= CLI flag. If both set, CLI wins.\n\n## Acceptance Criteria\n- [ ] CLAUDE_STATUSLINE_NO_CACHE=1 disables caching (same as --no-cache)\n- [ ] CLAUDE_STATUSLINE_NO_SHELL=1 disables shell-outs (same as --no-shell)\n- [ ] CLAUDE_STATUSLINE_COLOR=always forces color on, =never forces off, =auto uses detection\n- [ ] CLI flags take precedence over env vars\n- [ ] cargo test; cargo clippy --all-targets -- -D warnings\n\n## Files\n- src/bin/claude-statusline.rs (check env vars for no_cache, no_shell)\n- src/color.rs (add CLAUDE_STATUSLINE_COLOR to should_use_color precedence chain)\n\n## Edge Cases\n- CLAUDE_STATUSLINE_COLOR=invalid should fall through to auto\n- Both --color=always and CLAUDE_STATUSLINE_COLOR=never set: CLI wins","status":"open","priority":1,"issue_type":"task","created_at":"2026-02-06T20:18:38.622692Z","created_by":"tayloreernisse","updated_at":"2026-02-06T20:20:43.639516Z","compaction_level":0,"original_size":0,"dependencies":[{"issue_id":"bd-3q1","depends_on_id":"bd-khk","type":"blocks","created_at":"2026-02-06T20:20:43.639484Z","created_by":"tayloreernisse"}]} {"id":"bd-3s1","title":"src/layout/flex.rs: Flex expansion with context_bar rebuild","description":"Spacer wins over non-spacer flex. Only one flex winner per line. Spacer=spaces, context_bar=rebuild at wider width, other=trailing pad. Re-apply apply_formatting after context_bar rebuild.","status":"closed","priority":1,"issue_type":"task","created_at":"2026-02-06T18:11:03.583651Z","created_by":"tayloreernisse","updated_at":"2026-02-06T19:14:32.944474Z","closed_at":"2026-02-06T19:14:32.944429Z","close_reason":"Flex expansion: spacer wins, context_bar rebuilds with wider bar, others pad","compaction_level":0,"original_size":0,"labels":["layout","phase-2"],"dependencies":[{"issue_id":"bd-3s1","depends_on_id":"bd-7nx","type":"blocks","created_at":"2026-02-06T18:11:35.747152Z","created_by":"tayloreernisse"}]} +{"id":"bd-3uw","title":"Implement parallel shell-outs via std::thread::scope","description":"## Background\nPRD specifies that when multiple shell-out sections have expired caches in the same render, they should execute in parallel using std::thread::scope. Cache hits are >90% of renders, so this only matters for occasional cold renders. Currently all shell-outs run sequentially within each section render call.\n\n## Approach\nRestructure the render pipeline to pre-fetch shell results before section rendering.\n\n**Step 1: Define ShellResults type**\nIn src/section/mod.rs, add to RenderContext:\n```rust\npub shell_results: &'a HashMap>,\n```\n\n**Step 2: Pre-fetch function**\nIn src/lib.rs or a new src/prefetch.rs, create:\n```rust\npub fn prefetch_shell_outs(\n cache: &Cache,\n config: &Config,\n project_dir: &str,\n no_shell: bool,\n) -> HashMap> {\n let mut results = HashMap::new();\n if no_shell { return results; }\n\n // Collect which keys have expired caches\n let vcs_expired = cache.get(\"vcs_branch\", branch_ttl).is_none();\n let load_expired = cache.get(\"load\", load_ttl).is_none();\n let beads_expired = cache.get(\"beads\", beads_ttl).is_none();\n\n // Only spawn threads for expired keys\n std::thread::scope(|s| {\n let vcs_h = vcs_expired.then(|| s.spawn(|| {\n shell::exec_with_timeout(\"git\", &[\"-C\", dir, \"status\", \"--porcelain=v2\", \"--branch\"], None, timeout)\n }));\n let load_h = load_expired.then(|| s.spawn(|| {\n shell::exec_with_timeout(\"sysctl\", &[\"-n\", \"vm.loadavg\"], None, timeout)\n }));\n // ... beads, hostname, custom ...\n\n if let Some(h) = vcs_h { results.insert(\"vcs\".into(), h.join().ok().flatten()); }\n if let Some(h) = load_h { results.insert(\"load\".into(), h.join().ok().flatten()); }\n });\n results\n}\n```\n\n**Step 3: Wire into sections**\nEach shell-out section checks shell_results first:\n```rust\n// In vcs.rs render_git():\nlet output = ctx.shell_results.get(\"vcs\")\n .cloned()\n .flatten()\n .or_else(|| shell::exec_with_timeout(...));\n```\n\n## Acceptance Criteria\n- [ ] prefetch_shell_outs() runs before layout/render phase\n- [ ] Only cache-miss shell-outs are spawned as threads\n- [ ] Cache hits bypass thread::scope entirely (0 threads spawned)\n- [ ] shell_results is passed through RenderContext to all sections\n- [ ] Each shell-out section checks shell_results before calling exec_with_timeout\n- [ ] Thread panics are caught by join() and yield None\n- [ ] Cold render with 3 expired shell-outs is measurably faster (wall time)\n- [ ] cargo test; cargo clippy --all-targets -- -D warnings\n\n## TDD Loop\nRED: src/tests/prefetch.rs - test_prefetch_all_cached_skips_threads, test_prefetch_expired_keys_parallel\nGREEN: Implement prefetch_shell_outs with thread::scope\nVERIFY: cargo test -- prefetch\n\n## Files\n- src/section/mod.rs (add shell_results to RenderContext)\n- src/lib.rs or src/prefetch.rs (prefetch_shell_outs function)\n- src/bin/claude-statusline.rs (call prefetch before render, pass results to ctx)\n- src/section/vcs.rs, beads.rs, load.rs, custom.rs (check shell_results first)\n\n## Edge Cases\n- Only 1 shell-out expired: spawns 1 thread, no parallelism benefit but correct\n- Thread panic in scope: caught by join(), returns None, other threads unaffected\n- All cached: HashMap is empty, no threads spawned, zero overhead\n- Custom sections: may have N commands; each gets its own key in shell_results","status":"open","priority":2,"issue_type":"task","created_at":"2026-02-06T20:20:16.961440Z","created_by":"tayloreernisse","updated_at":"2026-02-06T20:23:09.400495Z","compaction_level":0,"original_size":0,"dependencies":[{"issue_id":"bd-3uw","depends_on_id":"bd-2vm","type":"blocks","created_at":"2026-02-06T20:20:43.683961Z","created_by":"tayloreernisse"}]} {"id":"bd-3w4","title":"src/config.rs: Config structs, deep merge, load_config","description":"Full typed Config struct hierarchy with SectionBase flatten pattern. Deep merge via recursive serde_json::Value. Config loading: embedded defaults + user overrides from XDG/dot-config/legacy paths. Unknown key warnings via serde_ignored. All section configs per PRD.","status":"closed","priority":0,"issue_type":"task","created_at":"2026-02-06T18:10:21.652551Z","created_by":"tayloreernisse","updated_at":"2026-02-06T19:08:28.839621Z","closed_at":"2026-02-06T19:08:28.839576Z","close_reason":"Fully implemented during scaffold: config hierarchy, deep merge, load_config, unknown key warnings","compaction_level":0,"original_size":0,"labels":["core","phase-1"],"dependencies":[{"issue_id":"bd-3w4","depends_on_id":"bd-1m2","type":"blocks","created_at":"2026-02-06T18:11:30.450820Z","created_by":"tayloreernisse"}]} +{"id":"bd-4b1","title":"Enhance --dump-state with per-section timing and cache diagnostics","description":"## Background\nPRD specifies enhanced --dump-state output: width detection source, per-section render timing in microseconds, priority drop reasons, cache hit/miss per key, trend throttle state. Currently only basic info is output (terminal width, theme, VCS type, layout, paths, session_id). This diagnostic mode is critical for debugging render issues.\n\n## Approach\n\n**Step 1: Width detection source tracking**\nIn src/width.rs, change detect_raw() to return (u16, &str) where the &str identifies the source:\n```rust\nfn detect_raw(cli_width: Option, config_width: Option) -> (u16, &'static str) {\n if let Some(w) = cli_width { return (w, \"cli_flag\"); }\n // ... \"env_var\", \"config\", \"ioctl\", \"process_tree\", \"stty\", \"columns_env\", \"tput\", \"fallback\"\n}\n```\nStore the source in a thread-local or return from detect_width().\n\n**Step 2: Cache hit/miss tracking**\nAdd a diagnostics field to Cache:\n```rust\npub struct Cache {\n dir: Option,\n diagnostics: RefCell>,\n}\npub struct CacheDiag {\n pub key: String,\n pub hit: bool,\n pub age_ms: Option,\n}\n```\nIn get(), push a CacheDiag entry. Expose via cache.diagnostics() method.\n\n**Step 3: Per-section render timing**\nIn src/layout/mod.rs render_line(), wrap each render_section() call:\n```rust\nlet start = Instant::now();\nlet output = section::render_section(id, ctx);\nlet elapsed_us = start.elapsed().as_micros() as u64;\nsection_timings.push((id.clone(), elapsed_us));\n```\nReturn timings alongside the rendered line.\n\n**Step 4: Priority drop recording**\nIn src/layout/priority.rs, return (Vec, Vec) where the second Vec contains dropped section IDs with their priority level.\n\n**Step 5: Assemble in dump_state_output**\nCollect all diagnostics into the JSON output:\n```json\n{\n \"terminal\": { \"effective_width\": 174, \"source\": \"process_tree\", ... },\n \"sections\": [\n { \"id\": \"model\", \"render_us\": 12, \"dropped\": false },\n { \"id\": \"beads\", \"render_us\": 0, \"dropped\": true, \"drop_reason\": \"priority_3_tiered\" }\n ],\n \"cache\": [\n { \"key\": \"vcs_branch\", \"hit\": true, \"age_ms\": 1200 },\n { \"key\": \"load\", \"hit\": false }\n ],\n ...existing fields...\n}\n```\n\n## Acceptance Criteria\n- [ ] --dump-state=json includes \"terminal.source\" identifying width detection method\n- [ ] --dump-state=json includes \"sections\" array with per-section render_us timing\n- [ ] --dump-state=json includes \"cache\" array with hit/miss and age per key\n- [ ] --dump-state=json includes dropped sections with drop_reason\n- [ ] --dump-state=text outputs human-readable formatted version\n- [ ] Diagnostics collection adds <1ms overhead to normal renders\n- [ ] cargo test; cargo clippy --all-targets -- -D warnings\n\n## TDD Loop\nRED: test_dump_state_json_has_sections, test_dump_state_json_has_cache_diag, test_dump_state_json_has_width_source\nGREEN: Instrument width.rs, cache.rs, layout/mod.rs, priority.rs\nVERIFY: cargo test -- dump_state\n\n## Files\n- src/width.rs (return detection source from detect_raw)\n- src/cache.rs (add CacheDiag tracking via RefCell)\n- src/layout/mod.rs (add per-section timing collection)\n- src/layout/priority.rs (return dropped section IDs)\n- src/bin/claude-statusline.rs (collect all diagnostics, assemble dump_state JSON)\n\n## Edge Cases\n- --dump-state with --test: uses mock data but shows real timing and detection\n- Cache disabled (dir: None): all keys show miss, age_ms: null\n- No sections dropped: \"sections\" array has all items with dropped: false\n- --dump-state=text: tabular format, human-readable (no JSON parsing needed)","status":"open","priority":3,"issue_type":"task","created_at":"2026-02-06T20:20:25.542018Z","created_by":"tayloreernisse","updated_at":"2026-02-06T20:24:09.847667Z","compaction_level":0,"original_size":0,"dependencies":[{"issue_id":"bd-4b1","depends_on_id":"bd-62g","type":"blocks","created_at":"2026-02-06T20:20:43.707878Z","created_by":"tayloreernisse"}]} {"id":"bd-4z1","title":"src/metrics.rs: ComputedMetrics from InputData","description":"Compute derived metrics once: cost_velocity, token_velocity, usage_pct, total_tokens, cache_efficiency_pct. Reused by all sections via RenderContext.metrics.","status":"closed","priority":1,"issue_type":"task","created_at":"2026-02-06T18:10:42.944244Z","created_by":"tayloreernisse","updated_at":"2026-02-06T19:08:28.970834Z","closed_at":"2026-02-06T19:08:28.970785Z","close_reason":"Fully implemented during scaffold: ComputedMetrics with from_input computing all derived values","compaction_level":0,"original_size":0,"labels":["core","phase-1"],"dependencies":[{"issue_id":"bd-4z1","depends_on_id":"bd-2bo","type":"blocks","created_at":"2026-02-06T18:11:33.222149Z","created_by":"tayloreernisse"}]} +{"id":"bd-62g","title":"Upgrade SectionDescriptor registry with metadata (estimated_width, shell_out)","description":"## Background\nPRD specifies a registry of SectionDescriptor { id, render_fn, priority, is_spacer, is_flex, estimated_width, shell_out }. Current registry() returns Vec<(&str, RenderFn)> without metadata. The metadata enables lazy shell-outs (skip commands for dropped sections), --list-sections with rich info, and cost estimation for render budget.\n\n## Approach\n\n**Step 1: Define SectionDescriptor struct (already in mod.rs, needs population)**\n```rust\npub struct SectionDescriptor {\n pub id: &'static str,\n pub render: RenderFn,\n pub priority: u8,\n pub is_spacer: bool,\n pub is_flex: bool,\n pub estimated_width: u16,\n pub shell_out: bool,\n}\n```\n\n**Step 2: Populate registry with all 24 sections**\n```rust\npub fn registry() -> Vec {\n vec![\n SectionDescriptor { id: \"model\", render: model::render, priority: 2, is_spacer: false, is_flex: false, estimated_width: 12, shell_out: false },\n SectionDescriptor { id: \"provider\", render: provider::render, priority: 2, is_spacer: false, is_flex: false, estimated_width: 10, shell_out: false },\n SectionDescriptor { id: \"project\", render: project::render, priority: 1, is_spacer: false, is_flex: false, estimated_width: 20, shell_out: false },\n SectionDescriptor { id: \"vcs\", render: vcs::render, priority: 1, is_spacer: false, is_flex: false, estimated_width: 15, shell_out: true },\n SectionDescriptor { id: \"beads\", render: beads::render, priority: 3, is_spacer: false, is_flex: false, estimated_width: 25, shell_out: true },\n SectionDescriptor { id: \"context_bar\", render: context_bar::render, priority: 1, is_spacer: false, is_flex: true, estimated_width: 18, shell_out: false },\n SectionDescriptor { id: \"context_usage\", render: context_usage::render, priority: 2, is_spacer: false, is_flex: false, estimated_width: 12, shell_out: false },\n SectionDescriptor { id: \"context_remaining\", render: context_remaining::render, priority: 2, is_spacer: false, is_flex: false, estimated_width: 10, shell_out: false },\n SectionDescriptor { id: \"tokens_raw\", render: tokens_raw::render, priority: 3, is_spacer: false, is_flex: false, estimated_width: 18, shell_out: false },\n SectionDescriptor { id: \"cache_efficiency\", render: cache_efficiency::render, priority: 2, is_spacer: false, is_flex: false, estimated_width: 10, shell_out: false },\n SectionDescriptor { id: \"cost\", render: cost::render, priority: 1, is_spacer: false, is_flex: false, estimated_width: 8, shell_out: false },\n SectionDescriptor { id: \"cost_velocity\", render: cost_velocity::render, priority: 2, is_spacer: false, is_flex: false, estimated_width: 10, shell_out: false },\n SectionDescriptor { id: \"token_velocity\", render: token_velocity::render, priority: 2, is_spacer: false, is_flex: false, estimated_width: 14, shell_out: false },\n SectionDescriptor { id: \"cost_trend\", render: cost_trend::render, priority: 3, is_spacer: false, is_flex: false, estimated_width: 8, shell_out: false },\n SectionDescriptor { id: \"context_trend\", render: context_trend::render, priority: 3, is_spacer: false, is_flex: false, estimated_width: 8, shell_out: false },\n SectionDescriptor { id: \"lines_changed\", render: lines_changed::render, priority: 2, is_spacer: false, is_flex: false, estimated_width: 10, shell_out: false },\n SectionDescriptor { id: \"duration\", render: duration::render, priority: 2, is_spacer: false, is_flex: false, estimated_width: 5, shell_out: false },\n SectionDescriptor { id: \"tools\", render: tools::render, priority: 2, is_spacer: false, is_flex: false, estimated_width: 15, shell_out: false },\n SectionDescriptor { id: \"turns\", render: turns::render, priority: 3, is_spacer: false, is_flex: false, estimated_width: 8, shell_out: false },\n SectionDescriptor { id: \"load\", render: load::render, priority: 3, is_spacer: false, is_flex: false, estimated_width: 10, shell_out: true },\n SectionDescriptor { id: \"version\", render: version::render, priority: 3, is_spacer: false, is_flex: false, estimated_width: 8, shell_out: false },\n SectionDescriptor { id: \"time\", render: time::render, priority: 3, is_spacer: false, is_flex: false, estimated_width: 5, shell_out: true },\n SectionDescriptor { id: \"output_style\", render: output_style::render, priority: 2, is_spacer: false, is_flex: false, estimated_width: 10, shell_out: false },\n SectionDescriptor { id: \"hostname\", render: hostname::render, priority: 3, is_spacer: false, is_flex: false, estimated_width: 10, shell_out: true },\n ]\n}\n```\n\n**Step 3: Update render_section to use SectionDescriptor**\nUse descriptor.render instead of linear scan.\n\n**Step 4: Update --list-sections output**\nPrint tab-separated: id, priority, flex, shell_out, estimated_width.\n\n**Step 5: Update section_meta() in layout/mod.rs**\nRead priority and is_flex from registry descriptors instead of placeholder (2, false).\n\n## Acceptance Criteria\n- [ ] registry() returns Vec with all 24 built-in sections\n- [ ] Each descriptor has accurate priority, is_flex, estimated_width, shell_out values\n- [ ] render_section() uses descriptor.render correctly\n- [ ] section_meta() reads from registry (fixes the current placeholder)\n- [ ] --list-sections outputs id + metadata (not just IDs)\n- [ ] Custom sections still handled by custom::render fallback (not in registry)\n- [ ] cargo test; cargo clippy --all-targets -- -D warnings\n\n## TDD Loop\nRED: test_registry_has_all_sections (count=24), test_section_meta_reads_registry, test_list_sections_output_format\nGREEN: Populate SectionDescriptor, update section_meta\nVERIFY: cargo test -- registry && cargo test -- section_meta\n\n## Files\n- src/section/mod.rs (change registry return type, update render_section, populate all descriptors)\n- src/layout/mod.rs (update section_meta to read from registry)\n- src/bin/claude-statusline.rs (update --list-sections output format)\n\n## Edge Cases\n- Custom sections not in registry: handled by custom::render fallback path\n- estimated_width is approximate: used for budget estimation, not exact rendering\n- Priority values must match defaults.json section config defaults\n- section_meta currently returns (2, false) for everything - this fix corrects layout behavior","status":"open","priority":3,"issue_type":"task","created_at":"2026-02-06T20:20:34.234266Z","created_by":"tayloreernisse","updated_at":"2026-02-06T20:23:41.949637Z","compaction_level":0,"original_size":0} {"id":"bd-7nx","title":"src/layout/mod.rs: resolve_layout, render_line, render_all","description":"Three-phase pipeline: Plan > Render survivors > Reflow. resolve_layout (preset lookup, responsive override). render_line per layout line. render_all joins with newlines.","status":"closed","priority":0,"issue_type":"task","created_at":"2026-02-06T18:10:57.309119Z","created_by":"tayloreernisse","updated_at":"2026-02-06T19:14:32.902849Z","closed_at":"2026-02-06T19:14:32.902096Z","close_reason":"Full 3-phase layout pipeline: ActiveSection, render_line with priority/flex/justify, assemble_left, section_meta","compaction_level":0,"original_size":0,"labels":["layout","phase-2"],"dependencies":[{"issue_id":"bd-7nx","depends_on_id":"bd-2u8","type":"blocks","created_at":"2026-02-06T18:11:35.699600Z","created_by":"tayloreernisse"}]} {"id":"bd-9hl","title":"src/width.rs: Terminal width detection chain with memoization","description":"Full priority chain: cli > env > config > ioctl > process tree walk > stty > COLUMNS > tput > 120. 1s in-memory memoization via Mutex>. WidthTier enum.","status":"closed","priority":1,"issue_type":"task","created_at":"2026-02-06T18:11:06.042830Z","created_by":"tayloreernisse","updated_at":"2026-02-06T19:16:28.937658Z","closed_at":"2026-02-06T19:16:28.937612Z","close_reason":"Full 9-level width detection chain with 1s memoization: cli > env > config > ioctl > process tree > stty > COLUMNS > tput > 120","compaction_level":0,"original_size":0,"labels":["core","phase-3"]} {"id":"bd-ape","title":"Phase 1 verification: --test produces correct output for all pure sections","description":"Create test_data.json with realistic mock data. Verify --test renders all 17 pure sections correctly. Snapshot test.","status":"closed","priority":1,"issue_type":"task","created_at":"2026-02-06T18:11:19.363621Z","created_by":"tayloreernisse","updated_at":"2026-02-06T19:21:21.610831Z","closed_at":"2026-02-06T19:21:21.610781Z","close_reason":"Phase 1 verification complete. --test produces 3-line verbose output with all pure and shell-out sections rendering correctly. Release build 864K, clippy/fmt/test all clean.","compaction_level":0,"original_size":0,"labels":["phase-1","verification"],"dependencies":[{"issue_id":"bd-ape","depends_on_id":"bd-3hc","type":"blocks","created_at":"2026-02-06T18:11:33.279153Z","created_by":"tayloreernisse"}]} +{"id":"bd-khk","title":"Wire --no-cache, --no-shell, --clear-cache CLI flags","description":"## Background\nPRD specifies three CLI flags (--no-cache, --no-shell, --clear-cache) that appear in --help output but are never parsed or wired. Users see them advertised but they have no effect. The variables are extracted (no_cache, no_shell, clear_cache) around line 95 of src/bin/claude-statusline.rs but never passed downstream.\n\n## Approach\n1. **--clear-cache**: After config load, resolve cache dir path using session_id, remove it via fs::remove_dir_all, print confirmation, exit 0. If no stdin project_dir, scan /tmp/claude-sl-* owned by current user.\n2. **--no-cache**: Pass flag to Cache::new() — return Cache { dir: None } so all get() returns None, set() is no-op. Or add a Cache::disabled() constructor.\n3. **--no-shell**: Add no_shell: bool to RenderContext. Shell-out sections (vcs, beads, load, hostname, time, custom) check ctx.no_shell; if true, return stale cache value or None.\n\n## Acceptance Criteria\n- [ ] --no-cache causes all cache reads to miss and writes to be skipped\n- [ ] --no-shell causes all shell-outs to be skipped (stale cache served if available)\n- [ ] --clear-cache removes cache dir for current session and exits 0\n- [ ] --clear-cache with nonexistent cache dir exits 0 without error\n- [ ] cargo test passes; cargo clippy --all-targets -- -D warnings passes\n\n## Files\n- src/bin/claude-statusline.rs (wire flags into Cache and RenderContext)\n- src/cache.rs (add disabled mode)\n- src/section/mod.rs (add no_shell to RenderContext)\n- src/section/vcs.rs, beads.rs, load.rs, hostname.rs, time.rs, custom.rs (check no_shell)\n\n## Edge Cases\n- --clear-cache with no stdin: needs project_dir; consider reading stdin first or scanning /tmp/claude-sl-*\n- --no-shell + --no-cache: only pure sections render, no dynamic data","status":"open","priority":1,"issue_type":"task","created_at":"2026-02-06T20:18:30.201511Z","created_by":"tayloreernisse","updated_at":"2026-02-06T20:18:30.201511Z","compaction_level":0,"original_size":0} {"id":"bd-s6n","title":"src/cache.rs: Secure dir, per-key TTL, flock, atomic writes","description":"Cache struct with secure dir creation (chmod 700, ownership verify, symlink check). Per-key TTL via mtime. Atomic write-rename. Non-blocking flock. Stale fallback. Session ID from MD5 of project_dir.","status":"closed","priority":1,"issue_type":"task","created_at":"2026-02-06T18:11:09.744524Z","created_by":"tayloreernisse","updated_at":"2026-02-06T19:16:28.912385Z","closed_at":"2026-02-06T19:16:28.912319Z","close_reason":"Full cache: secure dir, ownership check, per-key TTL, flock, atomic writes","compaction_level":0,"original_size":0,"labels":["core","phase-3"]} {"id":"bd-wdi","title":"src/theme.rs: COLORFGBG detection","description":"Theme enum (Dark/Light). Detection: config override > COLORFGBG env var > default dark.","status":"closed","priority":1,"issue_type":"task","created_at":"2026-02-06T18:10:25.956848Z","created_by":"tayloreernisse","updated_at":"2026-02-06T19:08:28.883445Z","closed_at":"2026-02-06T19:08:28.883397Z","close_reason":"Fully implemented during scaffold: Theme enum, COLORFGBG detection","compaction_level":0,"original_size":0,"labels":["core","phase-1"],"dependencies":[{"issue_id":"bd-wdi","depends_on_id":"bd-1m2","type":"blocks","created_at":"2026-02-06T18:11:30.490706Z","created_by":"tayloreernisse"}]} +{"id":"bd-ywx","title":"Implement shell allow/deny lists and shell_env merge","description":"## Background\nPRD specifies shell execution controls: global.shell_enabled, shell_allowlist, shell_denylist, shell_env merge, and shell_max_output_bytes. These gate which commands can run and what environment they inherit. Currently exec_with_timeout has no access control.\n\n## Approach\n\n**Step 1: Define ShellConfig struct in src/shell.rs**\n```rust\npub struct ShellConfig {\n pub enabled: bool, // global.shell_enabled\n pub allowlist: Vec, // global.shell_allowlist\n pub denylist: Vec, // global.shell_denylist\n pub env: HashMap, // global.shell_env\n pub max_output_bytes: usize, // global.shell_max_output_bytes (default 8192)\n pub timeout: Duration, // global.shell_timeout_ms\n}\n```\n\n**Step 2: Add gated execution function**\n```rust\npub fn exec_gated(\n program: &str,\n args: &[&str],\n dir: Option<&str>,\n shell_config: &ShellConfig,\n) -> Option {\n // 1. Check enabled\n if \\!shell_config.enabled { return None; }\n // 2. Denylist first (wins over allowlist)\n if shell_config.denylist.iter().any(|d| d == program) { return None; }\n // 3. Allowlist (empty = all allowed)\n if \\!shell_config.allowlist.is_empty()\n && \\!shell_config.allowlist.iter().any(|a| a == program) { return None; }\n // 4. Execute with merged env\n let result = exec_with_timeout_env(program, args, dir, shell_config.timeout, &shell_config.env)?;\n // 5. Truncate output\n if shell_config.max_output_bytes > 0 && result.len() > shell_config.max_output_bytes {\n Some(result[..shell_config.max_output_bytes].to_string())\n } else { Some(result) }\n}\n```\n\n**Step 3: Modify exec_with_timeout to accept extra env**\nAdd shell_env to Command::envs() call, layered ON TOP of GIT_ENV (shell_env overrides GIT_ENV).\n\n**Step 4: Add ShellConfig to RenderContext and migrate all sections**\n\n## Acceptance Criteria\n- [ ] New ShellConfig struct holds all shell execution parameters\n- [ ] exec_gated() checks enabled -> denylist -> allowlist -> execute\n- [ ] Denylist takes precedence over allowlist when both contain same command\n- [ ] shell_env vars merged into command environment (layers on GIT_ENV)\n- [ ] Output truncated to max_output_bytes (0 = unlimited)\n- [ ] Empty allowlist means all commands allowed\n- [ ] Skipped commands return None (caller falls back to stale cache)\n- [ ] All shell-out sections migrated to exec_gated\n- [ ] cargo test; cargo clippy --all-targets -- -D warnings\n\n## TDD Loop\nRED: tests in src/shell.rs - test_denylist_blocks, test_allowlist_gates, test_env_merge, test_output_truncation, test_denylist_wins_over_allowlist\nGREEN: Implement ShellConfig + exec_gated\nVERIFY: cargo test -- shell\n\n## Files\n- src/shell.rs (add ShellConfig struct, exec_gated function, modify exec_with_timeout for env)\n- src/section/mod.rs (add shell_config to RenderContext)\n- src/bin/claude-statusline.rs (construct ShellConfig from config, pass to ctx)\n- src/section/vcs.rs, beads.rs, load.rs, hostname.rs, time.rs, custom.rs (migrate to exec_gated)\n\n## Edge Cases\n- Allowlist and denylist both contain \"git\": denylist wins\n- max_output_bytes = 0: no truncation\n- shell_env overrides GIT_ENV for same key: intentional (user override)\n- Truncation in middle of UTF-8: truncate at byte boundary, then trim to valid UTF-8","status":"open","priority":2,"issue_type":"task","created_at":"2026-02-06T20:19:22.931468Z","created_by":"tayloreernisse","updated_at":"2026-02-06T20:23:17.072084Z","compaction_level":0,"original_size":0}