Four fixes for auto-mode telemetry and display bugs:
1. Metrics idempotency guard (metrics.ts)
- snapshotUnitMetrics now deduplicates entries by type+id+startedAt
- Prevents idle-watchdog from creating N duplicate entries per unit
- On duplicate: updates existing entry in-place instead of appending
- Observed: 31 duplicate entries for a single plan-slice unit
2. Elapsed time zero-guard (auto.ts, auto-dashboard.ts, dashboard-overlay.ts)
- getAutoDashboardData guards against autoStartTime=0 (uninitialized)
- formatAutoElapsed rejects negative, NaN, and >30-day values
- Dashboard overlay adds 30-day sanity check before formatting
- Observed: dashboard showed '492804h' (Date.now() - 0)
3. Em/en-dash title auto-fix (doctor.ts)
- Doctor now sanitizes em/en dashes in milestone H1 titles when fix=true
- Replaces Unicode dashes with ASCII hyphens in the roadmap file
- Prevents state document delimiter ambiguity
- delimiter_in_title issues are now marked fixable=true
4. Tests for all three fix areas
- Metrics: idempotency guard, simulated watchdog duplicate pattern
- Dashboard: negative/NaN autoStartTime handling
- Doctor: em-dash auto-fix with fix=true and fix=false verification
Root cause analysis:
- The idle watchdog (auto-timers.ts) calls closeoutUnit every 15s when
idle is detected. closeoutUnit calls snapshotUnitMetrics which blindly
appended to ledger.units. Each watchdog tick created a new entry with
identical type/id/startedAt but incremented finishedAt.
- autoStartTime defaults to 0 in the session class. If getAutoDashboardData
is called before auto-start sets the value, elapsed = Date.now() - 0.
- Milestone titles with em-dashes (U+2014) are written by the LLM during
roadmap creation and never sanitized, causing permanent doctor warnings.