Closes the v0.1 Bus/HUD S1 cluster — two critical audit findings from .planning/audit-2026-05-12/bus-hud-audit.md that broke basic UI signals:
#40 — BridgeNavigationDelegate had no didFail* arm. A 404 on
the JS bundle or any pre-mount JS error left the handshake stuck in .idle forever; the 2 s HandshakeTiming.timeout only armed from didFinish, so the user saw a silent black HUD with no banner and no
hard-block. Now both didFailProvisionalNavigation and didFail
route through WebviewBridge.failHandshake(reason:) → new terminal HandshakeState.loadFailed(reason:) + a critical-priority
non-dismissible HUDBannerCoordinator enqueue (same pipeline the
Round 4 bootHealthCritical banners use).
#41 — MenuBarIconController.transition(to:) was
production-dead. The full CABasicAnimation surface (breath / rotate /
shimmer / glow) shipped and was unit-tested, but HudStateCoordinator's emit closure only fanned state to the bus; the
menu-bar icon never animated. Expanded the emit closure to also call menuBarController?.transition(to: appState) so the icon follows
agent state.
App/AppDelegate.swift — added onDidFail to BridgeNavigationDelegate; new handleWebviewLoadFailure(_:) routes to failHandshake + critical banner.
App/Boot/BootHealthProbes.swift — new switch arm for .loadFailed in WebviewBootHealthProbe.
Tests: HandshakeTests +4; BridgeNavigationFailureTests (new App-level file) — drives both didFailProvisionalNavigation and didFail arms + the end-to-end handleWebviewLoadFailure path.
Issue #41 (fix(hud): fan HudState emit to MenuBarIconController…)
App/AppDelegate.swift — emit closure in installBus() now fans to both bridge.send(.hudState(...)) and menuBarController?.transition(to: appState).
Test: MenuBarIconStateTransitionTests (new) — drives the live coordinator via dormantAgentContinuation, asserts icon state advances through .thinking → .speaking → .idle.
Invariants preserved
scripts/check-single-writer-hudstate.sh — GREEN. transition(to:) writes the icon's currentState, not BusOutbound.hudState; the single-writer invariant only covers the .hudState(...) constructor surface.
scripts/check-no-evaluate-javascript.sh — GREEN. No new JS surfaces.
scripts/check-no-modal-presentation.sh — GREEN. Hard-block alert continues to route through allowlisted TCCAlertService.presentHardBlock.
All 18 boundary gates pass.
Test plan
swift test --package-path packages/Bus — 62 tests pass (+4 new failHandshake cases).
All 18 boundary gates pass (verified via manual sweep).
Manual: rename webview/index.html → index.html.bak and launch — verify a critical non-dismissible banner appears within ~100 ms (not after 2 s) instead of a silent black HUD.
Manual: observe menu-bar icon while submitting a turn — verify breath (listening) → rotate (thinking) → shimmer (speaking) transitions.
Notes for review
The hard-block alert (TCCAlertService.presentHardBlock) and the HUDBannerCoordinator banner BOTH fire on load failure — defense-in-depth. The alert is the existing path that already terminates on handshake-mismatch / timeout; the banner survives even if the alert is silenced (e.g., AppDelegate test seam with a no-op alertPresenter).
BridgeNavigationDelegate is now internal (not private) so tests in App/Tests/AppTests/ can drive its protocol methods directly via @testable import Jarvis. No other production code references it.
## Summary
Closes the v0.1 Bus/HUD S1 cluster — two critical audit findings from
`.planning/audit-2026-05-12/bus-hud-audit.md` that broke basic UI signals:
- **#40** — `BridgeNavigationDelegate` had no `didFail*` arm. A 404 on
the JS bundle or any pre-mount JS error left the handshake stuck in
`.idle` forever; the 2 s `HandshakeTiming.timeout` only armed from
`didFinish`, so the user saw a silent black HUD with no banner and no
hard-block. Now both `didFailProvisionalNavigation` and `didFail`
route through `WebviewBridge.failHandshake(reason:)` → new terminal
`HandshakeState.loadFailed(reason:)` + a critical-priority
non-dismissible `HUDBannerCoordinator` enqueue (same pipeline the
Round 4 `bootHealthCritical` banners use).
- **#41** — `MenuBarIconController.transition(to:)` was
production-dead. The full CABasicAnimation surface (breath / rotate /
shimmer / glow) shipped and was unit-tested, but
`HudStateCoordinator`'s emit closure only fanned state to the bus; the
menu-bar icon never animated. Expanded the emit closure to also call
`menuBarController?.transition(to: appState)` so the icon follows
agent state.
Two commits, one per issue.
## What changed
### Issue #40 (`fix(bus): surface webview load failures…`)
- `packages/Bus/Sources/Bus/Handshake.swift` — added `HandshakeState.loadFailed(reason:)` terminal case.
- `packages/Bus/Sources/Bus/WebviewBridge.swift` — added `failHandshake(reason:)` (idempotent, cancels timeout, alerts).
- `App/AppDelegate.swift` — added `onDidFail` to `BridgeNavigationDelegate`; new `handleWebviewLoadFailure(_:)` routes to `failHandshake` + critical banner.
- `App/Boot/BootHealthProbes.swift` — new switch arm for `.loadFailed` in `WebviewBootHealthProbe`.
- Tests: `HandshakeTests` +4; `BridgeNavigationFailureTests` (new App-level file) — drives both `didFailProvisionalNavigation` and `didFail` arms + the end-to-end `handleWebviewLoadFailure` path.
### Issue #41 (`fix(hud): fan HudState emit to MenuBarIconController…`)
- `App/AppDelegate.swift` — emit closure in `installBus()` now fans to both `bridge.send(.hudState(...))` and `menuBarController?.transition(to: appState)`.
- Test: `MenuBarIconStateTransitionTests` (new) — drives the live coordinator via `dormantAgentContinuation`, asserts icon state advances through `.thinking → .speaking → .idle`.
## Invariants preserved
- `scripts/check-single-writer-hudstate.sh` — GREEN. `transition(to:)` writes the icon's `currentState`, not `BusOutbound.hudState`; the single-writer invariant only covers the `.hudState(...)` constructor surface.
- `scripts/check-no-evaluate-javascript.sh` — GREEN. No new JS surfaces.
- `scripts/check-no-modal-presentation.sh` — GREEN. Hard-block alert continues to route through allowlisted `TCCAlertService.presentHardBlock`.
- All 18 boundary gates pass.
## Test plan
- [x] `swift test --package-path packages/Bus` — 62 tests pass (+4 new `failHandshake` cases).
- [x] `bash scripts/check-app-builds.sh` — App target compiles cleanly.
- [x] All 18 boundary gates pass (verified via manual sweep).
- [ ] **Manual:** rename `webview/index.html` → `index.html.bak` and launch — verify a critical non-dismissible banner appears within ~100 ms (not after 2 s) instead of a silent black HUD.
- [ ] **Manual:** observe menu-bar icon while submitting a turn — verify breath (listening) → rotate (thinking) → shimmer (speaking) transitions.
## Notes for review
- The hard-block alert (`TCCAlertService.presentHardBlock`) and the HUDBannerCoordinator banner BOTH fire on load failure — defense-in-depth. The alert is the existing path that already terminates on handshake-mismatch / timeout; the banner survives even if the alert is silenced (e.g., AppDelegate test seam with a no-op `alertPresenter`).
- `BridgeNavigationDelegate` is now `internal` (not `private`) so tests in `App/Tests/AppTests/` can drive its protocol methods directly via `@testable import Jarvis`. No other production code references it.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Summary
Closes the v0.1 Bus/HUD S1 cluster — two critical audit findings from
.planning/audit-2026-05-12/bus-hud-audit.mdthat broke basic UI signals:BridgeNavigationDelegatehad nodidFail*arm. A 404 onthe JS bundle or any pre-mount JS error left the handshake stuck in
.idleforever; the 2 sHandshakeTiming.timeoutonly armed fromdidFinish, so the user saw a silent black HUD with no banner and nohard-block. Now both
didFailProvisionalNavigationanddidFailroute through
WebviewBridge.failHandshake(reason:)→ new terminalHandshakeState.loadFailed(reason:)+ a critical-prioritynon-dismissible
HUDBannerCoordinatorenqueue (same pipeline theRound 4
bootHealthCriticalbanners use).MenuBarIconController.transition(to:)wasproduction-dead. The full CABasicAnimation surface (breath / rotate /
shimmer / glow) shipped and was unit-tested, but
HudStateCoordinator's emit closure only fanned state to the bus; themenu-bar icon never animated. Expanded the emit closure to also call
menuBarController?.transition(to: appState)so the icon followsagent state.
Two commits, one per issue.
What changed
Issue #40 (
fix(bus): surface webview load failures…)packages/Bus/Sources/Bus/Handshake.swift— addedHandshakeState.loadFailed(reason:)terminal case.packages/Bus/Sources/Bus/WebviewBridge.swift— addedfailHandshake(reason:)(idempotent, cancels timeout, alerts).App/AppDelegate.swift— addedonDidFailtoBridgeNavigationDelegate; newhandleWebviewLoadFailure(_:)routes tofailHandshake+ critical banner.App/Boot/BootHealthProbes.swift— new switch arm for.loadFailedinWebviewBootHealthProbe.HandshakeTests+4;BridgeNavigationFailureTests(new App-level file) — drives bothdidFailProvisionalNavigationanddidFailarms + the end-to-endhandleWebviewLoadFailurepath.Issue #41 (
fix(hud): fan HudState emit to MenuBarIconController…)App/AppDelegate.swift— emit closure ininstallBus()now fans to bothbridge.send(.hudState(...))andmenuBarController?.transition(to: appState).MenuBarIconStateTransitionTests(new) — drives the live coordinator viadormantAgentContinuation, asserts icon state advances through.thinking → .speaking → .idle.Invariants preserved
scripts/check-single-writer-hudstate.sh— GREEN.transition(to:)writes the icon'scurrentState, notBusOutbound.hudState; the single-writer invariant only covers the.hudState(...)constructor surface.scripts/check-no-evaluate-javascript.sh— GREEN. No new JS surfaces.scripts/check-no-modal-presentation.sh— GREEN. Hard-block alert continues to route through allowlistedTCCAlertService.presentHardBlock.Test plan
swift test --package-path packages/Bus— 62 tests pass (+4 newfailHandshakecases).bash scripts/check-app-builds.sh— App target compiles cleanly.webview/index.html→index.html.bakand launch — verify a critical non-dismissible banner appears within ~100 ms (not after 2 s) instead of a silent black HUD.Notes for review
TCCAlertService.presentHardBlock) and the HUDBannerCoordinator banner BOTH fire on load failure — defense-in-depth. The alert is the existing path that already terminates on handshake-mismatch / timeout; the banner survives even if the alert is silenced (e.g., AppDelegate test seam with a no-opalertPresenter).BridgeNavigationDelegateis nowinternal(notprivate) so tests inApp/Tests/AppTests/can drive its protocol methods directly via@testable import Jarvis. No other production code references it.🤖 Generated with Claude Code