Commit Graph
28 Commits
Author SHA1 Message Date
james.maes 795c3bf5c9 feat(app): v0.1 UX surfaces — startup panels + InputMonitoring banner + PTT (#87, #88, #89, #156)
#89: new AppDelegate.startupSurfacesMode enum (.minimal/.diagnostic). Debug defaults .diagnostic; Release defaults .minimal. openStartupSurfacesIfDiagnostic() runs at tail of bootHealthTask (skipped under XCTest or when wizard is open). Status panel top-right + Dev Overlay bottom-right of NSScreen.main.visibleFrame. DevOverlay opens to Logs tab.

#88: BannerContent.inputMonitoringDenied is now nonDismissible: true. New HUDBannerCoordinator.dismiss(id:) clears a banner programmatically without writing dismissedThisLaunch. AppDelegate.reprobeInputMonitoring fires on NSApplication.didBecomeActiveNotification; rebinds the global hotkey on re-grant. Uses IOHIDCheckAccess (query-only, no prompt) per CLAUDE.md guidance. 3 new coordinator tests.

#87: WizardState.pttHotkey persisted under Jarvis.pttHotkey (independent of summon hotkey). Ships unbound per CLAUDE.md hotkey hygiene. AppDelegate.rebindPTTFromState() called from installVoice, foreground TCC re-grant, Settings Save/Clear. SettingsView extended with PTT hotkey section. Audio-graph isolation: NSEvent monitors only, no audio-graph surgery — safe alongside fix/voice-crit-v0.1. 3 new WizardState tests.

DevOverlay 14 + Voice 26 tests pass; all 17 grep boundary gates green (incl. single-writer-hudstate + no-modal-presentation).
2026-05-13 06:32:16 -05:00
james.maes bc74494ada fix(mcp): v0.1 CRIT cluster — turnID resolver wired + tool-result double-cap (#20, #21, #157)
#21 (CRIT): ToolResultPacker.pack(_:) is now a pass-through decoder; redundant inner 8 KB cap removed. MCP dispatcher's SanitizeForModel.prepareForBoundary(_, capBytes: 8192) is the sole cap site. Doc-comment rewritten honestly: replay row reflects boundary-prepared bytes (8 KB), not original helper output. Replay fidelity beyond 8 KB lifted to v0.5/M-3 once SanitizeForModel can move to a shared module without dep cycle.

#20 (CRIT): ReplayingToolResultObserver.turnIDResolver lock-protected via OSAllocatedUnfairLock<ResolverBox> (same Swift-6-async-safe pattern as InProcessConfirmationCache). New attach(turnIDResolver:) method. AgentOrchestrator.currentTurnID() actor-isolated accessor. installAgent attaches the resolver immediately after orchestrator assignment, with defensive systemLogger.warning if observer type mismatches.

Tests: ToolResultPackerTests TP2/TP5 updated to assert no [TRUNCATED] marker; OrchestratorSubmitTests OS6 reframed + new OS6b reads SQLite events table directly to assert 8 KB exact; MCPRuntimeWiringTests adds attach pre/post-attach + replay-log dual-write correlation.

AgentCore 243 + Memory 98 + Replay 34 + MCP all green; all 18 boundary gates green.
2026-05-13 06:31:46 -05:00
james.maes 4b1dd6404b fix(voice): v0.1 CRIT cluster — wake / STT / TTS / boot probes (#28, #29, #30, #31, #32, #33, #34, #36, #158)
#28 (CRIT): setCancelInFlight calls stopFeed() (preserves stream); rebuild consumer re-arms DAG against new ring. Wake-word no longer dies on audio-graph rebuild.

#29 (CRIT): sttFactory reads PerTurnSnapshot.stt.whisperKitFallback; get_self_state returns canonical snake_case backend constants. Factory signature: () -> any STTProvider → () async -> any STTProvider (existing test sites compile unchanged under Swift 6 implicit-async-closure rules).

#30 (CRIT): TTSEngineActor.synthesize emits .ttsStopped after .finished on natural completion (two-emit-site model: barge-in + natural-completion).

#31 (CRIT): VoiceTTSAdapter.tierResolver wired to read snap.tts.tier from configStore.

#32 (HIGH): Rolling-window trim in OpenWakeWordSession.runORT — mel ≤ 84, embeddings ≤ 16, nextEmbStart slides on drop.

#33 (HIGH): three new boot probes — WakeWordBootHealthProbe, STTBootHealthProbe, TTSBootHealthProbe. New WakeWordDAG.isFeedArmed + VoiceTTSAdapter.hasEngine accessors.

#34 (HIGH): vad.reset() at top of startSTTSession clears Silero VAD LSTM hidden state + wasSpeech between sessions.

#36 (HIGH): new Broadcaster.Priority.agentHud (protects stateChange/turnEnd/error); installAgent adds subscriber pumping AgentHudIntent into dormantAgentContinuation.

Voice 90 + AgentCore 242 tests pass; all 18 boundary gates green.
2026-05-13 06:31:17 -05:00
james.maes a6f91e6b73 fix(bus,hud): v0.1 S1 cluster — webview didFail + menu-bar icon transition (#40, #41, #151)
#40: BridgeNavigationDelegate gains onDidFail closure (covers didFailProvisionalNavigation + didFail). WebviewBridge.failHandshake(reason:) transitions to new terminal .loadFailed state, cancels pending 2s timeout, fires existing alertPresenter, idempotent. AppDelegate.handleWebviewLoadFailure dual-surfaces: failHandshake + critical non-dismissible banner (defense-in-depth across test seams). WebviewBootHealthProbe gets .loadFailed case.

#41: HudStateCoordinator emit closure now ALSO calls menuBarController.transition(to:) in addition to bridge.send(.hudState(...)). Doc-comment correctly notes this is not a HUD-08 single-writer violation — transition writes currentState, not BusOutbound.hudState. scripts/check-single-writer-hudstate.sh still green.

Tests: 4 new Handshake cases (load-failure-from-sentHello with alert-not-duplicated, from-idle, idempotency, post-armed no-op), BridgeNavigationFailureTests end-to-end + per-arm, MenuBarIconStateTransitionTests drives dormantAgentContinuation and asserts .thinking → .speaking → .idle. 62/62 Bus pass; all 18 boundary gates green.
2026-05-13 06:29:34 -05:00
james.maesandClaude Opus 4.7 a9531e65e3 feat(round-4): menu-bar icon turns red when subsystems are degraded
Slice 4 of Round 4 — the visible polish. With Slices 1–3 the agent
preamble + non-dismissible banner cover the "agent must know +
user must see" loop, but a closed HUD with no banner visible can
still hide the warning. This slice paints the menu-bar icon red so
the warning is glanceable from anywhere on the screen.

Option A from the slice plan (single-writer-safe):

- `MenuBarIconController.applyHealth(_ health: OverallHealth)` paints
  `button.contentTintColor = NSColor.systemRed` for `.loud` and
  `.critical`, clears the tint for `.ok` and `.soft`. Template images
  recolor automatically with `contentTintColor` so no second icon
  asset is needed.
- `currentHealth: OverallHealth?` exposed read-only for tests.
- `AppDelegate.runBootHealth` calls `menuBarController?.applyHealth(
  snapshot.overallHealth)` after computing the degradation summary,
  so the icon updates on every probe sweep (cold boot + Status panel
  re-probe).

Single-writer invariant for HudState (HUD-08) is intact:
`contentTintColor` lives on `NSStatusItemButton`, not in HudState.
`scripts/check-single-writer-hudstate.sh` is green.

1 new test in MenuBarIconControllerTests:
  * test_applyHealthTintsRedForLoudAndCritical — exercises all four
    OverallHealth cases and asserts contentTintColor toggles correctly.

Boundary gates 18/18 green; App target compiles cleanly.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-11 15:59:48 -05:00
james.maesandClaude Opus 4.7 79955b67b7 feat(round-4): non-dismissible banners for critical subsystem failures
Slice 2 of Round 4. With Slice 1 the probe layer now reports severity;
this slice routes severity to the banner system so the user CANNOT
silently dismiss a "your memory is dead, Jarvis will lie to you" warning.

- `BannerContent.nonDismissible: Bool` (default false).
- `HUDBanner` hides the dismiss X when `content.nonDismissible == true`.
- `HUDBannerCoordinator.dismissCurrent()` no-ops on non-dismissible
  banners; the banner stays on screen until programmatically cleared
  (`clear()` on app quit, or a re-probe finding the subsystem healthy
  enqueues a different banner that pre-empts it).
- Two new presets:
  * `bootHealthCritical(subsystem:reason:)` — priority 1, non-dismissible.
    Title format: "Memory: critical failure". Body promises that the
    agent will be warned not to rely on this subsystem (Slice 3).
  * `bootHealthLoud(subsystem:reason:)` — priority 3, dismissible.
- Existing `bootHealthFailed(...)` preset kept as backwards-compat
  fallback (no current call sites after this slice).
- `AppDelegate.enqueueFailedBootHealthBanners` rewritten to iterate
  every non-ok subsystem and route by severity:
    .critical → bootHealthCritical + `[system] CRITICAL` log line
    .loud     → bootHealthLoud
    .soft     → no banner (Status panel only)
  Previously only `.failed` cases enqueued banners, which silently
  dropped `.degraded` (the Toby case) entirely.

Two new tests in HUDBannerCoordinatorTests cover the new behavior:
  * test_nonDismissibleBannerIgnoresDismiss
  * test_criticalBannerPreemptsLowerPriorityCurrent
(Note: `xcodebuild test -only-testing JarvisAppTests` is pre-existing-
broken on this checkout due to onnxruntime static-link duplication —
confirmed by `git stash` before this slice. The test code compiles
cleanly via the regular App build; SPM-target tests for the
AgentCore-side severity model are green.)

Boundary gates 18/18 green. `swift test --package-path packages/AgentCore
--filter BootHealthOrchestratorTests` → 13/13.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-11 15:36:43 -05:00
james.maesandClaude Opus 4.7 f9cd25d66e chore(test-naming): converge on Mock/Fake/Stub convention; document in CLAUDE.md; rename worst-mismatch stragglers
Per audit-2026-05-04 P2-15: 26 Mock*, 21 Fake*, 12 Stub* across the test
corpus, often used interchangeably for the same role. New CLAUDE.md
section "Test naming conventions" (line 127) sets the rule:

- Mock*: records calls AND returns scripted responses (assertion driver).
- Stub*: returns canned data without recording (deterministic input).
- Fake*: alternative implementation with realistic behavior (in-memory
  state-holder).

Renamed 15 worst-mismatch stragglers in the older corpus (per audit
constraint, did NOT touch types added in today's P1+P2 fix commits):

Stub* → Mock* (records + scripts):
- HybridSearchTests:    StubEmbedder → MockEmbedder
- HybridSearchTests:    StubStore → MockStore
- SessionHistoryTests:  StubStore → MockStore
- InProcessMemoryToolsTests: StubHybrid → MockHybrid
- InProcessMemoryToolsTests: StubHistory → MockHistory
- InProcessMemoryToolsTests: StubForget → MockForget

Mock* → Stub* (canned data, no recording):
- STTBackendSwitchTests: MockSpeechAnalyzerBridge → StubSpeechAnalyzerBridge
- STTBackendSwitchTests: MockWhisperKitBridge → StubWhisperKitBridge
- VoiceWiringTests:      MockVADEngineForW4 → StubVADEngineForW4
- VoiceWiringTests:      MockSTTProviderForW4 → StubSTTProviderForW4
- VoiceWiringTests:      MockTTSForW4 → StubTTSForW4
- VoiceWiringTests:      MockOrchestratorForW4 → StubOrchestratorForW4
- VoiceWiringTests:      MockBannerForW4 → StubBannerForW4
- VoiceWiringTests:      MockBusForW4 → StubBusForW4

Fake* → Mock* (records + scripts):
- WebviewBridgeOutboundTests:        FakeJSEvaluator → MockJSEvaluator
- HudStateCoordinatorBusWiringTests: FakeJSEvaluator → MockJSEvaluator
  (the comment on the type already said "Records each call for assertion")

Skipped per audit constraint:
- StubCapture/StubSink (Vision FrameAttachConfirmSendWiringTests, P1+P2)
- MockVADEngine + MockOrchestratorForController siblings (Voice
  VoiceControllerTests/VADGatedSessionTests, today's track-B churn)

Tests pass: Voice 87+23, MCP 85, Memory 88, Bus 58, AgentCore 19+72,
Vision 19, Shell 7+5. App target builds clean.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-04 16:42:16 -05:00
james.maesandClaude Opus 4.7 81357ca900 test(voice p1-2): production chunkPump factory extracted + integration test against real broadcaster
Per audit 2026-05-04 T2: the production chunkPump closure at AppDelegate
.installVoice was tested only end-to-end via synthetic pumps — the real
subscribe + readMono16k loop had zero coverage. Same divergence pattern
that produced BLOCKER-INT-1 (NoopBusGateway).

Extract the closure into App/Voice/ProductionChunkPump.swift as
makeProductionChunkPump(graphOwner:); AppDelegate becomes a one-line
caller. New ProductionChunkPumpTests covers three integration paths:

- PCT1: pump finishes immediately when graphOwner is nil
- PCT2: pump receives buffers via the broadcaster fan-out (publishes
        through a tap-block-capturing GraphBuilder against a real
        AudioGraphOwner)
- PCT3: pump's subscription does not steal samples from a sibling
        subscription (P1-1 / Track B-7 invariant regression guard)

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-04 15:46:32 -05:00
james.maesandClaude Opus 4.7 707c45b355 feat(track-d 2): register SearchMemoryTool + ForgetFactTool with MCP runtime
Closes the audit's "tools exist but unregistered" finding from
.planning/audit-2026-05-03/memory-audit.md §4-5: SearchMemoryTool +
ForgetFactTool source files have lived in `packages/MCP/Sources/MCP/
InProcess/` since Plan 07-03 with zero non-test instantiation. The agent
loop has had no way to call into memory.

Wiring:
  - New `App/MCP/InProcessMemoryAdapters.swift` bridges MCP's tool
    dispatch protocols to Memory's actors:
      * `HybridSearchAdapter: HybridSearchDispatching` wraps
        `Memory.HybridSearch` and translates `[FactRef]` → `[SearchMemoryHit]`
        (so MCP doesn't need to import Memory).
      * `ForgetFactStoreAdapter: ForgetFactDispatching` wraps
        `MemoryStore.forgetFact`.
  - `installMemory` builds an `InProcessToolRegistry` and registers tools
    based on preconditions:
      * `forget_fact` registers when `memoryStore != nil` (writes need DB)
      * `search_memory` registers when `memorySearchAvailable && embedder
        init succeeds` (reads need DB + vec0 + Ollama loopback)
  - Embedder construction (`OllamaEmbeddingClient` loopback assertion)
    failure is caught + logged; degrades to "writes ok, no search"
    rather than failing the whole subsystem.
  - New `inProcessToolRegistry: InProcessToolRegistry?` ivar, exposed
    @testable.

Test seam: `AppDelegate.buildInProcessToolRegistry(forgetDispatcher:
searchDispatcher:)` is the pure registry-construction helper — takes
already-built dispatchers (or nil) and applies the gating rules. Tests
inject stub dispatchers to assert positive registration without needing
a real `MemoryStore` (which requires vec0.dylib, env-gated in CI).

Tests added (compile-time gates per
.planning/debug/xctest-launch-runningboard-error-5.md):
  - `test_D2_noToolsRegisteredWhenStoreUnavailable` — production reality
    on every cold launch (vec0 placeholder)
  - `test_D2_bothToolsRegisterWhenBothDispatchersPresent` — store + search
    available
  - `test_D2_onlyForgetRegistersWhenSearchUnavailable` — store ok, vec
    failed (the future state once custom libsqlite3 lands but vec0.dylib
    is missing)
  - `test_D2_emptyRegistryWhenBothDispatchersNil` — degenerate case

Out of scope: exposing the registered tools to the LLM via
`AgentOrchestrator.availableTools` (currently `[]` — separate audit gap,
affects helper tools too). The "tools exist but unregistered" gap is
closed; the "tools exposed to LLM" gap is independent.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-04 14:09:46 -05:00
james.maesandClaude Opus 4.7 75a10be480 fix(track-d 1): installMemory no longer short-circuits on vec failure
Pre-fix: a single `return` after `MemoryStore(databaseURL:)` threw left
`memoryExtractionOrchestrator` AND `memoryExtractionCoordinator` both nil
for the lifetime of the process — so the .turnEnd subscriber was a no-op
and nothing about memory extraction was observable in system log even
though the LLM call to Ollama is independent of the SQLite store.

Post-fix: store nil → log warning, set memorySearchAvailable=false, but
extractor + orchestrator + coordinator all construct. The applyOp closure
captures `[weak self]` and routes to a debug log when self.memoryStore is
nil — drain loop survives, log only the turnId hash (T-06-05-03 / memory-
content discipline: never log subject/object content).

`memorySearchAvailable` is added as an ivar to track whether search is
viable (today: store!=nil ⟺ vec_version() succeeded ⟺ search works; when
custom libsqlite3 lands, the flag will split from store!=nil).

`installMemory` lowered from `private` to default `internal` so the new
JarvisAppTests/MemoryInstallWiringTests can drive it directly via
`@testable import Jarvis`. Tests run as compile-time gates (xctest harness
launch is broken on Xcode 26 + ad-hoc bundles per
.planning/debug/xctest-launch-runningboard-error-5.md), but the structural
assertions still catch regressions of the form "the cascade came back".

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-04 14:00:34 -05:00
james.maesandClaude Opus 4.7 08125a46d8 chore(test): remove tautologies and rubber-stamps per tests-audit.md
Per `.planning/audit-2026-05-03/tests-audit.md` section 5, five
`XCTAssertTrue(true)` placeholder tests were identified across the tree
that are mathematically incapable of failing. This commit fixes or
removes each, conservatively. Files touched and disposition:

- App/Tests/AppTests/MCPRuntimeWiringTests.swift:260
  fixed: replaced trailing `XCTAssertTrue(true)` rubber-stamp at end of
  CR-02 channel-drain test with a load-bearing assertion that the drain
  task completed via `finish()`, not cancellation. The implicit `await
  drainTask.value` is the real gate; the new assertion ensures regressions
  that cancel-instead-of-finish surface.

- App/Tests/AppTests/WebviewBundleLoadTests.swift:60
  deleted: `test_assetsDirectoryGitignored_documentedInSmokeScript`. Body
  was `XCTAssertTrue(true)` with comment "intentionally has no assertion
  — it exists to document the split of responsibility". Documentation,
  not a test. Replaced with a top-level comment block documenting the
  same split (smoke-test-hud.sh is the runtime gate).

- packages/AgentCore/Tests/AgentCoreTests/ToolChoiceTests.swift:38
  fixed: removed trailing `XCTAssertTrue(true) // Test compiles → contract
  holds.` from `testLLMProviderStreamRequiresToolChoice`. The closure
  body itself is the contract — if the test compiles, the contract holds.
  Comment updated to make the compile-time semantics explicit.

- packages/Shell/Tests/ShellTests/InputMonitoringDenialTests.swift:36
  deleted: `test_localMonitorOnlyFallback`. Body was `XCTAssertTrue(true)`
  documenting that the real fallback test lives in HotkeyBindingTests.
  Replaced with a top-level comment pointing readers there.

- packages/Vision/Tests/VisionTests/PackageBoundaryTests.swift:14
  deleted: `testVisionModuleCompilesStandalone`. Body was
  `XCTAssertTrue(true)` with comment "compile is the assertion." VISION-03
  isolation is enforced by `scripts/check-vision-isolation.sh` and the
  Package.swift dependency graph, both of which are real gates. Folded
  the documentation into the `testPublicAPISurface` doc-comment so the
  explanation isn't lost.

HudStateEnumTests rubber-stamps (audit items #4–6 in section 5) were
left as-is. The voiceOverLabel literals are user-facing accessibility
text that VoiceOver actually speaks; they are load-bearing in the sense
the audit calls out as worth preserving ("replace with behavioral
assertions if the literal under test is load-bearing"). A regression
that breaks an a11y label IS a real regression. Conservative read; not
worth the blast radius of guessing wrong.

Pre-existing baseline issues unrelated to this batch:
- `packages/Shell/Tests/ShellTests/InputMonitoringDenialTests.swift`
  fails to COMPILE on a clean tree (MockProbe missing
  `isListenEventAccessGranted()`). Verified via `git stash` that the
  failure is independent of this commit. Not addressed here per the
  scope rules — flagged for follow-up.

Net test count: -3 tests (3 deleted, 0 added — the
MCPRuntimeWiringTests assertion replaces the tautology in-place).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-04 12:34:56 -05:00
james.maesandClaude Opus 4.7 ad93dac665 fix(blocker-int-1): replace NoopBusGateway with real MCPBusGatewayAdapter
ConfirmingToolDispatcher emits tool-call events through `BusGateway`. The
prior `NoopBusGateway` (AppDelegate.swift:503 + dead struct at :1894-1904)
silently dropped every emission, so the HUD's chat-panel ToolCallCard
never rendered a Running / Completed transition for any tool dispatch.
This is the canonical "test mock and prod stub diverge in the same
direction" pattern called out in `.planning/audit-2026-05-03/tests-audit.md`
§6: SpyBus in MCPRuntimeWiringTests counted invocations; production
shipped with empty bodies — both satisfied the protocol; only the latter
was load-bearing in production.

The replacement `MCPBusGatewayAdapter` (App/MCPBusGatewayAdapter.swift):
- Conforms to `BusGateway`, forwards to `OutboundBatcher.flushAndSend`
  with `.toolCallStart` / `.toolCallEnd` envelopes.
- `updateArgsPreview` re-emits `.toolCallStart` with the same id; the
  HUD's `upsertToolCall` (webview/packages/hud/src/bus/client.ts) patches
  the existing card by id, transitioning awaiting-approval → running with
  the post-approval sanitized args.
- `String → UUID` mapping via SHA256-derived RFC 4122 v5-shape UUID, so
  `emitToolCallStart(id="toolu_01X")` and `emitToolCallEnd(id="toolu_01X")`
  produce IDENTICAL UUIDs for HUD reconciliation across start/update/end.
  The dispatcher protocol uses `String` (CR-03 in ConfirmingToolDispatcher.swift
  — Anthropic toolu_01ABCD… / Ollama free-form, never UUID); the bus
  protocol uses `UUID` (Phase 2 closed schema). Bridge is one-way; the
  audit-trail correlation by String id already lives at
  ReplayingToolResultObserver.
- Late binding via `@Sendable () async -> OutboundBatcher?` resolver:
  the batcher is built in installAgent (step 13), AFTER mcpInstallTask
  captures the gateway in step 10. The closure resolves it at emit-time;
  if the batcher is nil (early window before installAgent finishes,
  or shutdown), emissions drop silently — the replay log path still
  records the call so audit isn't lost.

Tests: App/Tests/AppTests/MCPBusGatewayAdapterTests.swift (+5):
- stableUUID determinism (same input → same UUID)
- stableUUID distinguishes different ids
- emitToolCallStart reaches the sink with right name/argsPreview/id
- emitToolCallEnd id matches the start id (reconciliation invariant)
- updateArgsPreview re-emits .toolCallStart with same id, updated args
- nil batcher drops events silently (no trap)

The recording sink + OutboundBatcher path the test exercises is the
same path production uses end-to-end through to WebviewBridge.send.
The remaining seam (sink → JS-side store) is already covered by
RealWKWebViewIntegrationTests + webview/hud streaming.test.tsx D5/D6/D7.

App build clean. Bus tests 58/58 (was 57/58 before F-A2-01 fix).
MCP tests 85/85 unchanged. All 5 boundary gates green.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-04 11:47:47 -05:00
james.maesandClaude Opus 4.7 e9c0a34298 fix(track-b 4): wire AudioGraphOwner + LiveSpeechAnalyzerBridge SpeechAnalyzer API
Track B-4 of the 2026-05-03 voice audit fix plan
(.planning/audit-2026-05-03/voice.md). Two production paths were dead:

- installVoice constructed WakeWordDAG but never built the AudioGraphOwner
  that owns the AVAudioEngine + ring buffer it feeds from. Mic taps were
  installed inside AudioGraph.init, but no AudioGraph was ever created in
  production, so the wake-word detached feed Task spun on an empty ring
  for the entire process lifetime.

- LiveSpeechAnalyzerBridge.feed(_:) was `_ = chunk` — a structural shell
  left over from Plan 06-03 that was meant to be wired in 06-05 but
  never was.

Changes:

* AudioGraphOwner: add `setCancelInFlight` / `setReleaseORTSessions`
  cross-actor setters so MainActor install code can install the
  cancel-in-flight closure under Swift 6 mode.

* PCMBufferBuilder: new pure-AVFoundation translator extracted from the
  bridge (`AudioChunk.pcm16k` → `AVAudioPCMBuffer` + sample-rate convert
  via `AVAudioConverter`). Lives outside the macOS-26-gated bridge so
  unit tests run on every supported macOS.

* LiveSpeechAnalyzerBridge: replace the no-op feed with real wiring —
  `start()` builds `SpeechTranscriber` + `SpeechAnalyzer`, queries
  `SpeechAnalyzer.bestAvailableAudioFormat`, opens the
  `AsyncStream<AnalyzerInput>` input pipe, and spawns a result-drain
  Task. `feed()` packs the chunk into a PCM buffer (with optional
  format convert) and yields `AnalyzerInput(buffer:)`. `finish()`
  finalises through end-of-input and awaits the drain. Eager partials
  stream in init removes the ordering race against the SpeechAnalyzerSTT
  feed/drain Task pair.

* AppDelegate: add strong properties `audioGraphOwner`,
  `audioGraphDegradationTask`, `audioGraphRebuildTask`. installVoice
  now constructs AudioGraphOwner with degradation/rebuild stream
  continuations, spawns degradation + rebuild consumers BEFORE
  `open()` (so the retry-with-aec=false path's `.aecUnavailable` is
  observed), opens the graph fail-soft, hands the ring buffer to
  `WakeWordDAG.start(ring:)`, and sets `cancelInFlight` to cancel the
  DAG before teardown. Failure to open is non-fatal: voice degrades
  to dormant + banner, app keeps running.

* VoiceWiringTests testW1: assert the three new strong properties
  exist and start nil.

* PCMBufferBuilderTests (new, +7): frame count, payload fidelity,
  empty input, same-format passthrough, 16k→24k upsample ratio,
  empty-input convert.

Verification:

* swift test packages/Voice — 77 tests, +7 PCMBufferBuilderTests, 3
  skipped, 1 pre-existing failure (TTSInterruptTests.testI3, the
  10s timeout flake unrelated to this work; deterministic before
  and after).
* `bash scripts/check-app-builds.sh` — App target Debug build clean.
* Boundary gates: install-order, no-modal-presentation,
  no-null-voice-adapters, orchestrator-events-single-consumer,
  single-writer-hudstate — all PASS.

What's still TODO (Track B-5):
* End-to-end voice loop test: feed a known WAV at the audio graph,
  assert STT result lands on the orchestrator. The pipe is now wired
  but no integration test exercises it yet.
* VAD-gated chunk feeding from VoiceController to STT — the
  bridge accepts chunks now but VoiceController.startSTTSession()
  has no producer feeding `sttChunkCont`. Tracked in audit
  PLAN 06-03 anti-pattern callout.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-03 19:05:41 -05:00
james.maes 75a4ed6991 feat(09-04): replace NullVoiceAdapters with real voice adapter triad + lock install order (D-09, D-10, BLOCKER-1, BLOCKER-2, WARNING-5)
- New App/Voice/{VoiceOrchestratorAdapter,VoiceTTSAdapter,VoiceBusEmitterAdapter,RejectReasonCopy}.swift
  replaces NullOrchestratorAdapter / NullTTSAdapter / NullBusEmitterAdapter (D-09).
- VoiceOrchestratorAdapter forwards submit/cancelAndSubmit to AgentOrchestrator,
  appends user-side text to TurnTranscriptStore (BLOCKER-1), enqueues a HUDBanner
  on every SubmitOutcome.rejected case via shared RejectReasonCopy (D-10).
- VoiceTTSAdapter wraps an optional TTSEngineActor (engine construction lands
  in a follow-on plan; nil-engine path no-ops gracefully).
- VoiceBusEmitterAdapter wraps an OutboundBatcher whose sink is the WebviewBridge
  so audio-level RMS reaches the HUD's RingMesh pulse.
- AppDelegate.installVoice now uses the production adapters; install order
  in applicationWillFinishLaunching reordered to vision -> agent -> voice
  with voiceInstallTask awaiting agentInstallTask?.value (WARNING-5).
- New broadcaster .voice subscriber in installAgent: per-turn assistantText
  accumulator + turnSourceWasVoice filter so text-originated turns NEVER drive
  emitTurnEnded on the voice adapter (BLOCKER-2).
- SubmitOutcome.superseded extended with newTurnId so cancelAndSubmit callers
  can append user text under the new turn id (BLOCKER-1 user-side closure on
  the cancel path).
- VoiceSubscriberTextTurnFilterTests proves text turns never trigger
  emitTurnEnded; VoiceOrchestratorAdapterTests covers RejectReasonCopy +
  banner id mapping + VoiceBusEmitterAdapter forwarding.
- New scripts/check-install-order.sh + scripts/check-no-null-voice-adapters.sh
  catch regressions structurally.
2026-05-01 20:39:19 -05:00
james.maes da42c079fd feat(06-05): AppDelegate production wiring + VoiceWiringTests (W1-W4)
- AppDelegate: import Voice, add voiceController/pushToTalk/muteWakeWord properties
- AppDelegate: installVoice() async method wires VoiceController to dormantVoiceContinuation
- AppDelegate: NullOrchestratorAdapter/NullTTSAdapter/NullBusEmitterAdapter (Phase 7 stubs)
- AppDelegate: AppDelegateBannerAdapter bridges VoiceBannerInterface→HUDBannerCoordinator
- MenuBarIconController: contextMenu is now public (for MuteWakeWord)
- VoiceWiringTests: W1-W4 type-level wiring verification
- NOTE: check-app-builds.sh fails on pnpm (pre-existing infra limit); Swift compiles clean
2026-04-27 13:56:34 -05:00
james.maesandClaude Opus 4.7 d95c4421fc fix(05-review): CR-02 wire ME-04 closure observer→channel→drain→ReplayLog
The orch→replay BoundedAsyncChannel<ReplayEvent> previously had no
producer (ReplayingToolResultObserver wrote directly to replayLog)
and the drain Task discarded everything it received. The channel +
drain were dead code; ME-04's contract was structurally unverifiable
in production.

Now:
- ReplayingToolResultObserver holds a BoundedAsyncChannel<ReplayEnvelope>
  (TurnID + ReplayEvent envelope so the consumer knows which turn to
  key against) and sends both pre- and post-sanitize bytes through it.
- AppDelegate opens a real ReplayLog at boot and the drain Task
  consumes envelopes off the channel and calls replayLog.record(...).
- AppDelegate calls MCPRuntimeWiring.build(...) (previously referenced
  only in a doc comment) so the dispatcher chain is actually
  instantiated. A NoopBusGateway placeholder fills the bus seam until
  Phase 6/7 orchestrator wiring lands.
- MCPRuntimeWiring.build's signature switches replayLog parameter to
  replayChannel — cleaner separation, since the channel is the
  load-bearing seam.

Adds three CR-02 regression tests in MCPRuntimeWiringTests:
observer→channel produces envelopes, observer→channel→drain→ReplayLog
end-to-end, and .dropOldest under burst saturation.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-25 16:19:58 -05:00
james.maesandClaude Opus 4.7 d07ac08f0e fix(05-review): CR-03+WR-04 String toolUseId and success-path toolCallEnd
CR-03: BusGateway's toolUseId parameter is now String (matching the bus
schema's Phase 2 closed contract) instead of UUID. Previously the
dispatcher synthesized `UUID(uuidString: toolUse.id) ?? UUID()`, which
silently produced a fresh random UUID for every Anthropic 'toolu_…' or
Ollama free-form id — breaking correlation across bus / replay /
orchestrator. The original String id now flows verbatim from
toolCallStart through argsPreviewUpdate to toolCallEnd. The broker's
internal UUID is now a fresh-per-call value and does NOT replace the
bus-facing id.

WR-04: ConfirmingToolDispatcher now emits toolCallEnd(ok: true, …) on
the success path so bus consumers (HUD ToolCallCard) transition
Running → Completed. On dispatch error post-approval, toolCallEnd
fires with ok: false carrying the error description before re-throwing.

Adds three new tests (busToolUseId verbatim across emissions, non-UUID
id flows through, success path emits 3 events). Updates SpyBus and
SpyBus-equivalent in MCPRuntimeWiringTests to use String toolUseId.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-25 15:54:34 -05:00
james.maes daae27e720 feat(05-05): App-level MCPRuntimeWiring + ME-04 channel + SEC-07 observer
Closes ME-04 (Plan 04-05 deferred state). AppDelegate.applicationWill
FinishLaunching now instantiates BoundedAsyncChannel<ReplayEvent>(
capacity: 2048, policy: .dropOldest) for the orch→replay seam — the
production instance that Plan 04-05's ChannelTopologyTests CT2 only
verified as a primitive.

- App/MCP/MCPRuntimeWiring.swift: dependency-graph constructor for the
  full chain (MCPClient → ToolRegistry → MCPToolDispatcher →
  ConfirmingToolDispatcher) plus a compose() test seam. The
  ConfirmationPresenterHolder breaks the broker↔presenter cyclic init.
- App/MCP/ReplayingToolResultObserver.swift: SEC-07 observer that
  records pre-/post-sanitize bytes via two ReplayEvent.toolResultFull
  rows (one canonical, one ':raw'-suffixed toolUseId). Uses an
  injected turnIDResolver closure since orchestrator wiring lands in a
  later plan.
- AppDelegate stores the orchToReplayChannel + drain Task and tears
  them down on applicationWillTerminate.
- App/Tests/AppTests/MCPRuntimeWiringTests.swift: 3 structural tests
  (compose-returns-ConfirmingToolDispatcher; channel-capacity-+-policy;
  MCPRuntime-struct-addressable). No xcodebuild test launch — known
  Xcode 26 RunningBoard blocker per .planning/debug/.

Deviations:
- Replay / AgentCore / MCP / Bus Package.swifts had implicit transitive
  swift-log deps that SPM tolerated but Xcode framework-linker rejects
  once the App pulls them in as separate frameworks. Added explicit
  swift-log deps to all four (Rule 3, blocking).
- HudStateCoordinatorBusWiringTests qualified two HudState references
  to Jarvis.HudState — the new framework deps disturbed the App vs Bus
  symbol disambiguation (Rule 1, surgical).
- The plan's plain ReplayEvent.toolResultFull(toolUseId:rawBytes:
  sanitizedBytes:) variant doesn't exist (Phase 4 schema closed); the
  observer writes two rows with a ':raw' toolUseId suffix instead.

xcodebuild build (Debug) succeeds with the new MCP deps wired into the
App target. Test build (build-for-testing) also succeeds; runtime test
execution is the documented Xcode 26 RunningBoard blocker per
03-05-PLAN.
2026-04-25 14:57:01 -05:00
james.maesandClaude Opus 4.7 cabc27299a feat(03-05): AppDelegate wires HudStateCoordinator + bridge; lint activated; W1-W6 + B1-B3 tests
- App/HUD/HudStateBridge.swift (new): @MainActor public func busHudState(
  from: HudState) -> Bus.HudState. RawValue round-trip with assertionFailure
  on drift, .idle fallback in Release. Option (b) from the plan — avoids
  name-collision between App.HudState and Bus.HudState via module-qualified
  Bus.HudState in the helper's scope.
- App/AppDelegate.swift:
    * New stored properties: hudStateCoordinator, dormantAgentContinuation,
      dormantVoiceContinuation, dormantConfirmContinuation (all internal so
      @testable import Jarvis can assert W5 retention), webviewEntryFilename
      (internal test seam for W6).
    * installBus(): constructs HudStateCoordinator with emit closure that
      bridges App.HudState -> Bus.HudState via busHudState() and dispatches
      to bridge.send(.hudState(...)) on the MainActor. Creates three dormant
      AsyncStream producers (Phase 4/5/6 replace them) and retains their
      continuations on self so coordinator.{agent,voice,confirm}Task stay
      live. markReady() fires from onHandshakeArmed so the HUD promotes
      .booting -> .idle as soon as the webview is responsive.
    * Load target: bus-harness.html -> index.html (via
      webviewEntryFilename property so W6 can assert post-install value).
      XCTest-host short-circuit from 02-03 deviation #5 preserved, updated
      to reference the property-driven filename.
- scripts/check-single-writer-hudstate.sh: allowlist extended to
  {HudStateCoordinator.swift, HudStateBridge.swift, AppDelegate.swift} +
  {BusOutbound.swift, Protocol.swift}. Rationale comment updated: Phase 4+
  drives the coordinator via its three input AsyncStreams, not new
  .hudState(...) call sites. Already wired as a preBuildScript by Task 1.
- App/Tests/AppTests/HudStateCoordinatorBusWiringTests.swift (new):
    * W1 test_coordinatorConstructedInInstallBus — delegate.hudStateCoordinator
      non-nil after applicationWillFinishLaunching.
    * W2 test_emitClosureBridgesToBusSend — FakeJSEvaluator observes the
      closure producing a decoded BusOutbound.hudState(.thinking) after
      coordinator.markReady() + agent.yield(.thinking). Proves the exact
      closure shape AppDelegate installs routes rawValues end-to-end.
    * W3 test_markReadyCalledOnHandshakeArmed — coordinator pinned at .booting
      pre-handshake, promotes to .idle after bridge.handleHelloAck.
    * W4 test_appHudStateRawValueMatchesBusHudState — table-driven bidirectional
      rawValue parity over HudState.allCases and Bus.HudState.allCases. Catches
      drift before busHudState()'s assertion fires at runtime.
    * W5 test_dormantStreamsHeldByDelegate — all three continuations non-nil.
    * W6 test_installBusLoadsIndexNotHarness — webviewEntryFilename == "index"
      post-installBus.
- App/Tests/AppTests/WebviewBundleLoadTests.swift (new):
    * B1 test_indexHtmlInRepoWebviewDir — structural (#filePath traversal to
      repo root), catches missing committed index.html.
    * B3 test_indexHtmlReferencesRelativeAssets — content-grep for "./assets/"
      present + "src=\"/assets/" absent. Catches Vite misconfig or absolute-
      path drift.
    * B-extra test_assetsDirectoryGitignored_documentedInSmokeScript — doc-only,
      asserts the split of responsibility between XCTest (committed index.html)
      and scripts/smoke-test-hud.sh (built-app assets/*.js).

Tests are repo-structural / evaluator-observation only; xcodebuild test
launch still blocked on Xcode 26 RunningBoard error 5 per 02-03 precedent.
Verification is xcodebuild build (succeeds) + scripts/smoke-test-hud.sh
(next task). This follows the 02-03 SUMMARY deviation #5 pattern.

xcodebuild build -configuration Debug: BUILD SUCCEEDED.
bash scripts/check-single-writer-hudstate.sh: PASS (allowlist active).
packages/Bus swift test: 47/47 green.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-24 15:53:58 -05:00
james.maes 79092263bb feat(03-01): HudStateCoordinator single-writer + intents + lint
Implements HUD-08 precedence ladder as the sole Swift-side writer of
HudState. Three producer streams (agent, voice, confirmation) feed a
MainActor-isolated resolver; one emit closure is the only path out.

- HudStateIntent.swift: AgentHudIntent/VoiceHudIntent/ConfirmHudIntent
  Sendable enums. SystemHudIntent intentionally omitted — markReady()
  is a one-shot direct call, not a stream.
- HudStateCoordinator.swift: @MainActor final class. Ladder per
  HUD-08 verbatim: awaitingConfirmation > speaking > listening >
  thinking > idle > booting > reconfiguring. Ready-gate tweak on the
  .booting step (RESEARCH Open Q #4 pre-authorized) so boot-time
  silent inputs don't prematurely resolve to .idle.
- Bus-agnostic by design: takes @escaping @MainActor (HudState) -> Void
  emit closure so Plan 03-05 can wire it to WebviewBridge without this
  plan importing Bus or WebKit.
- Weak-self capture + deinit cancel on three for-await loops prevents
  the T-03-03 leak.
- HudStateCoordinatorTests.swift: 19 methods covering every precedence
  pair, booting gate, idempotent emit, cleared-confirm restore,
  weak-self no-leak, interleaved producers.
- scripts/check-single-writer-hudstate.sh: executable; exits 0 on
  current codebase; exits 1 on planted .hudState() construction
  outside the allowlist. Pre-build wiring deferred to Plan 03-05.
2026-04-23 18:55:04 -05:00
james.maes ca17844883 feat(03-01): extend HudState enum 5→7 cases with voiceOverLabels
- Append .reconfiguring and .booting to App/Theme/HudState.swift;
  preserve Phase 1 declaration order + existing labels verbatim.
- Add voiceOverLabel arms 'Jarvis, reconfiguring audio' / 'Jarvis,
  starting up' per RESEARCH §HudState Enum Wire Format.
- MenuBarIconController: add forced exhaustive switch arm for the
  two new cases; both share .idle profile (no animation) with
  TODO for Phase 3/4 dedicated treatment per HUD-08 precedence.
- HudStateEnumTests: 6 methods covering case count, non-empty +
  unique voiceOverLabels, spec-verbatim labels, exhaustive switch.
2026-04-23 18:47:46 -05:00
james.maes 20e8647af4 feat(02-03): wire bus into AppDelegate + expose HUD webView + bundle harness
- JarvisHUDPanel.webView: private → public. Surgical one-line change per
  RESEARCH open question #4 — keeps the dep graph App → Bus, not inverted.
- AppDelegate.installBus(): new launch-chain step 4.5 (after installHUDPanel
  / installBannerPanel). Constructs WebviewBridge around panel.webView,
  wires alertPresenter → TCCAlertService.presentHardBlock + terminate,
  wires onHandshakeArmed → onBusArmed test hook, installs
  BridgeNavigationDelegate so WKNavigationDelegate.webView(_:didFinish:)
  triggers startHandshake, and loads bus-harness.html via
  Bundle.main.url(forResource:withExtension:subdirectory:). Missing
  harness is a hard-block (terminates) in production; silently skips
  under XCTest so unit tests assert bridge construction.
- AppDelegate.toggleHUD(): gates on handshakeState == .armed. Unarmed
  path enqueues BannerContent(id: "hud-not-ready", priority: 2) rather
  than popping an empty window.
- AppDelegate.onHandshakeMismatch + onBusArmed: @MainActor injectable
  closures for test hooks. Production default calls NSApp.terminate(nil)
  after the TCCAlertService modal returns.
- App/Resources/webview/bus-harness.html: bundle-vendored harness loaded
  from Jarvis.app/Contents/Resources/webview/bus-harness.html. Plan 04
  adds the parity check that keeps this in sync with webview/bus-harness.html.
- project.yml: adds Bus package dep, adds App/Resources/webview as a
  blue-folder reference (type: folder under sources:) so the subdirectory
  survives into the bundle. Excludes App/Resources/** from the flat
  source path to avoid double-including. xcodegen regenerates clean.
- AppDelegateBusWiringTests: 6 new test cases covering installBus
  construction, toggleHUD armed/unarmed gating, onHandshakeArmed hook
  firing, mismatch terminate hook, and Bundle.main harness lookup.

Verified:
- xcodegen generate + xcodebuild -scheme Jarvis build: BUILD SUCCEEDED.
- packages/Bus SPM tests: 47 passed / 0 failed.
- bus-harness.html + Bus_Bus.bundle/Injection.js both ship in the .app.
- grep -rn '.evaluateJavaScript(' App/ packages/Bus/Sources/: 0 matches.

Note: xcodebuild test -only-testing:JarvisAppTests fails at test-host
launch with dyld 'different Team IDs' on Xcode 26 + macOS 26 Debug — this
is a pre-existing repo-level issue documented in
.planning/debug/xctest-launch-runningboard-error-5.md and tracked in
01-HUMAN-UAT.md. Out of scope for Plan 02-03; SPM-level coverage is
complete via BatcherTests + WebviewBridgeOutboundTests.
2026-04-23 18:04:39 -05:00
james.maes b76ff75d81 fix(01-IN-07): replace tautology with real SEC-01 pasteboard assertion
Phase 1 REVIEW.md IN-07: `test_stateDumpDoesNotIncludeAPIKey` was an
`XCTAssertTrue(true)` tautology. It claimed to test SEC-01 /
T-04-02 but could not fail on a regression where `copyStateDump()`
accidentally started writing the raw API key to the pasteboard.

App/Tests/AppTests/AppDelegateWiringTests.swift
- Introduce `RecognizableKeychain` fake that returns a distinctive
  sentinel plaintext ("sk-ant-test-do-not-leak").
- Clear NSPasteboard before the test so prior-test contamination
  can't produce a false positive.
- Trigger `copyStateDump` via `perform(Selector("copyStateDump"))`.
- Assert pasteboard string:
    * does NOT contain the recognizable plaintext
    * does NOT contain any "sk-ant-" prefix
    * DOES contain "\"apiKeyStored\"" (sanity — exercised the path)

Now this test literally fails if copyStateDump regresses.
2026-04-23 16:24:56 -05:00
james.maes 9fc053511c fix(01-WR-05): cancellable 300ms banner drain, close race windows
Phase 1 REVIEW.md WR-05: the 300ms drain after `dismissCurrent()` was
a fire-and-forget `DispatchQueue.main.asyncAfter`. Two races fell out
of that:
  1. `clear()` during the drain window — pending drain still fires,
     banner pops up while app is quitting.
  2. Higher-priority banner enqueued during the drain — `current` is
     nil for 300ms, so `enqueue` calls `showBanner` immediately, then
     the drained banner fires afterwards and clobbers it.

App/HUD/HUDBannerCoordinator.swift
- Add `private var drainWorkItem: DispatchWorkItem?` — cancellable
  handle on the drain.
- `dismissCurrent()`: cancel any prior drain before scheduling a new
  one (belt + suspenders — prevents two drains from racing).
- `enqueue()`: when `current == nil`, cancel any pending drain before
  calling `showBanner`. Prevents the clobber race.
- `clear()`: cancel the pending drain. Prevents pop-up-at-quit.
- Drain closure re-checks `current != nil` at fire time for defense
  in depth.

App/Tests/AppTests/HUDBannerCoordinatorTests.swift
- `test_clearCancelsPendingDrain` — enqueue two, dismiss, clear,
  wait 500ms, assert currentBanner is nil.
- `test_enqueueDuringDrainCancelsDrain` — enqueue/enqueue/dismiss/
  enqueue-higher-priority, assert higher-priority wins immediately
  AND survives past the 300ms drain window.
2026-04-23 16:20:29 -05:00
james.maes 929e4cee6b fix(01-CR-02): shorten API key plaintext lifetime in wizard @State
Phase 1 REVIEW.md CR-02 (SEC-01): `WizardStageAPIKeyView` kept the raw
Anthropic API key in an `@State String` for the lifetime of the view —
outliving the Keychain write, retained by SwiftUI attribute-graph
diffing, and visible to any memory-dump forensic vector (crash report,
Xcode memory graph, `leaks(1)`).

App/Wizard/WizardStageAPIKeyView.swift:53-101
- `verify()` now captures the key into a single `capturedKey` local and
  reassigns `apiKey = ""` immediately, so the @State slot drops its
  strong reference before the async validate + keychain.set chain runs.
- Error branches selectively restore `apiKey = capturedKey` only where
  the user plausibly wants to retry without retyping (unauthorized /
  network / generic / keychain-error); `malformed` keeps apiKey empty.
- `.onDisappear { clearAPIKey() }` hook: zeros the @State on view
  teardown (including back-navigation and success-path dismissal),
  preventing the plaintext from persisting in the attribute graph after
  dismiss.
- `clearAPIKey()` first overwrites with null bytes of the same length
  (best-effort — Swift Strings are CoW/immutable so the backing page
  can't be zeroed in place), then drops the @State to empty so ARC can
  release the CoW storage.

App/Tests/AppTests/WizardStageAPIKeyViewTests.swift (new)
- Source-level grep gates for the three regression points: onDisappear
  hook, clearAPIKey helper shape, and verify()'s capture-then-clear
  pattern. SwiftUI @State lifecycle isn't observable from XCTest
  without ViewInspector, so text-level gates are the practical
  regression guard.

Jarvis.xcodeproj/project.pbxproj: regenerated via xcodegen to pick up
the new test file.
2026-04-23 16:09:59 -05:00
james.maesandClaude Opus 4.7 ebefb91043 feat(01-04): build first-launch wizard + full AppDelegate launch wiring
- AnthropicKeyValidator: local sk-ant- prefix check + URLSession-injectable
  GET to api.anthropic.com/v1/models; returns typed enum, never logs key
- WizardState: @MainActor ObservableObject; D-09 stateless re-entry via
  refresh(); firstUnresolvedStage advances apiKey → tcc → hotkey → complete
- WizardStageAPIKeyView: SecureField, validator + Keychain round-trip; key
  value lives only in Keychain (SEC-01)
- WizardStageTCCView: Input Monitoring row actively probes; Mic/Camera/
  Automation rows are explainer-only (D-08/SEC-04); no permission APIs
  called from the view
- WizardStageHotkeyView: ShortcutRecorderView + CollisionDetector wiring;
  Skip leaves hotkey unbound (SHELL-02)
- WizardView: three-stage root with header dots + Back footer
- OnboardingWizardController: NSWindow hosting; first-launch .modalPanel
  + close-button disabled until API key stored (D-06)
- AppDelegate: full 8-step launch chain — logging bootstrap (S-8) →
  entitlement gate (Plan 03) → config load (NSAlert on malformed, D-19/S-6) →
  menu bar + HUD + banner install → Keychain fetch (.keychainEmpty banner
  on itemNotFound) → Input Monitoring probe (.inputMonitoringDenied banner
  on denial, SHELL-06) → HotkeyBinder init → Wizard open on first launch.
  copyStateDump writes boolean-only presence; API key VALUE never clipboarded
- JarvisApp: Settings scene stub; real NSPanels managed by AppDelegate
- Tests: 11 new (WizardState 4, AnthropicKeyValidator 3, WizardTCC 1,
  AppDelegateWiring 4) + updated AppDelegateEntitlementTests for the new
  launch chain; all 25 JarvisAppTests pass
- Re-inserted PBXCopyFilesBuildPhase for Contents/Helpers per Plan 01/03
  precedent (xcodegen strips empty copyFiles)

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-22 14:07:04 -05:00
james.maes 122153d544 feat(01-03): add JarvisHUDPanel, HUDBannerPanel+Coordinator, AppDelegate entitlement hard-block
- JarvisHUDPanel: borderless 720×720 NSPanel, .statusBar level, .canJoinAllSpaces/.fullScreenAuxiliary/.transient collectionBehavior, transparent WKWebView (drawsBackground=false via KVO), cancelOperation(_:) + performClose(_:) overrides for Escape/⌘W dismiss, Reduce Transparency fallback to windowBackgroundColor@95%, fresh-center on summon via NSScreen.screenWithMouse()
- HUDBannerPanel: .floating non-activating 360pt banner panel, canBecomeKey/canBecomeMain both false (never steals focus), top-trailing positioning 24pt/8pt inset
- HUDBannerCoordinator (@MainActor): priority queue with pre-emption (lower priority int = higher precedence), dismissed-this-launch dedup by id, 300ms gap to next banner per UI-SPEC Surface 5
- BannerContent with four presets: keychain-empty (1) > input-monitoring-denied (2) > hotkey-bind-failed (3) > ollama-url-rejected (4); Sendable/Equatable/Identifiable
- HUDBanner (SwiftUI): 4pt arc-reactor-glow leading stripe + title/body/action/dismiss-× layout on .hudWindow visual-effect background with separatorColor border
- AppDelegate: EntitlementGateProbe protocol + InfoPlistEntitlementGateProbe impl + injectable onEntitlementFailure (@MainActor) defaulting to presentEntitlementFailureAlert + NSApp.terminate, skipped under XCTest host; bootstrap chain installs menu-bar + HUD panel + banner panel (Plan 04 prepends logging and appends config/keychain/hotkey)
- 9 new tests in JarvisAppTests: 3 HUD panel (style flags, summon/dismiss, Escape), 4 banner coordinator (enqueue, priority pre-emption, dismiss-drain, dismissed-this-launch dedup), 2 AppDelegate (hard-block triggers / does-not-trigger with mocked probe)
2026-04-22 13:26:40 -05:00
james.maes f8da4b7c39 feat(01-03): add MenuBarIconController + MenuBarContextMenu + JarvisAppTests target
- MenuBarIconController (@MainActor) owns NSStatusItem, loads Icon-MenuBar-Template asset with isTemplate=true, re-anchors layer.frame after anchorPoint change (Q7 gotcha #2), transitions across 5 HudStates via CAAnimationFactory with 150ms crossfade + rate-limited VoiceOver announcements (3s limiter per UI-SPEC Surface 3 line 421)
- MenuBarContextMenu builds Setup… / Settings… (disabled) / Show Dev Overlay / Copy State Dump / Quit Jarvis (⌘Q), with a ClosureTarget trampoline for closure-based actions
- Left-click vs right-click disambiguation via NSApp.currentEvent.type inspection (D-16 / UI-SPEC Surface 3 lines 406-409)
- JarvisAppTests Xcode unit-test bundle target added via project.yml; scheme wires test action against JarvisAppTests
- MenuBarIconControllerTests covers state transitions, no-op-on-same-state, context-menu items, and HudState voiceOverLabels (4 tests pass)
- PBXCopyFilesBuildPhase for Contents/Helpers re-applied post-xcodegen (Plan 01 invariant — MCP-05 acceptance-criteria grep)
2026-04-22 13:14:09 -05:00