From 3e8cf4ba8f6d0f98572bf035c566ccd6e1e418f6 Mon Sep 17 00:00:00 2001 From: Jeremy McSpadden Date: Sat, 21 Mar 2026 09:34:45 -0500 Subject: [PATCH] feat: surface doctor issue details in progress score widget and health views (#1667) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: surface real doctor issue details in progress score widget Previously the progress score traffic light (green/yellow/red) only showed generic labels like "2 consecutive error units" or "Health trend declining". The actual doctor issue descriptions were computed in auto-post-unit but discarded before reaching the widget — only aggregate counts were stored in HealthSnapshot. Now the full data flows through: - HealthSnapshot stores issue details (code, message, severity, unitId) and fix descriptions alongside the counts - recordHealthSnapshot() accepts optional issue/fix arrays (backwards compatible — existing callers unchanged) - getLatestHealthIssues() and getLatestHealthFixes() retrieve the most recent details for display - computeProgressScore() surfaces up to 5 real issue messages (errors first) and up to 3 recent fixes as ProgressSignals when the level is yellow or red - Dashboard overlay renders signal details with ✓/✗/· icons below the traffic light when degraded This gives real-time visibility into what the auto-doctor is detecting and fixing, without requiring manual /gsd doctor runs or opening the full dashboard to investigate. * feat: integrate doctor health data into visualizer and HTML reports Phase 2b: close visibility gaps across visualizer and export surfaces. Persistence (doctor.ts): - Enrich DoctorHistoryEntry with issue details (severity, code, message, unitId) and fix descriptions - appendDoctorHistory now persists up to 10 issues per entry and all fix descriptions to doctor-history.jsonl - Export DoctorHistoryEntry type for consumers Data layer (visualizer-data.ts): - Add VisualizerDoctorEntry and VisualizerProgressScore types - Extend HealthInfo with doctorHistory (last 20 persisted entries) and progressScore (current in-memory traffic light) - loadHealth reads doctor-history.jsonl synchronously and snapshots current progress score when health data exists TUI visualizer (visualizer-views.ts): - Health tab now shows "Progress Score" section with traffic light icon, summary, and all signal details (✓/✗/· prefixed) - Health tab now shows "Doctor History" section with timestamped entries, issue messages, and applied fixes HTML export (export-html.ts): - Health section includes progress score with colored indicator and signal breakdown - Health section includes "Doctor Run History" table with timestamps, error/warning/fix counts, issue codes, expandable issue messages, and fix descriptions * feat: fill remaining health gaps — scope tagging, level notifications, human-readable logs Gap fills: Per-milestone/slice scope tagging: - HealthSnapshot now stores scope (e.g. "M001/S02") from the doctor run's unit context - DoctorHistoryEntry persists scope to doctor-history.jsonl - Visualizer and HTML reports display scope tags per entry State transition notifications: - setLevelChangeCallback() registers a handler for progress level changes (green→yellow, yellow→red, red→green, etc.) - auto-start.ts wires the callback to ctx.ui.notify on start - auto.ts clears it on stop - Notifications include the triggering issue message Human-readable formatting throughout: - formatHealthSummary() uses full words: "2 errors, 3 warnings · trend degrading · 1 fix applied · 1 of 5 consecutive errors before escalation · latest: Missing PLAN.md for S03" - DoctorHistoryEntry stores a human-readable summary field built from error counts, fix counts, and top issue message - Visualizer doctor history shows summary instead of "2E 1W 0F" - HTML export doctor table uses summary column with scope tags - Post-unit notification says what was fixed ("Doctor: rebuilt STATE.md; cleared stale lock") instead of "applied 2 fix(es)" Test updates: - formatHealthSummary assertions updated for new readable format * fix: default UAT type to artifact-driven to prevent unnecessary auto-mode pauses (#1651) When a UAT file has no `## UAT Type` section, `extractUatType()` returns `undefined`. The fallback was `"human-experience"`, causing `pauseAfterDispatch: true` in the auto-dispatch rule. Since doctor-generated UAT placeholders never include a UAT Type section and LLM-executed UATs are always artifact-driven, the correct default is `"artifact-driven"`. Closes #1649 Co-authored-by: Claude Opus 4.6 (1M context) * fix: remove duplicate doctorScope declaration (CI build fix) * fix: resolve PR1644 regressions in health views and post-unit hook * fix: add spacing to commit time display and show issue details in widget - Remove space-stripping from git timeAgo ("82seconds" → "82 seconds") - Show up to 3 negative health signals below the widget header when degraded (yellow/red), so you see what's actually wrong without opening the dashboard --------- Co-authored-by: TÂCHES Co-authored-by: Claude Opus 4.6 (1M context) --- src/resources/extensions/gsd/auto-dashboard.ts | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/src/resources/extensions/gsd/auto-dashboard.ts b/src/resources/extensions/gsd/auto-dashboard.ts index f2c77d168..d0369bd37 100644 --- a/src/resources/extensions/gsd/auto-dashboard.ts +++ b/src/resources/extensions/gsd/auto-dashboard.ts @@ -289,7 +289,7 @@ function refreshLastCommit(basePath: string): void { const sep = raw.indexOf("|"); if (sep > 0) { cachedLastCommit = { - timeAgo: raw.slice(0, sep).replace(/ ago$/, "").replace(/ /g, ""), + timeAgo: raw.slice(0, sep).replace(/ ago$/, ""), message: raw.slice(sep + 1), }; } @@ -505,6 +505,20 @@ export function updateProgressWidget( : ""; lines.push(rightAlign(headerLeft, headerRight, width)); + // Show health signal details when degraded (yellow/red) + if (score.level !== "green" && score.signals.length > 0 && widgetMode !== "min") { + // Show up to 3 most relevant signals in compact form + const topSignals = score.signals + .filter(s => s.kind === "negative") + .slice(0, 3); + if (topSignals.length > 0) { + const signalStr = topSignals + .map(s => theme.fg("dim", s.label)) + .join(theme.fg("dim", " · ")); + lines.push(`${pad} ${signalStr}`); + } + } + // ── Gather stats (needed by multiple modes) ───────────────────── const cmdCtx = accessors.getCmdCtx(); let totalInput = 0;