19 Commits
Author SHA1 Message Date
james.maes 05411f82ff fix(vision): v0.1 cluster — T2 honesty + pendingFrame leak + TCC revoke observation (#1, #2, #3, #152)
#1 (HIGH): new hasRealT2Provider init param on AgentOrchestrator; t2AvailableForThisTurn now requires both decision.tier != .t3Cloud AND hasRealT2Provider. Production passes false (MissingT2Provider only). Pre-existing escalation test still passes true with mock T2.

#2 (MED): new FrameAttachController.releaseAfterRejectedSubmit() routes through the sole discardFrame() emission site (D-15 single-emission grep gate still green). Both handleChatSubmit and handleChatCancelAndSubmit call it on .rejected.

#3 (MED): AVCaptureSessionWasInterruptedNotification observer yields .midSessionRevoked. macOS limitation surfaced: AVCaptureSessionInterruptionReasonKey is API_UNAVAILABLE(macos), so we cannot discriminate by reason — any interruption surfaces the banner. NSApplication.didBecomeActive observer calls becameAuthorized() on non-authorized → .authorized flip.

Vision 75 + AgentCore 243 tests pass; all 18 boundary gates green.
2026-05-13 06:30:09 -05:00
james.maesandClaude Opus 4.7 508cf924d0 docs(packages): per-package README files
Adds README.md to each of the 13 SPM packages (AgentCore, Bus, Config,
DevOverlay, Harness, Keychain, Logging, MCP, Memory, Replay, Shell, Vision,
Voice). Each covers purpose, key public types, deps, consumers, invariants
(pulled from CLAUDE.md + grep-gates + audit findings), test stack, and
notable files. Memory README explicitly marks the package functionally OFF
until vec0.dylib + Ollama models land.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-04 17:13:33 -05:00
james.maesandClaude Opus 4.7 19362f6988 test(track-c 6): real-hardware vision integration test (XCTSkipIf no camera)
Closes vision-audit §7 critical gap: "zero tests exercise
CameraCapture.captureFrame()'s success path. No test asserts that a
non-trivial JPEG comes back. The 1×1 stub passes every test in this
package."

CameraCaptureRealHardwareTests:
- testRealCameraCaptureProducesNonTrivialJPEG: opens a real session,
  invokes captureFrame() through the production photoOutput delegate
  flow (Track-C 1), and asserts: jpeg byte count > 1000, SOI marker
  prefix [0xFF, 0xD8], width > 1, height > 1. The historical 1×1 stub
  was 125 bytes — the > 1000 threshold and width/height > 1 guards
  make a regression to a trivial fixture trip the test.
- testRealCameraFrameStreamYieldsSamplesWithinTimeout: subscribes to
  frameStream(forPresence: true) (Track-C 2 fan-out) and asserts a
  PresenceFrameSample.sampleBuffer arrives within 2 seconds of opening
  the session.

Gating (skip paths, never prompts):
1. JARVIS_REAL_CAMERA=1 must be set in the environment (matches the
   JARVIS_REAL_MODELS convention used by Memory + Voice hardware
   tests).
2. AVCaptureDevice.default(for: .video) must be non-nil.
3. AVCaptureDevice.authorizationStatus(for: .video) must be
   .authorized — the test does NOT call requestAccess(for:), so a
   headless / first-launch test runner never blocks on a TCC prompt.

Run interactively after granting Camera TCC to the test host:
   JARVIS_REAL_CAMERA=1 swift test --package-path packages/Vision \
     --filter CameraCaptureRealHardwareTests

Vision 70 → 72 (2 tests, both skip without env+TCC). Six boundary
gates pass clean.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-04 13:20:57 -05:00
james.maesandClaude Opus 4.7 eac16c9a33 fix(track-c 5): wire FrameAttachController.confirmSend
Closes vision-audit §4 "confirmSend has zero non-test callers".
FrameAttachController.confirmSend(userText:) was structurally complete
but unreachable from production: requestAttach armed the slot, the
2-second default-cancel timeout fired, the frame was discarded, and the
agent ran text-only. Track-C 5 wires confirmSend into the chat-submit
flow so a user clicking the camera button (Track-C 3) and then sending
text within the confirm window actually delivers the captured frame to
the agent.

App/AppDelegate.swift changes:
- handleChatSubmit now calls a new private helper
  `consumePendingFrameIfAny(userText:)` BEFORE submit. When the
  controller has a pending frame, the helper pulls the resulting
  `ImageBlock` via `confirmSend(userText: text)` and the submit routes
  through `TurnInput.withImages(.text, text:, images: [imageBlock])`.
  When nothing is pending the path collapses to the original
  `submit(.text(text))` — text-only turns are unaffected.
- handleChatCancelAndSubmit gets the same wiring (mirrors the
  WARNING-4 pattern that already applied to phrase triggers — without
  it, barge-in after a camera-button arm silently drops the frame).

This completes the Track-C 3 click flow:
- Camera button click → BusInbound.frameAttachRequested
  → FrameAttachController.requestAttach(.hudButton) (already wired)
  → CameraCapture.captureFrame() returns real JPEG (Track-C 1 fix)
  → frame parked in pendingFrame slot
- User types + sends within 2s
  → handleChatSubmit consumes the pending frame via confirmSend
  → AgentOrchestrator.submit(.withImages(...)) routes through
    VisionRouter.route(...) which selects T1 (or T3 with explicit
    cloud opt-in)

Tests: +3 (FrameAttachConfirmSendWiringTests)
- testAppDelegateCallsConfirmSendInChatSubmitPath: structural grep
  enforcing the wiring exists.
- testConfirmSendIsReachableFromBothChatHandlers: ensures both
  chatSubmit AND chatCancelAndSubmit invoke the helper (WARNING-4
  parity).
- testConfirmSendNilWhenNothingPending: confirms the contract the
  AppDelegate wiring depends on (no surprise auto-confirm).

Vision 67 → 70. Six boundary gates + check-app-builds pass clean.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-04 13:18:46 -05:00
james.maesandClaude Opus 4.7 df6a4d27cc fix(track-c 4): explicit T2Provider missing-variant; remove silent t1 fallback
Closes vision-audit §3 "T2 is decorative". AppDelegate previously wired
the VisionRouter as `t2Provider: t1`, so `evaluatePostResponse →
.escalateToT2 → providerForTier(.t2LocalQuality) → t1Provider` silently
re-ran the same low-confidence response on the same model with no
operator signal that T2 was never built.

This commit takes the explicit-missing path. Real T2 wiring
(VllmMlxSidecar instantiation + binary bundling + codesigning +
feature-flag gating) is downstream work and out of scope for Track C.

Changes:
- packages/Vision/Sources/Vision/VisionError.swift: new
  `.t2ProviderUnavailable` case.
- packages/Vision/Sources/Vision/MissingT2Provider.swift (new):
  `LLMProvider` conformer whose text and multimodal `stream(...)`
  entry points finish with `VisionError.t2ProviderUnavailable`.
- App/AppDelegate.swift: replaces `t2Provider: t1` with
  `MissingT2Provider()`. Comment now documents that callers MUST gate
  T2 escalation on `evaluatePostResponse(..., t2Available: false)`,
  which routes to `.useT1Result` and never invokes T2 — so this
  provider is never actually streamed in the live flow.

Why explicit-missing over real T2: real T2 demands ~6 GB Qwen 3.5 VL
weights, vllm-mlx binary at a stable path, codesigning a nested .app
helper, feature-flag plumbing, and a new TCC surface. The bug being
fixed here is "silent T1-as-T2 fallback hides intent" — making the
fallback explicit (impossible to mistake for working T2) is enough.

Tests: +4 (MissingT2ProviderTests)
- testStreamFinishesWithExplicitError: text path throws
  t2ProviderUnavailable.
- testMultimodalStreamFinishesWithExplicitError: image path throws
  t2ProviderUnavailable.
- testRouterStillStaysOnT1WhenT2Unavailable: re-asserts router's
  no-auto-cloud invariant — production never reaches the missing
  provider's stream when wired correctly.
- testAppDelegateNoLongerCarriesSilentT1Fallback: structural grep
  enforces no regression to `t2Provider: t1`.

Vision 63 → 67. Six boundary gates + check-app-builds pass clean.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-04 13:13:38 -05:00
james.maesandClaude Opus 4.7 ea09fd4e93 fix(track-c 2): frameStream delegate-yielded continuation
Replaces the immediately-finished AsyncStream from 07-04..07-06 with a
real fan-out backed by `AVCaptureVideoDataOutputSampleBufferDelegate`.

CameraCapture changes:
- buildSession() now constructs a `VideoSampleDelegate` and binds it as
  the AVCaptureVideoDataOutput's sampleBufferDelegate on a private
  user-initiated DispatchQueue with `alwaysDiscardsLateVideoFrames`
  enabled (RESEARCH §9 / D-11 throttle posture).
- frameStream(forPresence:) now generates a UUID per subscription, hops
  into the actor to register the continuation against the live delegate,
  and tears down via `cont.onTermination` on consumer cancellation.
  When the session is not yet open, the stream finishes immediately
  (consumers' for-await exits cleanly).
- shutdown() drains the delegate's subscription set via
  `finishAllStreams()` so outstanding consumers terminate.

VideoSampleDelegate is a small NSObject (`@unchecked Sendable`) holding
an NSLock-guarded `[UUID: Continuation]` map. The fan-out path is:
captureOutput → snapshot under lock → yield outside lock. PresenceMonitor
is the only in-tree consumer today.

Tests: +4 (CameraCaptureFrameStreamTests)
- testFrameStreamYieldsInjectedSamplesAfterOpen: opens a real session
  (XCTSkip if no camera), asserts an injected sample reaches the stream.
- testShutdownFinishesOutstandingStreams: shutdown() exits a draining
  for-await within 200 ms.
- testFrameStreamFinishesImmediatelyWhenNotOpen: pre-open semantics.
- testFrameStreamSourceNoLongerImmediatelyFinishes: structural grep
  gate against re-introducing the empty-stream stub.

Vision 59 → 63. Six boundary gates + check-app-builds pass clean.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-04 13:02:46 -05:00
james.maesandClaude Opus 4.7 7a5543a8be fix(track-c 1): CameraCapture AVCapturePhotoCaptureDelegate conformance
Replaces the 1×1 black-JPEG hardcode (vision-audit §1) with the real
AVCapturePhotoOutput delegate flow.

CameraCapture.captureFrame() now:
- Builds AVCapturePhotoSettings(format: jpeg) and invokes
  photoOutput.capturePhoto(with:delegate:).
- Bridges the AVCapturePhotoCaptureDelegate callback into a
  CheckedThrowingContinuation via a new internal PhotoCaptureProxy
  (NSObject conformance — actors can't conform to the AVFoundation
  delegate protocols). The proxy retains itself for the lifetime of
  the capture (AVCapturePhotoOutput holds the delegate weakly), and
  breaks the cycle on the first didFinishProcessingPhoto callback.
- Reads width/height from photo.resolvedSettings.photoDimensions
  with a CGImageSource fallback for hosts that don't populate it.

Test seam: photoCaptureOverride lets unit tests exercise captureFrame()
end-to-end (including the session.isRunning precondition) without real
hardware. Mirrors the existing authStatusProbe pattern.

Tests: +3 (CameraCaptureDelegateTests)
- testCaptureFrameRoutesThroughOverrideSeam: opens a real session
  (XCTSkip if no camera), asserts the override seam returns the
  fixture frame.
- testCaptureFrameThrowsSessionNotRunningBeforeOpen: precondition
  guard preserved.
- testCaptureFrameSourceDoesNotEmbedHardcodedJPEG: structural grep
  gate against re-introducing the onePixelJPEG hardcode.

frameStream(forPresence:) is left as the empty-stream stub for now —
that's track-c 2.

Vision 56 → 59. Six boundary gates + check-app-builds pass clean.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-04 12:56:31 -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.maes b87981cea4 feat(09-03): add PresenceStateSnapshot actor + drain ContextBuilder.installPresence (D-13)
- New PresenceStateSnapshot actor (.shared singleton + record + currentEnrichment); D-14 plain-sentence rendering with 5s present-suppression and 5min absent-threshold rules
- ContextBuilder.installPresence body now drains the AsyncStream into PresenceStateSnapshot.shared.record (replaces the 07-06 deferred-wiring no-op)
- VISION-03 boundary preserved: snapshot returns only String?; no Voice or AgentOrchestrator refs
- 10 PresenceStateSnapshotTests pass (initial nil, suppression, present, absent <5min/=7min, absentLongTerm, unknown, overwrite, type-only, ContextBuilder round-trip)
2026-05-01 20:02:18 -05:00
james.maes dfc237db0c feat(07-06): installMemory + installVision + TS bus protocol 2.1.0 (Task 1)
- AppDelegate gains installMemory() and installVision() mirroring installVoice's
  six-step pattern. Memory installs BEFORE voice (memory ready before first
  turn); vision AFTER voice (camera lifecycle independent).
- MemoryStore + MemoryExtractionOrchestrator + MemoryExtractionCoordinator
  constructed; AppDelegateMemoryReplaySink bridges Memory.MemoryReplaySink ->
  Replay.ReplayLog.record(_:for:). Coordinator wiring deferred until
  AgentOrchestrator lands in AppDelegate (documented in SUMMARY).
- CameraCapture + PresenceMonitor instantiated; the SINGLE PresenceSignalBus
  reference (owned by PresenceMonitor) flows via AppDelegateContextBuilderAdapter
  to ContextBuilder.installPresence and via hudStateCoordinator?.attachPresence
  to the HUD coordinator. VISION-03 boundary preserved.
- VisionRouter constructed with T1=Ollama, T2=Ollama (sidecar deferred), T3=
  Anthropic (key from Keychain); the runTurn dispatcher branch is deferred
  until AgentOrchestrator wires up.
- DisablePresence menu-bar toggle wired alongside MuteWakeWord.
- BannerContent.cameraDenied + cameraRevoked presets; cameraDegradationTask
  watches CameraCapture.degradationStream and enqueues banners.
- HudStateCoordinator.attachPresence is a generic AsyncSequence subscriber
  preserving HUD-08 single-writer invariant (presence is record-only).
- ContextBuilder.installPresence(_:) is a static no-op drain (deferred wiring
  for the future per-turn system-prompt enricher).
- TS BUS_PROTOCOL_VERSION 2.0.0 -> 2.1.0; sessionHistory + TurnRow added to
  the BusOutbound discriminator union with full decode arm. Round-trip test
  bumped accordingly. scripts/check-bus-protocol-version.sh now passes.
2026-04-29 18:40:47 -05:00
james.maes aa31e753bc feat(07-05): FrameAttachController + always-confirm gate + sole emission site (Task 6 GREEN)
- FrameConfirmationDecision: .send/.cancel/.timeout (default-cancel
  semantic — anything other than .send discards)
- FrameAttachController actor:
  * requestAttach(reason:) is the SINGLE entry point — phrase + HUD
    button both reach it (D-13 dual-trigger)
  * confirmSend(userText:) returns the ImageBlock the orchestrator
    routes; replays the placeholder via FrameAttachReplaySink
  * cancel() and timeout-fired both discard via the SOLE emission site
  * onAssistantTurnComplete() releases bytes on LLMEvent.messageStop edge
  * discardFrame() is the SOLE function that clears pendingFrame —
    'pendingFrame = nil' appears EXACTLY ONCE in the file
  * Cancellation-aware timeout task (do/catch around Task.sleep) so
    confirmSend cancellation cleanly suppresses .timeout firing
- FrameAttachReplaySink: actor that records the placeholder JSON via
  the injected ReplayLogProtocol; raw image bytes never enter the sink
- ReplayEvent.userInput doc comment: documents the D-15 placeholder
  convention for image-bearing turns
- All 10 new tests + 37 existing tests pass; 4-leg grep gate enforces
  single emission + raw-bytes egress fence
2026-04-29 18:13:43 -05:00
james.maes 003f8e7546 test(07-05): add failing FrameAttachController + grep + placeholder tests (Task 6 RED)
- FrameAttachControllerTests: dual-trigger ingest, always-confirm gate,
  2s default-cancel timeout, explicit cancel, confirmSend produces
  ImageBlock, onAssistantTurnComplete clears pendingFrame
- FrameAttachDiscardSiteGrepTests: discardFrame appears exactly once in
  FrameAttachController.swift; raw-bytes egress fence on Replay sources
  and FrameAttachReplaySink
- FrameAttachReplayPlaceholderTests: payload is the literal
  {"type":"image","discarded":true,"text":"..."} and contains
  no base64/JPEG/PNG signatures
2026-04-29 18:06:46 -05:00
james.maes cd7381f137 feat(07-05): VisionRouter + VisionRouterConfig + heuristic + ContextBuilder (Task 5 GREEN)
- VisionTier enum with t1Local/t2LocalQuality/t3Cloud + defaultModelID
  mapping; VisionTier.swift is the SOLE source of the literal
  'claude-opus-4-7' / 'gemma4:31b' / 'Qwen/Qwen3.5-35B-A3B-VL' strings
- VisionRouterConfig public struct with named planner-tuned fields
  (substring set, length threshold, phrase sets, timeouts)
- VisionEscalationHeuristic.evaluateLowConfidence — pure function,
  reads from passed config (no hidden defaults)
- EscalationPhraseDetector — word-bounded case-insensitive matchers
  for cloud opt-in (D-18) and frame-attach (D-13) phrase sets
- ContextBuilder — sole non-test caller of EscalationPhraseDetector
- VisionRouter actor: route() never returns T2 directly; T3 only on
  explicit opt-in; evaluatePostResponse() never returns escalateToT3
  (D-18 no-auto-cloud invariant)
- All 16 new tests + 21 existing tests pass; grep gate enforces structural
  no-orphan-cloud-reference invariant
2026-04-29 18:04:22 -05:00
james.maes cc54c8f23d test(07-05): add failing VisionRouter + heuristic + phrase-detector + grep-gate tests (Task 5 RED)
- EscalationPhraseDetectorTests: cloud-opt-in + frame-attach phrase set,
  case-insensitivity, near-miss rejection
- VisionRouterTierLadderTests: deterministic tier ladder (T1 default,
  T3 on explicit opt-in, T2 never directly)
- VisionRouterEscalationTests: D-17 substring + length triggers,
  T2-unavailable + low-confidence stays on T1 (D-18 no-auto-cloud)
- VisionRouterCloudOptInGrepTests: structural grep gate enforcing
  every cloud reference in packages/Vision/Sources/Vision lives in
  a file that also references matchesCloudOptIn (or is VisionTier.swift)
2026-04-29 17:59:40 -05:00
james.maes 65efe64dfd feat(07-05): VllmMlxSidecar + VllmMlxProvider + VllmMlxAvailability (Task 4 GREEN)
- VllmMlxSidecar: actor with lazy ensureRunning(), bounded /health warmup,
  ChildSpawnGate.shared.prepare() before Process factory, env =
  ChildSpawnGate.minimalEnvironment verbatim
- ProcessFactory + SpawnGate injection seams enable test fakes
- VllmMlxProvider: thin LLMProvider actor wrapping
  OllamaProvider(useOpenAICompat: true); both stream overloads delegate
- VllmMlxAvailability: combines macOS 26+ runtime guard, binary-path
  existence check, features.vision.tier2 feature flag
- VisionError gains .sidecarStartupTimeout
- All 5 VllmMlxSidecarSpawnTests + 14 existing Vision tests pass
- VISION-03 boundary holds (no AgentOrchestrator/JarvisVoice/JarvisMCP in deps)
2026-04-29 17:56:40 -05:00
james.maes 16ee858361 test(07-05): add failing VllmMlxSidecar spawn invariant tests + Vision deps (Task 4 RED)
- VllmMlxSidecarSpawnTests asserts:
  * ChildSpawnGate.prepare() runs before Process factory invocation
  * proc.environment equals ChildSpawnGate.minimalEnvironment exactly
  * /health timeout surfaces VisionError.sidecarStartupTimeout
  * VllmMlxAvailability.isAvailable false on missing binary / disabled flag
- Vision/Package.swift gains JarvisChildSpawn (via MCP), OllamaProvider,
  AnthropicProvider deps for upcoming tasks
2026-04-29 17:53:28 -05:00
james.maesandClaude Opus 4.7 43e7b8a1fc feat(07-04): DisablePresence menu-bar toggle (D-12) + tests
Verbatim clone of packages/Voice/Sources/Voice/Control/MuteWakeWord.swift
adapted to PresenceMonitor:
- @MainActor final class
- UserDefaults key: features.vision.presenceDisabled
- NSMenuItem with title 'Disable presence'; state mirrors isDisabled bool
- handleToggle() flips state, persists, and dispatches pause()/resume() to
  PresenceMonitor on a fire-and-forget Task
- toggleForTest() seam for synchronous test driving

D-12 invariant enforced: this class touches PresenceMonitor only —
CameraCapture is intentionally untouched so frame-attach (VISION-04)
remains usable while presence is paused. testDisablePresenceDoesNotTouchCameraCapture
opens DisablePresence.swift on disk, strips comment lines, and asserts
'CameraCapture' does not appear in non-comment lines.

Tests (4 cases, all PASS):
- testInitReadsPersistedDisabledState: defaults seeded true → isDisabled
  true, menu item state .on
- testInitReadsPersistedEnabledState: defaults seeded false → isDisabled
  false, menu item state .off
- testToggleFlipsStateAndPersists: toggle round-trips state, persistence,
  and menu item state
- testDisablePresenceDoesNotTouchCameraCapture: static-grep invariant

All 14 Vision tests pass; both VISION-03 grep gates green.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-29 16:31:03 -05:00
james.maesandClaude Opus 4.7 023881715c feat(07-04): PresenceSignalBus + PresenceMonitor + CameraCapture (TDD GREEN)
Vision presence pipeline (signal-only, VISION-02 / VISION-03):

- PresenceEvent / Presence enums (Sendable, Equatable). Three published
  states: .present, .absent(since:), .absentLongTerm(since:); .unknown is
  the pre-first-frame sentinel and is never published.
- PresenceSignalBus: a bare AsyncStream-wrapper struct with public read
  side and internal init — there is no subscriber registry, no broadcast
  fan-out, no inbound API. Only PresenceMonitor constructs the bus.
- CameraCapture actor: AVCaptureSession lifecycle owner with TCC switch,
  authStatusProbe test seam (mirrors AudioGraphOwner), open()/shutdown()
  pair, becameAuthorized() entry for D-09 first-grant flow, single-frame
  captureFrame() with sessionNotRunning guard (stub JPEG body until 07-05
  wires AVCapturePhotoOutput delegate), nonisolated frameStream(forPresence:)
  returning an empty stream until 07-06 wires the delegate.
  - Mid-session AVCaptureSessionRuntimeError observer captures only
    Sendable values (Continuation + label string) so the @Sendable closure
    crosses the actor boundary cleanly under Swift 6 strict concurrency.
- CapturedFrame value type for VISION-04 frame-attach (consumed by 07-05).
- PresenceMonitor actor: WakeWordDAG-shaped (actor + nonisolated bus +
  Task.detached feed loop + paused/resume + cancel). Edge-debounced 2.0s
  state machine; 5-min absent secondary threshold; feedObservation +
  evaluateLongTerm test seams (both honor paused for VOICE-12-style
  parity). VNDetectFaceRectanglesRequest paths in for sampleBuffer +
  cgImage; tests exercise the .syntheticDetection path.
- PresenceFrameSample: `@unchecked Sendable` because CMSampleBuffer +
  CGImage are reference types with no Swift Sendable conformance; the
  enum is single-producer / single-consumer in this codebase.

Tests (10 cases across 4 files, all PASS):
- PackageBoundaryTests: module compiles standalone; public API surface.
- PresenceSignalBusTests: PresenceEvent equality + bus delivery order.
- PresenceMonitorDebounceTests: sub-2s blips suppressed; 5-min long-term
  fires; pause stops emissions.
- CameraCaptureTCCTests: TCC denied yields banner+throws; notDetermined
  throws without banner; captureFrame throws when session not running.

Deviations from plan body (Rule 3 — blocking, deviation_rules):

1. Module renamed Vision -> JarvisVision. The plan's module name `Vision`
   collides with Apple's Vision framework: when a file inside module
   `Vision` writes `import Vision`, Swift silently treats it as a self-
   import and the framework symbols (VNDetectFaceRectanglesRequest etc.)
   never resolve. `JarvisVision` matches the existing JarvisLogging
   convention. The package directory stays packages/Vision; the grep
   gates key off the directory path, not the module name. Tests use
   @testable import JarvisVision.

2. project.yml product entry: `product: JarvisVision` instead of `Vision`
   — same root cause as #1.

3. testPauseStopsEmissions: feedObservation must honor the paused gate
   identically to consume(); the plan's body skipped that guard inside
   feedObservation, which would have made the paused test fail. Fix is a
   one-line `guard !paused else { return }` at the top of feedObservation.

4. testEmitsAbsentLongTermAfter5Minutes: bumped the evaluator timestamp
   from `t0 + 5*60 + 1` to `t0 + 3 + 5*60 + 1` because the second
   feedObservation at `t0+3` is what publishes `.absent(since: t0+3)`,
   so the 5-min threshold check uses t0+3 as the reference. The +3s
   alignment matches the production semantics literally.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-29 16:29:19 -05:00
james.maesandClaude Opus 4.7 c9889705fb feat(07-04): create packages/Vision SPM scaffold + VISION-03 grep-gates
- New packages/Vision SPM target (macOS 14, swift-tools 6.0, strict-concurrency v6).
- Dependencies: Logging + AgentCore + swift-log only (NO Voice, NO AgentOrchestrator).
  This is the compile-time half of VISION-03; the textual halves are the two
  scripts below.
- VisionError (TCC + capture errors) and VisionDegradationReason (cameraDenied,
  midSessionRevoked) — analogs of Voice's Error / DegradationReason.
- scripts/check-vision-isolation.sh: fails build on any `import Voice` /
  `import AgentOrchestrator` under packages/Vision/Sources.
- scripts/check-presence-bus-no-tts-orchestrator.sh: fails build on any
  PresenceSignalBus reference inside packages/Voice/Sources or
  packages/AgentCore/Sources/AgentOrchestrator (non-comment lines only).
- Wired Vision into project.yml (XcodeGen) — workspace has no top-level
  Package.swift; mirrors the Memory wiring from 07-01 (same deviation
  pattern, same rationale).
- Updated NSCameraUsageDescription to the plan's wording (key was already
  present from earlier scaffolding).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-29 16:09:39 -05:00