feat: implement remaining PRD features (10 beads)

Complete the PRD feature set with shell gating pipeline, cache
improvements, layout enhancements, and diagnostics:

- Shell: exec_gated with allowlist/denylist, circuit breaker, env merge
- Shell: parallel prefetch via std::thread::scope for cold renders
- Cache: TTL jitter (FNV-1a), config hash namespace, garbage collection
- Cache: diagnostic tracking (hit/miss, age) for dump-state
- Layout: gradual drop strategy (one-by-one vs tiered)
- Layout: render budget timer with graceful priority-based degradation
- Layout: breakpoint hysteresis to prevent preset toggling
- Width: detection source tracking for diagnostics
- CLI: --no-cache, --no-shell, --clear-cache, env var overrides
- Diagnostics: enhanced --dump-state with section timing and cache stats

Closes: bd-3oy, bd-62g, bd-khk, bd-3q1, bd-ywx, bd-3l2,
        bd-2vm, bd-1if, bd-2qr, bd-30o, bd-3ax, bd-3uw, bd-4b1

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Taylor Eernisse
2026-02-06 15:59:15 -05:00
parent 73401beb47
commit 4c9139ec42
15 changed files with 1198 additions and 138 deletions

View File

@@ -13,18 +13,24 @@ pub fn render(ctx: &RenderContext) -> Option<SectionOutput> {
return None;
}
// --no-shell: serve stale cache only
if ctx.no_shell {
return render_from_cache(ctx, ctx.cache.get_stale("beads_summary")?);
}
let ttl = Duration::from_secs(ctx.config.sections.beads.ttl);
let timeout = Duration::from_millis(200);
let cached = ctx.cache.get("beads_summary", ttl);
let summary = cached.or_else(|| {
// Run br ready to get count of ready items
let out = shell::exec_with_timeout(
"br",
&["ready", "--json"],
Some(ctx.project_dir.to_str()?),
timeout,
)?;
// Use prefetched result if available, otherwise exec
let out = ctx.shell_results.get("beads").cloned().unwrap_or_else(|| {
shell::exec_gated(
ctx.shell_config,
"br",
&["ready", "--json"],
Some(ctx.project_dir.to_str()?),
)
})?;
// Count JSON array items (simple: count opening braces at indent level 1)
let count = out.matches("\"id\"").count();
let summary = format!("{count}");
@@ -46,3 +52,17 @@ pub fn render(ctx: &RenderContext) -> Option<SectionOutput> {
Some(SectionOutput { raw, ansi })
}
fn render_from_cache(ctx: &RenderContext, summary: String) -> Option<SectionOutput> {
let count: usize = summary.trim().parse().unwrap_or(0);
if count == 0 {
return None;
}
let raw = format!("{count} ready");
let ansi = if ctx.color_enabled {
format!("{}{raw}{}", color::DIM, color::RESET)
} else {
raw.clone()
};
Some(SectionOutput { raw, ansi })
}