Commit Graph
29 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.maes e9a0bb14d2 feat(01-05): add JarvisEntitlementProbeTests target + EntitlementProbe scheme
- App/Tests/JarvisEntitlementProbeTests/JarvisEntitlementProbeTests.swift:
  one-shot XCTest gated on #if JARVIS_ENTITLEMENT_PROBE. Calls
  SpeechTranscriber(locale:preset:) + AssetInventory.status(forModules:)
  and accepts any of three failure surfaces as confirmation that the
  speech-recognition-assets entitlement is load-bearing:
    (a) NSError SFSpeechErrorDomain code 1 (assetUnavailable)
    (b) NSError SFSpeechErrorDomain code 10 (macOS 26 'unallocated locales')
    (c) AssetInventory.Status description containing 'unavailable' /
        'unsupported'
- App/Tests/JarvisEntitlementProbeTests/Info.plist: standard test bundle.
- project.yml: new JarvisEntitlementProbeTests target (OTHER_SWIFT_FLAGS =
  -DJARVIS_ENTITLEMENT_PROBE) + EntitlementProbe scheme. Default Jarvis
  scheme's test action lists only JarvisAppTests — the probe target is
  opt-in via EntitlementProbe scheme only.
- Jarvis.xcodeproj/xcshareddata/xcschemes/EntitlementProbe.xcscheme: new
  shared scheme referencing JarvisEntitlementProbeTests as its sole
  testable.

Deviations from plan's copy-verbatim XCTest shape:
- Added @available(macOS 26.0, *) to the test class: macOS 26 SpeechModule
  / SpeechTranscriber / AssetInventory are not available pre-Tahoe.
  Deployment target stays at 13.0 for the rest of the project.
- preset: .progressiveLiveTranscription -> .progressiveTranscription
  (actual Apple SDK name on macOS 26.4).
- AssetInventory.status(forModules:) is async non-throwing on macOS 26.4;
  wrapped in a throwing adapter so the historical do/catch shape from
  RESEARCH Q4 (NSError SFSpeechErrorDomain codes 1/10) stays intact.

Also: [Rule 3 - Blocking] extend codesign.sh to handle nested .xctest
plugins under Contents/PlugIns/. When build-for-testing is invoked, the
xctest lives inside Jarvis.app/Contents/PlugIns/ and must be signed
before the main app, or codesign on the parent bundle fails with
'bundle format unrecognized'. Empty xctest shells that materialize
before the test target has been compiled are removed (Xcode recreates
+ populates them in a later iteration).

Task 3 acceptance: xcodebuild -list shows EntitlementProbe scheme +
JarvisEntitlementProbeTests target; EntitlementProbe build-for-testing
succeeds with the full codesign + entitlement verification chain.
2026-04-22 14:42:30 -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