2 Commits
Author SHA1 Message Date
james.maesandClaude Opus 4.7 cfd469f72b feat(round-4): probe severity model — ok/soft/loud/critical
Round 4 of boot-health work. The Toby-the-dog incident — Jarvis said "Got
it" while the embedder model was missing and the fact never persisted —
proved that Round 2's probe banner was too quiet. Probes were already
reporting `.degraded` correctly; what was missing was a way for the App
layer to route a critical failure differently from a soft one (e.g.,
non-dismissible banner for memory off vs. silent Status row for camera
TCC not yet requested).

This slice introduces a severity axis without changing behavior:

- `FailureSeverity { critical, loud, soft }` on `ProbeStatus`'s non-ok
  cases (degraded/failed/unknown). `.ok` carries no severity.
- `ProbeStatus.severity` accessor (nil for `.ok`).
- `BootHealthSnapshot.overallHealth: OverallHealth { ok, soft, loud,
  critical }` — worst severity wins; `.ok` only if every subsystem ok.
- Each concrete probe annotates its failure modes:
  * Memory: failed → .critical (agent grounding; the Toby case)
  * Anthropic: missing/empty key → .critical (agent can't run)
  * Ollama: missing models → .loud (fact extraction broken, chat fine)
  * Voice: mic denied → .loud; dormant install → .soft
  * Vision: TCC denied → .soft (vision is optional)
  * MCP: 0 tools → .critical; <minimum → .loud
  * Replay: init failed → .critical; missing file → .loud
  * Webview: mismatched/timedOut → .critical; sentHello → .soft
- DormantSubsystemProbe gains `severity` parameter (default .soft);
  AppDelegate wires memory-dormant as .critical.

No behavioral change yet — banners still treat all failures equally;
the agent system prompt is unchanged. Slices 2 + 3 land those.

Tests: 13/13 in BootHealthOrchestratorTests pass, including 4 new tests
for overallHealth + severity accessor + JSON roundtrip with severity.
2 pre-existing failures in LLMEventTests/OllamaRequestBodyTests are
unrelated (qwen model-ID literal drift from commit 8d0a959, present
before this slice — confirmed via git stash).

Boundary gates 18/18 green.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-11 15:24:43 -05:00
james.maesandClaude Opus 4.7 67748f5336 feat(round-3): Status… menu item + NSPanel — live re-probe on open
New AppKit panel (NSPanel + SwiftUI hosting view) renders the live
BootHealthSnapshot, re-probes on every appearance, and offers
"Re-probe all" + "Copy report" buttons. Operator/dev tool, not a
user-facing surface — AppKit deliberately so it doesn't ride the
R3F render pipeline.

Anti-fake-status carries forward from Round 2: the panel's `.task`
modifier fires a fresh `runBootHealth()` sweep on every open, never
showing a cached boot snapshot. Cmd+R re-probes; Cmd+Shift+C copies.

Non-modal: NSPanel with [.titled, .closable, .resizable,
.nonactivatingPanel] + makeKeyAndOrderFront(_:) — no runModal calls.
scripts/check-no-modal-presentation.sh stays green.
isReleasedWhenClosed=false so the user can close and re-open without
re-creating the model (preserves "Copied X B" caption + last
snapshot across opens, in addition to surviving the AppKit
"window deallocs on close" default that would otherwise crash on
second open).

Files:
  - App/MenuBar/StatusPanel.swift (new):
    * StatusPanelModel — @MainActor ObservableObject, owns the
      snapshot + isProbing + copyConfirmation @Published state.
      `probeRunner` closure injected so "Re-probe all" hits the
      production AppDelegate.runBootHealth() path (single source
      of truth — panel + boot use the same code).
    * StatusPanelView — SwiftUI body. Scrollable rows, header
      with spinner during probe, footer with re-probe + copy
      buttons + caption.
    * SubsystemRow (private) — glyph (✓/⚠/✗/?), name, status
      label, latency, evidence, reason. Colors per status
      (green/orange/red/gray). Multi-line evidence + reason.
    * StatusPanel: NSPanel — hosts the SwiftUI view, `present()`
      method centers + brings front + activates app.
  - App/MenuBar/MenuBarContextMenu.swift:
    * `build` now takes a `statusAction:` closure (5th param).
    * "Status…" NSMenuItem between "Settings…" and "Show Dev Overlay".
  - App/AppDelegate.swift:
    * statusPanel: StatusPanel? lazy property.
    * openStatusPanel() method — lazy-creates panel on first click,
      passes self.runBootHealth as the probe runner closure.
    * installMenuBar's MenuBarContextMenu.build call updated for
      the new param.
  - Jarvis.xcodeproj/project.pbxproj: PBXBuildFile + PBXFileReference
    for StatusPanel.swift, added to MenuBar PBXGroup + Sources phase.

Verification:
  - bash scripts/check-app-builds.sh: PASS
  - bash scripts/check-no-modal-presentation.sh: OK
  - bash scripts/check-install-order.sh: PASS
  - bash scripts/check-no-leftover-stubs.sh: PASS

Dogfooding next: cold-launch → right-click menu bar icon → Status…,
verify rows render with concrete evidence (mic device name, model
ID list, etc.); click Re-probe and confirm rows refresh with new
latency values; click Copy report and paste into a text editor to
verify JSON is well-formed and complete.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-11 14:12:37 -05:00