Commit Graph
24 Commits
Author SHA1 Message Date
james.maesandClaude Opus 4.7 5ebe510627 docs: scrub Phase-7 + JarvisBusWorld doc drift (#19, #45) (#195)
* docs(memory): scrub Phase-7 hedge language from MemoryStore (#19)

Audit 2026-05-12 flagged five doc-comment references that mis-described
the implementation state. The cited plans (07-02, 07-03, 07-06) shipped
months ago — the bounded extraction channel, the hybrid FTS5 + vec0 read
path, the ReplayEvent emission, the regression corpus test seams, and
`installMemory`'s replay-sink wiring are all live, not "wires" or
"will replace" promises.

Five touchpoints rewritten to describe the code as it is:

- `MemoryReplaySink` doc: drop the "Plan 07-06" plan reference; the seam
  is just used at startup.
- `MemoryStore` actor doc: replace the "Plan 07-02 wires / Plan 07-03
  wires" sentence with the actual contract (single writer; hybrid search
  through `MemoryReadStore`; `recordRetrieval` emits `memoryRetrieval`).
- Read-path MARK: drop the "Plan 07-03" plan label; keep the
  requirement-ID list (MEM-07 / TEXT-03 / D-02 / D-05) since those are
  still useful for traceability to the audit corpus.
- `recordRetrieval` doc: replace "Mirrors recordMemoryMutation from 07-02"
  with "Mirrors recordMemoryMutation on the write path."
- Test-seams MARK: drop "Plan 07-06" prefix; describe by the test target
  (JARVIS_REAL_MODELS=1 corpus suite) instead.

The 2026-05-11 D-5/D-6 markers and the "Boot policy (changed 2026-05-11)"
doc are preserved unchanged — they document current behaviour, not
historical hedges.

`swift build --package-path packages/Memory` clean.

Closes #19

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* docs(bus): scrub JarvisBusWorld drift + add grep gate (#45)

`fb41c5f` (2026-05-02) unified `Injection.js` and the HUD bundle into the
default `WKContentWorld.page` and retired the prior isolated
`JarvisBusWorld` named world. The doc-comments and test fixtures across
20+ lines kept describing the bug-era topology — pre-existing-bus attach
"because cross-world isolation", message-handler-only-visible-in-named-
world, etc. — for months afterward. Result: a new reader following the
docs would conclude the bus uses two worlds when it actually uses one.

Five rationale rewrites:

- `WebviewBridge.swift` class doc — handler is on the page world, not an
  isolated named world. Page scripts in the loaded HUD bundle address it
  via `window.webkit.messageHandlers.jarvisBus`.
- `Injection.js` header — names the page world explicitly; one-line nod to
  `fb41c5f` for the historical context.
- `bridge.ts` JSDoc + `isJarvisBusShape` rationale — recast the H-02
  attach-to-existing path as a "defensive double-install guard" inside
  ONE page world. Injection.js fires first at document-start; the bundle
  installer fires later as a top-level `main.tsx` side-effect; the guard
  prevents the bundle from clobbering Injection.js's queued early
  messages and captured handshake state. The duck-type `isJarvisBusShape`
  CODE is unchanged — only the comment narrative was misleading.
- `webview/packages/hud/src/main.tsx` — drop the cross-world-isolation
  framing; describe the single-world reality.
- `App/AppDelegate.swift` install comment — page world, not isolated.

Test-side updates (same scrub, plus one latent test bug surfaced):

- `WebviewBridgeTests.test_scriptMessageHandlerIsRegisteredUnderConfigured-
  Name`: was removing from `WKContentWorld.world(name: "JarvisBusWorld")`
  while the bridge registered against `.page` — silent no-op since
  `removeScriptMessageHandler` doesn't throw on absent. Now removes from
  `.page` (the world the bridge actually uses), making the
  "registration-is-evidence-of-success" assertion meaningful again.
- `WebviewBridgeOutboundTests`: comment-only.
- `RealWKWebViewIntegrationTests`: comment-only (forensic narrative kept,
  literal world name replaced with "prior isolated named content world").
- `webview/packages/bus/tests/bridge.test.ts`: H-02 describe-block + one
  it-name rewritten to match the new defensive-double-install rationale.

Infrastructure:

- `scripts/check-no-jarvis-bus-world-references.sh`: new boundary gate.
  Greps for the literal `JarvisBusWorld` in tracked files; excludes
  `.planning/`, `docs/`, `webview/packages/*/dist/`, and this script
  itself. Wired into `CLAUDE.md`'s gate listing; the CI
  `boundary-gates` job auto-discovers all `scripts/check-*.sh`, so no
  workflow edit is needed.
- `webview/packages/bus/package.json`: 0.0.0 → 0.0.1 (per spec —
  comment-only changes still bump patch so consumers can pin).
- `webview/packages/bus/dist/bridge.{js,d.ts}`: rebuilt via `pnpm --filter
  @jarvis/bus build`; NOT committed because `dist/` is not tracked (the
  build phase regenerates on each xcodebuild invocation).

Verified locally:
- `swift build --package-path packages/Bus` clean
- `pnpm --filter @jarvis/bus test` — 37/37 pass
- `bash scripts/check-no-jarvis-bus-world-references.sh` — PASS
- `bash scripts/check-bus-protocol-version.sh` — PASS (v2.3.0)
- `bash scripts/check-bus-harness-parity.sh` — PASS

Closes #45

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-14 09:51:03 -05:00
james.maesandClaude Opus 4.7 a121d79174 fix: STT backend constant casing + audioLevel NaN guard (#37, #47) (#196)
* docs(voice): pin STT backend constant identity contract (#37)

The runtime mismatch flagged by #37 was already fixed at the call sites
in `App/AppDelegate.swift:1631-1642` and `:2562-2575` — both routes now
reference `STTBackendSelector.backendSpeechAnalyzer` /
`backendWhisperKit` (snake_case) instead of the pre-2026-05-12 camelCase
string literals. The regression test
`STTBackendSwitchTests.testB5_canonicalConstantsRoundTripThroughMake`
already locks both halves of the contract:

  1. The snake_case constants round-trip through `make(backend:)` without
     hitting the unknown-backend default branch.
  2. The pre-fix camelCase shape (`"whisperKit"`) DOES land in the
     default branch and DOES emit an unknown-backend warning.

So the #37 close action is just to make that contract self-documenting
at the declaration site rather than relying on readers finding the test.
Add a doc-comment to the two `public static let backend*` constants that
explains the wire shape, names the regression test, references Issues
#29 + #37, and warns against renaming without updating `PerTurnSnapshot`
and the test in lockstep.

No code change; `swift test --package-path packages/Voice` continues to
pass (26 tests).

Closes #37

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* fix(bus): clamp non-finite audioLevel rms to 0.0 at encode (#47)

`BusOutbound.audioLevel` carries a `Float rms` that `AudioLevelEmitter`
produces per audio frame (~100 Hz once the consumer is wired up).
Foundation's `JSONEncoder.NonConformingFloatEncodingStrategy` defaults
to `.throw`, so a single NaN — easy to land via a CoreAudio
over-/underflow or an all-zero buffer fed through certain RMS shapes —
would throw inside `WebviewBridge.sendRaw` and tear down the per-frame
bus path. The HUD-side consumer in `webview/packages/hud/src/bus/
client.ts:135` is currently a no-op (the ring shader is still on the
synthetic-sine animation from Plan 03-03), so the bug is LATENT, not
actively firing — but the clamp is cheap, defensive, and pins the
contract before the consumer activates.

Inline clamp at the encode site (`BusOutbound.encode(to:)` switch arm
for `.audioLevel`): replace non-finite floats with `0.0` via
`rms.isFinite ? rms : 0.0` immediately before `container.encode(_:)`.
No refactor of `AudioLevelEmitter` — the per-spec call here is
defensive at the serialization boundary, where every encoder-level
overflow path converges.

Two new unit tests in `CodableRoundTripTests`:
- `test_audioLevel_nanIsClampedToZero` — encodes `.audioLevel(rms: .nan)`,
  decodes, asserts `rms == 0.0` and `rms.isFinite`.
- `test_audioLevel_infinityIsClampedToZero` — companion for ±∞ so a
  future refactor cannot narrow the guard to NaN-only.

`swift test --package-path packages/Bus --filter CodableRoundTripTests`
passes (30 tests, +2 from this commit).

Closes #47

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-14 09:50:50 -05:00
james.maesandClaude Opus 4.7 bcc55c68be fix(ci): pin Xcode 26.x, drop phantom VoiceLog, harden Batcher timing (#167)
CI run 25799596445 failed on 6 SPM packages + the App target. Root cause
was a toolchain skew between local dev (Swift 6.3.2 / Xcode 26) and CI
(Swift 6.0 / Xcode 16):

- mlx-audio-swift v0.1.2 ships swift-tools-version:6.2 which Swift 6.0
  cannot parse, breaking Voice + Harness + the App target with
  "Dependencies could not be resolved because 'mlx-audio-swift' contains
  incompatible tools version (6.2.0)".
- MCP swift-sdk 0.12.0 uses the new `withThrowingTaskGroup` overload
  (no `of:` parameter) that only exists in Swift 6.2+.
- AgentCore tests use `@MainActor` XCTestCase with `await super.setUp()`,
  a pattern Swift 6.0 rejects but Swift 6.1+ accepts.

The macos-15 runner already ships Xcode 26.3 / 26.2 / 26.1 / 26.0
alongside 16.x, so the fix is to switch the Xcode select. Fallback to
Xcode_26.app preserves robustness against patch-level shuffles.

Also:
- Drop `VoiceLog` from the SPM matrix. No `packages/VoiceLog` directory
  exists; the job failed with `chdir error: No such file or directory`.
  Voice-log work is parked under v1.1.
- Bump BatcherTests `waitForDrain` from 80ms to 250ms. The batcher
  window is 33ms; 80ms was tight enough that macos-15 dropped
  test_tokenDeltaConcat with sendCount=0 because the scheduled drain
  hadn't completed its actor + main-actor hop before the test resumed.
  250ms (~7.5× the window) survives CI jitter; verified locally — all
  five Batcher tests pass in 1.27s.

Refs: CI run 25799596445.

Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-13 08:37:47 -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 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 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 6f6617e67b fix(test): update F-A2-01 stale JarvisBusWorld assertion to .page
The Bus production switched from `WKContentWorld.world(name: "JarvisBusWorld")`
to `WKContentWorld.page` in fb41c5f, but `WebviewBridgeOutboundTests
.test_sendWhenArmedCallsEvaluator` line 80 still asserted the retired
named world. Test was reported green in HANDOFF but actually fails on a
fresh run — the stale literal flagged in the 2026-05-03 tests audit.

Updated assertion + comment trail explaining the fb41c5f drift.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-04 11:29:59 -05:00
james.maesandClaude Opus 4.7 1f8e03edeb fix(track-a): demo-blocker class + audit reports + chat-turn e2e tests
User feedback: 50+ hours of agent work, nothing demonstrably working.
Fanned out 6 deep auditors (voice, HUD, vision, memory, LLM, tests);
all reports landed at .planning/audit-2026-05-03/ with brutal-honesty
mandates. Synthesis (SYNTHESIS.md) confirmed: every modality has at
least one fatal break between code-that-passes-tests and
code-that-runs. Track A is the smallest credible path to one working
modality (text turn + animated rings).

Closed under TDD discipline:

**Item 4 — HUD idle ring static rendering (F-A1-01).**
- webview/packages/hud/src/hud/stateUniforms.ts:30 — was
  `pulse: 0.0, rotate: 0.0` for idle, the all-static configuration
  that rendered STANDBY as dots instead of animated rings. Now
  pulse: 0.4, rotate: 0.05 — gentle ambient distinct from booting +
  thinking.
- webview/packages/hud/tests/stateUniforms.test.ts — table updated;
  new regression-guard `STATE_PARAMS[*] is not all-static` (7 cases,
  one per HudState) prevents anyone from re-zeroing a state.

**Item 1 — CacheHints eligibility gate (likely streamTruncated cause).**
- packages/AgentCore/Sources/AgentCore/CacheHints.swift —
  + static func eligibleForSystemPrompt(_:ttl:). Anthropic's prompt
  cache requires ≥1024 tokens at the breakpoint; below that, the
  server returns 200 OK + immediate-EOF for `extended-cache-ttl`
  hints. The orchestrator's default system prompt is ~10 tokens.
  Helper returns nil for sub-threshold prompts so no cache_control
  marker is emitted.
- packages/AgentCore/Sources/AgentOrchestrator/AgentOrchestrator.swift
  — call sites now pass `CacheHints.eligibleForSystemPrompt(finalSystem)`
  through runTurnLoop instead of unconditional `.extended1h`.
- Tests: CacheHintsEligibilityTests (6 cases), RequestBodyTests R5a/R5b
  (2 cases verifying small-prompt → no cache_control in encoded JSON,
  large-prompt → cache_control present).

**Item 2 — StreamOutcome diagnostic.**
- packages/AgentCore/Sources/AnthropicProvider/StreamOutcome.swift —
  new pure classifier that distinguishes:
    - non-2xx HTTP status
    - 200 OK + 0 bytes (cache_control / auth-header rejection class)
    - 200 OK + bytes + 0 frames (malformed body)
    - 200 OK + frames + EOF before message_stop (mid-stream drop)
    - 200 OK complete (healthy)
  AnthropicProvider's read loop counts frames + bytes and logs the
  summary once per stream. Production logs now distinguish failure
  modes that previously all surfaced as `streamTruncatedFinal`.
- Tests: StreamOutcomeTests (7 cases, all branches).

**Item 3 — KeychainError surfacing.**
- packages/AgentCore/Sources/AnthropicProvider/AnthropicAPIKeyProvider.swift
  — new builder `make(keychain:item:)` returns the
  `@Sendable () async throws -> String` closure with errors
  propagating verbatim. Closes the audit's hidden-401 finding:
  `(try? keychainStoreLocal.get(.anthropic)) ?? ""` swallowed the
  real KeychainError into an empty x-api-key, producing
  `401 x-api-key header is required` indistinguishable from a
  stale-key 401. Now the actual error reaches the chat panel via
  AnthropicProvider → orchestrator.error → BusForwarder →
  submitRejected.
- App/AppDelegate.swift — both Anthropic provider construction sites
  (installAgent, installVision T3 wiring) now use the propagating
  builder.
- Tests: AnthropicAPIKeyProviderTests (6 cases — itemNotFound,
  unexpectedStatus, duplicateItem propagation; happy path; explicit
  item; default item).

**Item 5 — F1 #2 chat-turn round-trip e2e tests.**
- packages/Bus/Tests/BusTests/RealWKWebViewIntegrationTests.swift —
  + 2 new tests using a real WKWebView:
    - test_chatTurnRoundTrip_busOutboundReachesPageHandler — Swift
      `bridge.send(.turnStarted/tokenDelta/turnEnded)` reaches the
      page's onOutbound handler. This is the test the v0.12.0 audit
      called out as catching BLOCKER-INT-1, INT-2, INT-3,
      F-E-RACE-1, F-E-FK-1, F-E-WIRE-1 in one shot.
    - test_chatTurnRoundTrip_busInboundFromJSReachesSwiftHandler —
      JS-side `window.jarvisBus.send({type:'chatSubmit',...})` —
      exactly what ChatInput.tsx does — reaches the bridge's
      onInbound handler with the right body.

**Verification:**
- AgentCore: 215/215 (was 178; +37 across BusForwarder, CacheHints,
  StreamOutcome, AnthropicAPIKeyProvider, RequestBody R5).
- Replay: 33/33.
- Bus: 57/58 (1 pre-existing failure: F-A2-01 stale `JarvisBusWorld`
  assertion documented in AUDIT-FINDINGS, separate scope).
- webview/packages/hud: 94/94 (was 87; +7 stateUniforms regression-
  guards + idle table updates).
- All 15 boundary grep gates: PASS.
- scripts/check-app-builds.sh: PASS.

**Audit reports retained at .planning/audit-2026-05-03/:**
- SYNTHESIS.md (consolidated, with triage plan)
- hud-audit.md, llm-audit.md, memory-audit.md, tests-audit.md,
  vision-audit.md, voice-audit.md, voice.md (per-modality detail)

**Carry-forward (NOT closed this commit):**
- F-A2-01 stale WebviewBridgeOutboundTests assertion (pre-existing).
- Voice (F): models never bundled, audio graph never instantiated,
  STT bridge body is `_ = chunk`, TTS engine nil. ~1 day.
- Vision (F): AVCapturePhotoOutput allocated but capturePhoto never
  called; HUD camera button doesn't exist. ~1 day.
- Memory (F): Apple's stripped libsqlite3 prevents sqlite-vec from
  loading; "future ops plan" has zero commits. ~3 days.
- LLM (D+): NoopBusGateway still drops tool-call events
  (BLOCKER-INT-1).
- Tests (C-): 5 XCTAssertTrue(true) tautologies + multiple
  rubber-stamp tests defending stubs.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-03 17:23:13 -05:00
james.maes 326a27d530 test(bus,f1): real-WKWebView handshake integration test — would have caught the WKContentWorld bug class
Phase F1 first deliverable, AUDIT-AND-FIX-PLAN.md. Pragmatic scope
adjustment: lands as `packages/Bus/Tests/BusTests/RealWKWebViewIntegrationTests.swift`
(SPM test target, runnable via `swift test`) instead of a brand-new top-
level `IntegrationTests` Xcode target. Reasoning:

  • Xcode-target scaffolding requires substantial pbxproj surgery (new
    PBXNativeTarget + dependency proxy + build-config list + 2 build
    configs + sources/frameworks phases + N package-product-dependencies)
    that risks breaking the project file.
  • The Bus package's existing SPM test target already imports WebKit and
    uses real WKWebView in its other tests; adding to it is a pure file-
    addition with no project-graph changes.
  • SPM `swift test` runtime is immune to upstream Xcode 26 RunningBoard
    error 5 that had blocked Phase 3's xcodebuild test runtime.
  • Phase D-Phase-2 of AUDIT-AND-FIX-PLAN.md specifies this exact path
    (`packages/Bus/Tests/BusTests/RealWKWebViewIntegrationTests.swift`),
    so F1's "first starter test" + D-Phase-2's required test collapse
    into one shipping file.

Two tests, both pass in ~1.0–1.4s on the fixed branch:

  test_endToEndHandshake_reachesArmed
    Constructs a real WKWebView, loads a tiny HTML page so Injection.js
    runs at document-start, calls bridge.startHandshake(), polls for
    handshakeState == .armed within 2.5s. Asserts armed AND alert-path
    NOT fired (alert is the FAILURE handler).

    On the fb41c5f-fixed branch: passes (~1.0s — load + Injection.js +
    handshake round-trip in unified .page world; auto-ack-hello at the
    stub layer means no bundle handler needed).

    On the bug branch (WKContentWorld(name: "JarvisBusWorld") for
    Injection.js + page world for the message handler): would hang on
    .sentHello, hit the 2s production timeout, transition to .timedOut,
    invoke alertPresenter. Both assertions fail. THIS is the test that
    would have caught the WKContentWorld isolation bug before shipping.

  test_endToEndHandshake_passesThroughSentHello
    Same setup but samples handshakeState rapidly post-startHandshake()
    to confirm .sentHello was observed before .armed. Defends against a
    future regression where Swift short-circuits .armed without the JS
    round-trip (which would make the first test pass spuriously).

Test verdict on production code: green. Bus suite count delta: 54 → 56
tests. One failure (F-A2-01) remains unchanged — pre-existing stale
JarvisBusWorld assertion in WebviewBridgeOutboundTests.test_sendWhenArmedCallsEvaluator.

Honest scope: 3 of the 4 planned F1 starter tests still TODO:
  - Wizard happy path
  - Toggle HUD via hotkey
  - Ask-the-agent-something flow (would catch BLOCKER-INT-2 directly)

The third is the highest-priority remaining F1 work — once it lands,
BLOCKER-INT-2 becomes a failing test we can then TDD-fix in Phase E.

Refs: .planning/AUDIT-AND-FIX-PLAN.md Phase F1 + Phase D-Phase-2.
2026-05-02 20:15:12 -05:00
james.maes 2c349a9b45 chore(diag): strip BootDiag instrumentation; keep WKContentWorld + Injection.js production fixes
Phase A2 of AUDIT-AND-FIX-PLAN.md. The BootDiag instrumentation served its
purpose during the 2026-05-02 session — it produced the JS-state snapshot
(`handler=null pending=1`) that pinpointed the WKContentWorld isolation
bug behind the HUD-stuck-on-BOOTING. With the production fix landed at
fb41c5f (content-world unification + always-ack-hello at the stub level),
the diagnostics are no longer load-bearing.

Stripped:
  • App/Diagnostics/BootDiag.swift (deleted; was untracked)
  • packages/Bus/Sources/Bus/BusBootDiag.swift (deleted; was untracked)
  • Call sites in App/AppDelegate.swift, App/HUD/HudStateCoordinator.swift
    (reverted via `git checkout` — pure diagnostic additions, no
    interleaving with production logic)
  • BootDiag PBXFileReference + PBXBuildFile entries in
    Jarvis.xcodeproj/project.pbxproj (reverted)
  • Three diagnostic helpers in WebviewBridge.swift:
    - `diagnoseJSReceiveState(after:)` — JS-bus state snapshot orchestration
    - `snapshotJSBus(label:)` — `evaluator.callAsync` query that read
      `_handler`/`_pendingMessages.length`/`protocolVersion`/`#root` state
    - `messageDescription(_:)` — compact JSON encoder for diag log lines
  • All `BusBootDiag.info(...)`/`.error(...)` call sites in
    `startHandshake()`, `send(_:)`, and `handleHelloAck(_:)`
  • The `do { ... } catch { logBootDiag; throw }` wrap in `send(_:)` —
    the wrap existed only to log; without diagnostics, the bare
    `try await sendRaw(message)` propagates the throw natively.

Kept (production fixes from fb41c5f):
  • WebviewBridge default contentWorld parameter (`.page`)
  • Injection.js always-ack-hello at the stub layer
  • Injection.js FIFO `_pendingHello` queue replacing the single-slot
    pre-handler buffer

Verification:
  • `scripts/check-app-builds.sh`: PASS — App target compiles cleanly
    after revert.
  • Bus package tests: 54/54 → 53/54. The single failure
    (`test_sendWhenArmedCallsEvaluator` asserting the retired
    `JarvisBusWorld` named world) is **pre-existing relative to A2** —
    `git show fb41c5f -- .../WebviewBridgeOutboundTests.swift` returns
    empty, confirming fb41c5f did not update the test alongside the
    production world-name change. Logged as F-A2-01 in AUDIT-FINDINGS.md;
    will be addressed in Phase D-Phase-2 (real-WKWebView integration test)
    or Phase E (per-finding fix loop), not here, per operating principle
    "audit first, fix second" of AUDIT-AND-FIX-PLAN.md.

Also adds `.planning/AUDIT-FINDINGS.md` — durable triage list for the
audit. Started early during Phase A1 to capture two findings observed
before the formal `/gsd-audit-uat`/`/gsd-audit-milestone` runs:
  • F-A1-01 🟡 — HUD rings render statically at STANDBY (animation
    regression; system-side bus is healthy per `snapshotJSBus` while it
    was still in tree).
  • F-A2-01 🔴 — pre-existing stale test assertion described above.

Refs: .planning/AUDIT-AND-FIX-PLAN.md Phase A2.
2026-05-02 19:23:05 -05:00
james.maes fb41c5f1df fix(bus): unify WKContentWorld + always-ack hello at stub level
Two related bugs blocking the HUD from advancing past BOOTING after
handshake. Found via BootDiag instrumentation that proved Swift was
correctly emitting .idle but the JS handler was never receiving it
(_handler null, _pendingMessages stuck with 1 entry).

Root cause #1 — content-world isolation:
  Injection.js installed window.jarvisBus in WKContentWorld 'JarvisBusWorld'.
  The page bundle (<script type=module>) ran in the DEFAULT page world.
  WKContentWorld isolates window.* property access (the comment in
  main.tsx claiming otherwise was wrong — content worlds share the DOM
  *object identity* but properties are per-world). Result: bundle's
  attachBus() saw window.jarvisBus = undefined in default world, so
  installJarvisBus's Vy() created a SECOND fresh bus object there.
  Two stubs, two different objects, no shared handler. Swift sent
  outbound to the JarvisBusWorld stub which had _handler=null forever.

  Fix: WebviewBridge defaults to WKContentWorld.page so Injection.js,
  the page bundle, and the script-message-handler all share one world.
  Injection.js stub IS the only window.jarvisBus, bundle's Vy() shape-
  checks it and skip-replaces (correct), bundle's onOutbound(handler)
  sets _handler on the same object Swift's outbound hits. Public init
  gains a contentWorld parameter (default .page) for tests to inject.

Root cause #2 — hello ack lost when handler is registered first:
  The stub's receive() routed messages to _handler if registered, falling
  through to auto-ack only when _handler was null. Once world-isolation
  was fixed, the bundle's handler registered BEFORE Swift's first hello
  arrived. The bundle's outbound switch has 'case hello: break' (it
  intentionally treats hello as a no-op at the application layer). With
  no auto-ack and no handler ack, the handshake timed out and the bridge
  never reached .armed.

  Fix: Injection.js receive() now ALWAYS auto-acks hello at the stub
  level, before checking _handler. Hello is owned by the stub regardless
  of bundle handler state — only the stub knows the protocolVersion the
  Swift side needs to match and the bundle's case-hello-break makes
  routing through to the bundle pointless. The 'case hello: break' line
  in the bundle becomes dead code but harmless.

Verified via BootDiag trace:
  Before: snapshotJSBus: handler=handler-null pending=1 (stuck)
  After:  snapshotJSBus: handler=handler-set pending=0 (delivered)
2026-05-02 08:55:06 -05:00
james.maes e8a956c69f fix(bus): replace single-slot _pendingHello with FIFO queue in Injection.js
The injected stub at packages/Bus/Sources/Bus/Resources/Injection.js
buffered exactly one non-hello message in _pendingHello before the
bundle's handler registered via onOutbound(). Subsequent non-hello
messages overwrote the slot, so only the LAST early message survived.

In production this manifested as HUD-stuck-on-booting after Phase 9
shipped: handshake armed correctly (after the v2.3.0 fix), markReady()
fired, HudStateCoordinator emitted .hudState(.idle) — but that message
landed in _pendingHello while the bundle was still loading, then got
clobbered by a subsequent boot-time outbound (telemetry, indicator
state, etc.) before the bundle's handler finished registering. The
HudStateCoordinator never re-emits .idle after the initial promotion
(state changes only fire on intent stream activity), and there's no
'reissue current state on uiReady' path on the Swift side, so the HUD
stayed pinned to its initial .booting render.

Fix: change _pendingHello to _pendingMessages (a FIFO array). Push every
pre-handler non-hello message; on onOutbound() registration, drain the
queue in arrival order. Hello still auto-acks inline so the Swift
handshake can arm before the bundle finishes loading.

Verified: rebuilt, launched, HUD progresses past booting; bus parity
gate green at v2.3.0 across Swift / TS / Injection.js.
2026-05-02 07:02:55 -05:00
james.maes 2bb710d3e6 fix(bus): bump Injection.js placeholder to v2.3.0 + extend parity gate
The WKUserScript at packages/Bus/Sources/Bus/Resources/Injection.js
installs window.jarvisBus at document-start with a hardcoded
`protocolVersion` placeholder. Because the full TS bundle's installer
shape-checks window.jarvisBus and skip-replaces when send/receive/
onOutbound already exist (Vy() in the minified output), the placeholder
IS the version reported in helloAck at runtime. The placeholder had been
stale at 2.0.0 since the first protocol bump (2.0.0 → 2.1.0 → 2.2.0 →
2.3.0), surfacing as runtime 'HUD bundle was built against bus protocol
v2.0.0; the app expects v2.3.0' hard-fails after Phase 9's Plan 04
re-bumped the protocol.

Fix:
- Bump Injection.js's protocolVersion literal to '2.3.0'.
- Extend scripts/check-bus-protocol-version.sh to assert three-way
  parity (Swift Protocol.swift ↔ TS protocol.ts ↔ Injection.js) so
  this drift can never recur silently.

Verified: bus parity OK at v2.3.0; check-app-builds green; runtime
launch with PID 32607.
2026-05-02 06:54:46 -05:00
james.maes 96a8e2da72 feat(09-04): add chatSubmit / chatCancelAndSubmit / submitRejected bus messages + AppDelegate text-input handlers (D-10, D-12, BLOCKER-1, WARNING-4)
Wire layer:
- BusInbound gains chatSubmit(text:) + chatCancelAndSubmit(text:) cases.
- BusOutbound gains submitRejected(reason:) case.
- BUS_PROTOCOL_VERSION 2.2.0 -> 2.3.0 (Swift + TS); fixtures byte-identical.

AppDelegate (chat-panel handlers):
- handleChatSubmit + handleChatCancelAndSubmit dispatch into
  AgentOrchestrator.submit(.text(...)) / cancelAndSubmit(.text(...)) (D-12).
- BOTH handlers call tryPhraseAttachIfMatch BEFORE submit so phrase-matched
  text input arms the FrameAttachController (WARNING-4 closure — previous
  plan only wired chatSubmit).
- BLOCKER-1 user-side: appendUserTextIfRunning appends user text to
  TurnTranscriptStore on .ran/.superseded so MemoryExtractionCoordinator
  finds non-nil pair text.
- handleTextOutcome routes SubmitOutcome.rejected to a Bus toast via
  RejectReasonCopy.body — byte-identical wording to the voice-path HUD
  banner (D-10 single source of truth).

VISION-03 gate adjustment:
- check-presence-vision-isolation.sh Layer 3 no longer flags `cancelAndSubmit`
  in App/ files holding PresenceSignalBus references. Plan 4 legitimately
  introduces orchestrator.cancelAndSubmit at the chat-panel handler call
  site; the SEC-06 invariant (presence enrichment lives outside the wrapper)
  is enforced by AgentOrchestrator.runTurn and proven by
  AgentOrchestratorPresenceEnrichmentTests. TTSEngine* and runTurn (private)
  remain forbidden tripwires.

Tests:
- CodableRoundTripTests adds round-trips for chatSubmit / chatCancelAndSubmit /
  submitRejected; webview round-trip.test.ts mirrors.
- 54/54 Bus tests pass; 37/37 TS bus tests pass; check-bus-protocol-version
  reports parity at v2.3.0.
2026-05-01 22:21:14 -05:00
james.maesandClaude Opus 4.7 87aec282a0 feat(09-02): wire FrameAttachController + Bus frameAttachRequested + broadcaster release subscriber (D-15, D-16, INT-07-04)
- App/Vision/AppDelegateFrameAttachAdapters.swift (NEW): FrameAttachCaptureSourceAdapter wraps CameraCapture, FrameAttachReplaySinkAdapter wraps Replay.ReplayLog (placeholders log via JarvisLogChannel.replay — see file docstring for FK rationale), FrameAttachControllerReplaySinkBridge bridges Vision's FrameAttachReplaySink actor to FrameAttachController.ReplaySink protocol
- AppDelegate: new frameAttachController + frameAttachReleaseTask properties; installVision now constructs FrameAttachController (D-15); installAgent waits for visionInstallTask before constructing the orchestrator and passes visionRouter:; new step 5d frame-attach release subscriber subscribes broadcaster's .frameAttach priority and fires onAssistantTurnComplete() on .turnEnd for image-bearing turns (D-16)
- AppDelegate: tryPhraseAttachIfMatch(_:) helper exposed for Plan 4's chatSubmit + chatCancelAndSubmit handlers (WARNING-4: must call from BOTH paths)
- AppDelegate.installBus: bridge.onInbound now switches on BusInbound; .frameAttachRequested → frameAttachController.requestAttach(reason: .hudButton)
- BusInbound: additive .frameAttachRequested case + Discriminator + Codable arms (NO default — exhaustive-switch drift preventer); BUS_PROTOCOL_VERSION 2.1.0 → 2.2.0 in Swift + TS mirror
- TS protocol: matching frameAttachRequested case + decoder arm; round-trip.test.ts asserts version 2.2.0 + decodes new fixture
- New fixtures (parity): packages/Bus/Tests/BusTests/Fixtures/frameAttachRequested.json + webview/packages/bus/fixtures/frameAttachRequested.json (byte-identical, no trailing newline per check-bus-protocol-version.sh)
- Memory package bumped from .v13 to .v14 (cascade: Memory depends on AgentCore which now requires v14 due to JarvisVision dependency added in Task 1)
- xcodegen regen for the new App/Vision/AppDelegateFrameAttachAdapters.swift source file
- 2 new FrameAttachReleaseSubscriberTests prove broadcaster→onAssistantTurnComplete glue (image-bearing turns release once; text-only turns NEVER release); subscription uses devOverlay priority for the sync wait to avoid competing with broadcaster's drain on orch.events
- 1 new CodableRoundTripTests entry (test_roundTrip_frameAttachRequested)
- Phase 7 SOLE-emission-site invariant intact: FrameAttachDiscardSiteGrepTests passes; check-presence-vision-isolation.sh exits 0; orchestrator-events single-consumer grep gate exits 0; 65 AgentOrchestratorTests + 51 BusTests + 34 webview tests all green
- App target compiles cleanly (check-app-builds.sh exits 0)

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-01 19:41:29 -05:00
james.maes 52457f0159 feat(07-03): in-process MCP tools + BusOutbound.sessionHistory + version bump
- InProcessTool protocol + InProcessToolRegistry actor (PATTERNS 2.18A):
  bypasses stdio framing for memory tools
- SearchMemoryTool (D-07 all-sessions, requiresConfirmation=false)
- SearchConversationTool (TEXT-03 session-scoped, requiresConfirmation=false)
- ForgetFactTool (D-02 destructive, requiresConfirmation=true)
- BusOutbound.sessionHistory(turns: [TurnRow]) additive case +
  hand-written Codable arms; Bus.TurnRow local mirror (no Bus->Memory
  dependency edge introduced)
- BUS_PROTOCOL_VERSION 2.0.0 -> 2.1.0 (MINOR: additive case); TS-side
  catch-up deferred to Plan 07-06 per plan body — check-bus-protocol-
  version.sh xcodebuild pre-script will fail until then (expected)

Bus 50 tests, MCP 85 tests, Memory 67/9 skipped, Replay 27 — all green.
.memoryRetrieval grep gate stays at 1 production hit; .memoryMutation
grep gate stays at 1; no new chat-panel components.
2026-04-29 17:19:59 -05:00
james.maes a5107218e8 test(07-03): add failing in-process MCP tools + Bus session-history tests
- InProcessMemoryToolsTests: dispatch coverage for search_memory,
  search_conversation, forget_fact through InProcessToolRegistry; stub
  dispatchers; requiresConfirmation gate (D-02 forget_fact only);
  unknown-tool error
- SessionHistoryOutboundTests: BusOutbound.sessionHistory round-trip +
  shape ("type":"sessionHistory") + version constant present

RED: tests reference InProcessTool, InProcessToolRegistry,
SearchMemoryTool, SearchConversationTool, ForgetFactTool, plus
BusOutbound.sessionHistory + Bus.TurnRow — none exist yet.
2026-04-29 17:08:05 -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.maes 61ba047068 chore: merge executor worktree (worktree-agent-af785327 / plan 02-04)
# Conflicts:
#	project.yml
2026-04-23 18:11:00 -05:00
james.maes a63d521d38 feat(02-04): add bus parity + HUD-04 lint + harness-parity pre-build scripts
Three build-breaking check scripts + two smoke test harnesses + two fixture dirs:

- scripts/check-bus-protocol-version.sh: greps Swift vs TS BUS_PROTOCOL_VERSION
  constants, diffs the two fixture directories (packages/Bus/Tests/BusTests/
  Fixtures/ vs webview/packages/bus/fixtures/). Fails build on any drift.
- scripts/check-no-evaluate-javascript.sh: scans App/ + packages/ for
  `.evaluateJavaScript(` outside test dirs; HUD-04 architectural lint.
- scripts/check-bus-harness-parity.sh: byte-identical check between
  canonical webview/bus-harness.html and bundle copy (Plan 02-03 provides
  the bundle copy; this script enforces no-drift going forward).
- scripts/test-check-bus-protocol-version.sh + bus-mismatch/ fixture:
  drives the protocol-version checker against a v2.0.0/v1.9.0 mismatch,
  asserts exit 1 + both versions in error text.
- scripts/test-check-no-evaluate-javascript.sh + bus-evaluate-js/ fixture:
  drives HUD-04 lint against bad.swift (evaluateJavaScript) + good.swift
  (callAsyncJavaScript). Asserts bad.swift flagged, good.swift not flagged.
  Fixture is copied to a temp dir to bypass the production */test-fixtures/*
  path-skip rule.

[Rule 3 - Blocking] Normalized Swift-side fixtures to drop trailing newlines.
The 14 JSON files under packages/Bus/Tests/BusTests/Fixtures/ had stray
trailing \n while the TS mirror at webview/packages/bus/fixtures/ did not.
Plan 02-02's SUMMARY documented the no-trailing-newline convention (matches
Swift JSONEncoder default) but Plan 02-01's fixtures were authored via a
text editor that added them. Stripping \n lets check-bus-protocol-version.sh
pass without relaxing the byte-equality invariant. Swift CodableRoundTripTests
remain green (24/24) because JSONSerialization ignores trailing whitespace.
2026-04-23 17:51:56 -05:00
james.maes de59a2d211 feat(02-03): add OutboundBatcher + fill in WebviewBridge outbound path
- OutboundBatcher: actor-based 33ms coalescer (latest-wins audio, concat
  tokens, flush-then-send for state transitions). Task.sleep-based window
  per RESEARCH §HUD-06. Order preserved on flushAndSend.
- WebviewBridge.sendRaw(_:): real callAsyncJavaScript call with primitive
  string payload argument (HUD-04 / T-02-13 — never concat-into-body).
- WebviewBridge.send(_:): gates on handshakeState == .armed, else throws
  BusError.bridgeNotReady; delegates to sendRaw.
- WebviewBridge.startHandshake(): now actually sends the hello frame via
  sendRaw (bypasses the armed gate — hello is what STARTS the handshake).
- WKUserScript at .atDocumentStart in JarvisBusWorld installs
  window.jarvisBus stub before any page script runs (pitfall 3).
- JSEvaluator protocol seam: WebviewBridge holds an evaluator, production
  uses WKWebViewJSEvaluator (WKWebView wrapper); tests use a recording
  fake so the outbound suite doesn't need a real WebKit process.
- WebviewBridge: OutboundBatcher.Sink conformance — sendRaw already
  matches the Sink signature.

Tests: 11 new (5 Batcher + 6 Outbound), 47 total all green.
Invariants: 0 .evaluateJavaScript( call sites in packages/Bus/Sources/;
1 webView.callAsyncJavaScript call site (in WKWebViewJSEvaluator).
2026-04-23 17:48:40 -05:00
james.maes f7f87d8dba test(02-03): add failing OutboundBatcher + outbound WebviewBridge tests
RED phase — these tests reference APIs that do not yet exist:
- OutboundBatcher actor (postAudio, postToken, flushAndSend)
- WebviewBridge.JSEvaluator protocol + internal test-only init
- WebviewBridge WKUserScript injection resource

Adds packages/Bus/Sources/Bus/Resources/Injection.js + declares
resources: [.process("Resources")] on the Bus target so the bundle
contains the document-start stub that GREEN will load.
2026-04-23 17:43:03 -05:00
james.maes a555093865 feat(02-01): add WebviewBridge + Handshake state machine
- `Handshake.swift` — `HandshakeState` enum (`.idle` → `.sentHello` → `.armed`
  / `.mismatched` / `.timedOut`) + `HandshakeTiming.timeout = .seconds(2)`.
- `WebviewBridge.swift` — `@MainActor final class` conforming to
  `WKScriptMessageHandlerWithReply` with `MainActor.assumeIsolated` to read
  `WKScriptMessage.body` under Swift 6 strict concurrency (Apple DevForums
  751086). Installs its handler in `WKContentWorld.world(name: "JarvisBusWorld")`
  so page scripts cannot observe or post to the bus.
- Inline handshake handling: `helloAck` matching `BUS_PROTOCOL_VERSION` arms
  the bridge; mismatch or 2s timeout flips to terminal state and invokes an
  injectable `alertPresenter` (NSAlert-free under test). `onInbound` is never
  called for `helloAck`.
- Internal `handleInboundString(_:replyHandler:)` seam lets tests exercise
  the decode path without synthesizing `WKScriptMessage` (not constructible
  from user code).
- `send(_:)` is a stub that serializes via `BusCoder` but raises
  `BusError.bridgeNotReady` until armed. Plan 03 swaps the stub body for a
  real `callAsyncJavaScript` wiring.
- 5 HandshakeTests + 7 WebviewBridgeTests cover: match/mismatch/timeout/
  cancel-on-arm transitions, decode-success dispatch, decode-failure reply,
  non-string body reply, onInbound-throws reply, nil-return implicit success,
  handler registration. All green (36 total Bus tests pass).
2026-04-23 17:31:22 -05:00
james.maes cf61b6bac8 feat(02-01): add Bus package with discriminated-union Codable + fixtures
- New `packages/Bus` SPM package (Swift 6 strict concurrency, WebKit linked).
- `BUS_PROTOCOL_VERSION = "2.0.0"` + shared `HudState` / `TurnTerminator` /
  `BusCoder` in `Protocol.swift`.
- `BusOutbound` (8 cases) + `BusInbound` (2 cases) with hand-written
  `Codable` using a `type` discriminator and exhaustive switches — SE-0295
  synthesized `{"case": {...}}` shape is rejected by design.
- `BusReply` plist-serializable reply + `BusError` error surface.
- 14 JSON fixtures + 24 XCTest round-trip tests asserting byte-equal
  re-encoding, shape invariants, UUID-lowercasing, and negative-path decode
  failures. All green.
- `JarvisLogChannel` gains `case bus` (OBS-06 extension per RESEARCH-07);
  `LoggingTests.test_fiveChannelsAreDefined` updated to match.
2026-04-23 17:23:02 -05:00