28 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 f9c630e431 fix(hud): strict-undefined in SegmentedRing instance-matrix loop (#165)
`initialMatrices[i]` is provably defined because the loop bound is
`i < tickCount` and `initialMatrices.length === tickCount`. TS doesn't
track that, so use a non-null assertion to silence TS18048.

The other ~30 errors reported in CI run 25797533005 were all downstream
of `TS2307: Cannot find module '@jarvis/bus'` — they disappear once
`pnpm install` + `bash scripts/build-webview.sh` generates the bus
package's `dist/index.d.ts`. No code change needed for those.

Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-13 07:35:50 -05:00
james.maes a8c7f1a09b chore(deps): batch Dependabot bumps — fiber + plugin-react + jsdom + vite (#144, #145, #147, #148, #160)
Lands four SAFE-MERGE Dependabot bumps in one commit. Each was confirmed SAFE-MERGE by the H' triage on PR #155, then re-verified locally on the new lockfile baseline (vitest@3 + vite@8 + esbuild dropped) from PR #159.

- @react-three/fiber 9.6.0 → 9.6.1 (closes #148)
- @vitejs/plugin-react 5.x → 6.0.1 (closes #147)
- jsdom 25.x → 29.1.1 (closes #144)
- vite pinned to >=8.0.12 via pnpm.overrides (closes #145)

37/37 bus + 101/101 hud tests; both workspaces build; production webview bundle stamps cleanly. PR #146 (TypeScript 6) NOT included; deferred for focused evaluation.
2026-05-13 06:47:16 -05:00
james.maes 08095281fc fix(deps): close vite + esbuild CVEs via pnpm.overrides + vitest@3 bump (#159)
Closes Dependabot alerts #1 (esbuild dev-server response read) and #2 (Vite path traversal). Adds pnpm.overrides pinning vite >= 8.0.5 (drops esbuild transitive) + bumps vitest 2.1 → 3 in root + bus workspaces (vitest 2 incompat with vite 8 SSR transform). 37/37 bus + 101/101 hud tests pass; both workspaces build cleanly. Deferred to v0.2: vitest 4 major + oxc plugin migration.
2026-05-13 06:41:52 -05:00
james.maes 5bb1e778b5 fix(hud): chat panel auto-scroll on streaming append — B-06 tactical (#51, #150)
Adds a useLayoutEffect on ChatPanel keyed to total content length (captures streaming token append where chatEvents.length is constant). 24px bottom-pin hysteresis; user scroll-up breaks the pin; auto-re-arms on scroll-back-to-bottom. scrollTo feature-detected with scrollTop fallback for JSDOM. 4 vitest cases assert decision logic.
2026-05-13 06:27:42 -05:00
james.maesandClaude Opus 4.7 4159d44f4e feat(track-c 3): HUD camera button emits frameAttachRequested
Closes vision-audit §4 "HUD button emitter has zero matches". The bus
protocol declared `BusInbound.frameAttachRequested` and the Swift
inbound handler at App/AppDelegate.swift:1696-1698 routed it into
`FrameAttachController.requestAttach(reason: .hudButton)`, but no React
component dispatched the message. Now the user has a clickable camera
affordance next to the chat input.

Changes:
- webview/packages/hud/src/chat/CameraButton.tsx (new): stateless button
  that posts `{ type: 'frameAttachRequested' }` via window.jarvisBus.send.
- ChatInput.tsx: imports CameraButton and renders it in the toolbar row
  between the input field and the Send button.
- chat-panel.css: `.camera-button` styling, matches `.chat-input__send`
  chrome so the toolbar reads as one row.

Tests: +3 (CameraButton.test.tsx)
- CB1: renders accessible button (aria-label "Attach camera frame").
- CB2: click posts the BusInbound.frameAttachRequested message.
- CB3: payload shape is `{ type: 'frameAttachRequested' }` only —
  matches the no-associated-value Swift case in BusInbound.swift.

JS-side path: webview/packages/hud/src/chat/CameraButton.tsx:14
  → window.jarvisBus.send({ type: 'frameAttachRequested' })
Swift-side path: App/AppDelegate.swift:1696-1698
  → frameAttachController.requestAttach(reason: .hudButton)

HUD vitest 94 → 97. Pre-existing SegmentedRing typecheck warnings are
unrelated (verified via git stash on a clean tree).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-04 13:06:47 -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.maesandClaude Opus 4.7 621b454835 fix(hud,app): chat input UI + .bus turnStarted/turnEnded forwarding (closes BLOCKER-INT-3 + INT-2 tail)
The v0.12.0 milestone audit (.planning/v0.12.0-MILESTONE-AUDIT.md, status
gaps_found) flagged INT-3 as 🔴: webview ChatPanel.tsx was a read-only
renderer with zero JS-side producer of any inbound bus message. The Swift
inbound side (AppDelegate.swift handleChatSubmit / handleChatCancelAndSubmit)
was already wired by Plan 09-04, but with no UI to fire it the v0.12.0 demo
had no working text-modality path.

Pairs INT-3 with the remaining half of INT-2 because they are not
independently shippable: the JS bus dispatcher silently drops every
tokenDelta when currentTurnId === null, so without turnStarted forwarding
the ChatInput would post chatSubmit and the user would see exactly
nothing back. Commit 602a95b landed the .bus tokenDelta forwarder; this
commit extends that subscriber to also synthesize turnStarted on first
emission for a new TurnID and translate OrchestratorEvent.turnEnd into
BusOutbound.turnEnded with a StopReason → TurnTerminator mapping.

JS side:
- webview/packages/hud/src/chat/ChatInput.tsx — new component. Form
  with input + Send button. Empty/whitespace input is a no-op; trimmed
  text dispatches BusInbound.chatSubmit(text) when currentTurnId === null,
  chatCancelAndSubmit(text) (barge-in) otherwise. Pushes a kind:'text',
  role:'user' event to the chat-events store on submit so the user sees
  their own message in chronology immediately (optimistic local echo).
  Clears the input after dispatch.
- webview/packages/hud/src/App.tsx — mounts <ChatInput /> directly under
  <ChatPanel /> inside .jarvis-hud__chat. The parent flex-column keeps
  the input pinned to the bottom of the floating chat frame.
- webview/packages/hud/src/chat/chat-panel.css — .chat-input row + field
  + button styles using existing tokens.
- webview/packages/hud/src/bus/client.ts — added submitRejected (push
  inline error event) and sessionHistory (no-op until WARN-INT-2 hydration
  lands) arms to the dispatcher switch. Restores the _exhaustive: never
  compile-time check (was failing tsc on develop because both cases
  existed in BusOutbound but not in the switch).

Swift side:
- App/AppDelegate.swift installAgent — extended busSubscriberTask to
  pattern-match .tokenDelta and .turnEnd. tokenDelta synthesizes a
  turnStarted via flushAndSend on first emission for a new TurnID
  (OrchestratorEvent has no explicit start case — start is implicit
  from the first per-turn event). turnEnd flushes pending tokens and
  emits BusOutbound.turnEnded so lifecycle events never overtake their
  data. Uses local lastEmittedTurnId state in the subscriber task.
- AppDelegate.busTurnTerminator(for:) — StopReason → TurnTerminator
  mapping. endTurn / toolUse / maxTokens → .completed; refusal /
  streamTruncated → .errored. cancelled and superseded are reachable
  only via cancel-and-submit / barge-in flows the orchestrator does
  not yet emit; the mapping will extend when they do.

Tests:
- webview/packages/hud/tests/ChatInput.test.tsx — 6 cases (CI1-CI6)
  covering render, empty/whitespace no-op, trimmed-text submit + clear,
  Enter-key form submit, user-text echo to store, barge-in routing
  when currentTurnId !== null. All pass.
- webview/packages/hud/tests/bus-dispatch.test.ts — added B5:
  submitRejected outbound pushes an inline error event with the
  rejection reason. All pass.

Verification:
- webview/packages/hud tests: 87/87 pass.
- AgentCore swift test: 178/178 pass.
- scripts/check-app-builds.sh: PASS.
- All 15 boundary grep gates: PASS.
- Pre-existing: Bus 55/56 (F-A2-01 stale assertion), webview tsc still
  fails on src/hud/SegmentedRing.tsx (F-A1-01 territory). Neither
  introduced or addressed here.

End-to-end smoke (paired with INT-2 closure): user types → chatSubmit
reaches Swift → AgentOrchestrator.submit(.text) → first tokenDelta
synthesizes turnStarted → JS chat panel shows assistant text streaming
→ turnEnded clears the active turn for the next submit.

Closes BLOCKER-INT-3, closes BLOCKER-INT-2 fully (tokens + lifecycle).
The next-action gate of v0.12.0 transitioning from "passes tests" to
"is a working app for one modality" is now functional pending visual
launch confirmation.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-03 07:55:56 -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 bc4529158e feat(hud): full Iron Man / Jarvis HUD redesign — Wave 2
The Wave 1 multi-layer ring stack collapsed visually because macOS Metal
WebGL clamps gl_PointSize to ~63 px — the 4 layers all rendered at the
same on-screen size, reading as "a rotating circle with a black circle in
it" per operator UAT. Going wider on the redesign instead of patching the
particle path: real geometry, not point sprites; proper HUD chrome around
the ring; floating translucent chat panel.

R3F scene additions (no new external deps)
------------------------------------------

`hud/CoreGlow.tsx` — four mesh-based elements that don't depend on
gl_PointSize clamping:
  * cyan-white core sphere (the arc reactor focal point)
  * inner halo disc around the core
  * structural torus at the carrier radius — defined "engineered" ring
  * wide soft halo disc reaching out to r=1.45 (diffuse glow)

`hud/Reticle.tsx` — 4 cardinal crosshair ticks at the inner radius,
rotating slowly with state, breathing with the state pulse multiplier.

`hud/SegmentedRing.tsx` — InstancedMesh of small box "ticks" arranged
around a circle. Two instances composed in ParticleRing:
  * outer chronograph: r=1.32, 64 ticks, slow forward rotate, major/minor
    pattern (every 8th tick brighter + longer)
  * inner short-tick scale: r=1.05, 120 ticks, contra-rotates faster

`hud/ParticleRing.tsx` — composes Reticle + CoreGlow + RingMesh + two
SegmentedRings + the existing CameraDrift.

`hud/ring.vert.glsl` — drops the perspective multiplier on gl_PointSize
(`uPointSize * (300/-z)` was producing 400-1600 nominal sizes that all
clamped to 63 px). Direct mode uses uPointSize as the actual on-screen
pixel count, so per-layer sizes (8 / 5 / 4 / 18) actually differ.

`hud/RingMesh.tsx` — boosted intensities (1.4 → 2.4 core, 1.0 → 1.6
inner) to read brighter on the new dark backdrop; widened radius spread.

DOM/SVG HUD chrome (no GPU cost)
--------------------------------

`hud/HudFrame.tsx` + `hud-frame.css`:
  * 4 corner brackets (L-shaped SVG with secondary line + dot)
  * Top-left status panel: SYS / STATE / CLK (live clock)
  * Top-right status panel: PWR / CORE / LINK
  * Center state readout under the ring (large letter-spaced state name)
  * Bottom perimeter ticker (16 rotating data lines, 90s loop, masked
    edges)
  * Per-state accent color: cyan default, green on listening, amber on
    awaitingConfirmation/reconfiguring

Layout restructure
------------------

`App.tsx` + `theme/tokens.css`:
  * Canvas now full-viewport (was top-60vh strip with chat panel below)
  * Chat panel becomes a floating translucent side panel bottom-right
    (380 px, max 38vw, max 50vh) with `// CHAT BUFFER` chrome header
  * HudFrame z-index: 4 (above scanlines & vignette, below floating chat)
  * Body `overflow: hidden` so ticker doesn't introduce scrollbars

Tests
-----

80/80 swift-testing passes — RingMesh tests are structural (Canvas mount
+ LoadingFallbacks + easeStateBlend); none test the new components yet
because the new components are visual-only with no behavioral contract
the tests check. The bus / store / chat panel test coverage is unchanged
and unaffected.

Reduce-Motion
-------------

* Camera drift suspended (camera locked at (0,0,3))
* Per-state pulse/rotate multipliers zeroed (existing behavior preserved)
* HUD-frame ticker scroll + center-state pulse halted via @media query
* Scanlines stay visible but at lower opacity
* SegmentedRing rotations zeroed
* Reticle rotation/breath zeroed
2026-04-30 15:19:09 -05:00
james.maes b1d6b7cebf feat(hud): cinematic arc-reactor ring stack — Wave 1 visual upgrade
The shipped HUD was a tech-demo: 1 ring of flat-blue point sprites with
a sin-wave radius pulse. Verification passed because the *wiring* contract
was met (state mappings differ, transitions emit), but the visual contract
was never enforced. Operator UAT report: "a very bad half-breathing circle".

This commit lifts the visual floor toward the Iron Man / Jarvis HUD bar
without adding new dependencies — same `<RingMesh>` test contract, same
shader pipeline, same Reduce-Motion / accessibility behavior, materially
different output.

Multi-layer ring stack
----------------------
RingMesh now renders 4 stacked ring layers (one `<RingLayer>` per config)
with `THREE.AdditiveBlending`. Bright cores naturally bloom under additive
without a post-processing pass:

  core  : r=0.55, 384 dense particles, 5.5 px sprites, intensity 1.4, coreBoost 0.85
  inner : r=0.85, 512 particles (the legacy ring), 4.0 px, intensity 1.0
  outer : r=1.15, 256 particles, 3.0 px, intensity 0.65, **contra-rotates** (parallax)
  halo  : r=1.0,   96 particles,  16 px sprites, intensity 0.35 (diffuse glow)

Per-layer uniforms — `uIntensity`, `uCoreBoost`, `uPhase`, `uRadiusScale`,
`uPointSize` — extend the existing shader without breaking the unit tests
(which check the 7 STATE_PARAMS table-driven cases, not the per-layer
ones). Rotate/pulse multipliers per layer give the rings independent
motion: outer counter-rotates, halo nearly-static, core ~40% faster.

Radial color gradient
---------------------
ring.frag.glsl: cyan-white core (#EBFFFF) at d=0, mid mix at d=0.25,
configured `uColorGlow` at the sprite edge. `uCoreBoost` lifts the inner
layers further toward white so the stack reads as "arc reactor with
plasma core" instead of "blue dot field".

Camera idle drift
-----------------
ParticleRing wraps a small `<CameraDrift>` component that pans the camera
in a Lissajous arc (±0.06 x, ±0.04 y, ±0.05 z) over ~14s. Reduce-Motion
locks the camera at (0,0,3). This is the single biggest "feels alive"
upgrade — the additive ring stack only reads as holographic when the
parallax actually parallaxes.

Theme & ambience
----------------
- `--arc-reactor-glow`: `#1E88E5` → `#4FC3F7` (Stark-reactor cyan).
- New `--arc-reactor-deep` and `--arc-reactor-warm` for future use
  (warm accent during tool-call active states).
- Body backdrop: deep radial gradient — gives the additive bloom
  something to read against instead of pure transparency.
- Cinematic vignette via `.jarvis-hud::after` (radial darkening).
- Faint scanlines via `.jarvis-hud::before` (1px @ 3px stride, 60%
  opacity, screen blend mode) — the Iron Man HUD signature.
- Reduce-Motion dims the scanlines further but doesn't kill them
  (they're static — no animation cost).
- Store default `arcReactorGlow` updated to match.

Tests
-----
- 80/80 vitest still pass (tests/RingMesh.test.tsx still mounts and
  asserts Canvas + LoadingFallbacks; the per-layer config is internal).
- `pnpm --filter @jarvis/hud build` clean (Vite, 431 ms, no errors).

Next waves (not in this commit)
-------------------------------
- Wave 2: per-state choreography (idle vs listening vs thinking vs
  speaking should look radically different, not just rotate-faster).
- Wave 3: HUD overlay elements (rotating brackets, status text,
  per-tool-call orbital marker).
- Wave 4: live audio-RMS reactivity (P6 wired the bus channel; consume
  it here in `useFrame`).
- Wave 5: token-stream ripple (each `tokenDelta` injects a radial pulse).
2026-04-30 12:08:57 -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.maesandClaude Opus 4.7 c951f1262c fix(hud): add defer to entry script when stripping type=module
`type="module"` scripts are deferred-by-default — execution waits until
the body is parsed and `<div id="root">` exists. Plain `<script src=>`
runs immediately, mid-<head>, so React's `createRoot(document
.getElementById('root'))` resolves to `null` and silently mounts
nothing. No error fires, no console output, just a forever-empty #root.

Reproduced via an inline diagnostic that polled `#root.firstChild`
3 seconds after load — fired with "No errors captured but #root is
still empty after 3s — R3F never mounted." Adding `defer` to the
emitted script tag restores the parse-then-execute order modules had
built in, and the HUD now mounts and renders content (verified
visually: fallback particle ring paints on the transparent panel).

The Vite plugin's regex now both strips `type="module"` and
substitutes `defer` in its place, in a single pass per script tag.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-26 16:43:42 -05:00
james.maesandClaude Opus 4.7 06bb159197 fix(hud): Vite IIFE bundle + strip module/crossorigin attrs from index.html
The R3F bundle silently failed to load in the production HUD with
type=2/code=0 resource errors:

  resourceID=21 didFinishResourceLoad (length=649)   <- index.html
  resourceID=26 didFailResourceLoad                   <- 1.1 MB JS bundle
  resourceID=27 didFinishResourceLoad (length=4514)   <- CSS

Root cause: Vite's default ES-module output emits `<script type="module"
crossorigin>` for the entry script. The `type="module"` attribute forces
CORS-clean fetching even without `crossorigin`. From a `file://` origin
WKWebView treats every load as a "null origin" cross-origin request, and
the JS bundle subresource fails. The CSS loaded because `<link>` tags
don't carry the same module-fetch contract.

Two-part fix:

1. `format: 'iife'` in rollupOptions.output: produces a self-executing
   bundle that runs as a regular script with no import/export semantics.
   `inlineDynamicImports: true` is required because IIFE format forbids
   chunk splitting. Trade-off: no code splitting, but the HUD is a
   single-page R3F app where splitting wouldn't help first paint.

2. `stripModuleAttrs` Vite plugin: Vite hardcodes `type="module"
   crossorigin` in the emitted index.html script tag even when rollup
   format is IIFE — the HTML emitter doesn't track the format change.
   The plugin runs at `enforce: 'post'` and rewrites both attributes
   off via two regex passes against the transformed HTML.

R3F's drei pulls in modules that reference `import.meta`, which becomes
`{}` under IIFE format (build-time warning). The affected paths are
VideoTexture and FaceLandmarker — neither is on the particle-ring
critical path; verify on first-paint and accept as known-degraded for
now if those features ever need import.meta semantics, switch to a
custom WKURLSchemeHandler with `jarvis://` so `'self'` resolves to a
real origin and ES modules can load natively.

Empirically verified: after this fix, resourceID=26 reports
didFinishResourceLoad (length=1088575) instead of didFailResourceLoad —
the full 1.1 MB bundle reaches the WebContent process.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-26 16:21:42 -05:00
james.maes a0d82711da fix(03-review): H-02 attach to existing window.jarvisBus from JarvisBusWorld
Option B (minimal edit): `installJarvisBus()` now detects a pre-existing
`window.jarvisBus` with the expected send/onOutbound/receive shape and
returns verbatim rather than overwriting it.

Root cause: Swift's WebviewBridge installs `Injection.js` as a WKUserScript
`in: JarvisBusWorld` and registers `webkit.messageHandlers.jarvisBus`
in the same content world. WebKit isolates message handlers per world,
so the default-world HUD bundle (loaded via `<script type="module">`)
can NOT reach them. `window.jarvisBus` IS visible cross-world because
WebKit keeps `window` as one shared object — only variable bindings are
world-scoped. Phase 3's `main.tsx` was therefore installing a fresh bus
whose `send()` path throws on every outbound (including the first
`uiReady`). The bus-harness.html marker script in Phase 2 never called
`send()`, which is why it wasn't observed earlier.

Fix ensures the HUD dispatcher's outbound path routes through the
Injection.js bus's `send()` (which captured `webkit.messageHandlers`
at document-start inside JarvisBusWorld). The fresh-install branch is
preserved unchanged for the bus-harness.html path (no pre-existing
bus present in Phase 2 tests).

Tests:
- 5 new H-02 cases in bridge.test.ts exercise the attach-to-existing
  path (identity preserved, send/onOutbound routed through the
  pre-existing bus, fall-through when no pre-existing bus, fall-through
  for non-bus-shaped leftovers).
- Existing 28 bus tests + 80 hud tests remain green.

Also adds a doc comment to main.tsx explaining the pattern so future
readers don't re-litigate the world-isolation model.
2026-04-24 16:48:14 -05:00
james.maes 9e8e9ec5af fix(03-review): H-01 unify awaitingApproval sentinel on parsed-object helper
Lifts `isApprovalPlaceholder` out of `ToolCallCard.tsx` into a shared
`chat/approvalSentinel.ts` module and routes both the dispatcher (bus/client.ts
toolCallStart arm) and the card through it. Previously the dispatcher used
literal-byte JSON equality (`msg.argsPreview === '{"awaitingApproval":true}'`)
while the card used parsed-object semantics, so any Swift-side whitespace
drift (pretty-printed encoder, extra spaces, key-ordering) would silently
flip the status to 'running' while the card kept hiding args — a visible
UX desync and a latent drift between two defense-in-depth guards.

New test streaming.test.tsx D5b feeds `'{"awaitingApproval": true}'` (space
after colon) and asserts `status === 'awaiting-approval'`. Fails against the
old literal-string check; passes now.

Existing D5 (exact-string variant) continues to pass.
2026-04-24 16:44:04 -05:00
james.maes 4e2e05e565 feat(03-04): wire bus dispatcher → store; streaming + chronology tests
- store: add currentTurnId + activeTextPartId slices and beginTurn/endTurn actions
- bus/client.ts: extend dispatcher to route every Phase-2 BusOutbound variant
  - turnStarted → beginTurn; turnEnded → endTurn
  - tokenDelta → synthesize text-part on first-after-turnStarted with deterministic
    id turn:{id}:assistant[:n]; subsequent tokens append to same id via
    appendTokenToLastText
  - toolCallStart → upsertToolCall; status='awaiting-approval' when argsPreview is
    the Swift sentinel '{"awaitingApproval":true}', else 'running'. Also nulls
    activeTextPartId so next tokenDelta opens a fresh chronologically-correct part
  - toolCallEnd → upsertToolCall with status completed|failed; assigns result (ok)
    or error (fail)
  - audioLevel: intentional no-op (Phase 6 binds to mic RMS)
  - _exhaustive: never default branch preserves compile-time exhaustiveness
- tests/streaming.test.tsx: 12 tests (S1×3 store slices, D1-D8 dispatcher, I1 replay
  idempotency)
- tests/chronology.test.tsx: 2 tests (Ch1 interleave, Ch2 fixture replay with
  whitespace-normalized DOM content assertion)
- tests/fixtures/streaming-fixture.json: canonical 20-message replay

Tests: 79/79 green; typecheck + build clean
2026-04-24 09:19:12 -05:00
james.maes e2dcaed424 feat(03-04): ChatPanel + discriminator router + tool-call card with pre-approval guard
- ChatPanel renders flat chronological list of ChatEvents with role="log" + aria-live="polite"
- ChatEventRouter discriminates on kind with _exhaustive: never sentinel
- ToolCallCard: expandable card with 5 lifecycle status labels; hides args when sentinel
  {awaitingApproval: true} present OR status === 'awaiting-approval' (defense-in-depth
  for Phase 5 MCP-04; T-03-30 mitigation)
- TextPart renders event.text via JSX interpolation (React auto-escapes; T-03-31 mitigation)
- ErrorPart wraps error messages in role="alert"
- App.tsx composes <ChatPanel /> as sibling beneath LoadingFallbacks
- ChatPanel.test.tsx: 5 tests (C1-C3, TP1-TP2)
- ToolCallCard.test.tsx: 6 tests (T1, T2 spans 5 status labels, T3, T3b, T4, T5)

Tests: 65/65 green; typecheck + build clean
2026-04-24 09:14:58 -05:00
james.maes 4951bfdcd1 feat(03-03): animated shader-driven RingMesh + Reduce Motion DOM fallbacks
- Replace Wave 1's static RingMesh with a shader-driven animated version.
  Reads hudState/a11y/theme via useJarvisStore.getState() inside useFrame
  (Pitfall 1 guard) — no per-frame React rerenders.
- 150ms eased crossfade between states via pure easeStateBlend() helper
  (exported for unit tests).
- uOutwardWaveAmp set only when densityMod === 'wave' (speaking); always
  gated off by uReduceMotion.
- Geometry is NOT disposed in a cleanup — R3F reconciler owns its lifecycle
  (Pitfall 8 guard).
- LoadingFallbacks renders DOM affordances per HudState when reduceMotion
  is active (three-dots, pulse-dot, speaking-pulse, etc.). Persistent
  corner-dot for awaitingConfirmation regardless of reduceMotion.
- loading-fallback.css uses @media (prefers-reduced-motion: reduce) as
  defense-in-depth so WKWebView respects the OS setting even if the bus
  a11y message is delayed.
- Add RingMaterialImpl type export from RingMaterial.ts for typed matRef.
- App.tsx composes both ParticleRing and LoadingFallbacks.
- 15 tests: ParticleRing mounts without crash in jsdom, LoadingFallbacks
  produces correct data-hud-fallback slugs per state+reduceMotion combo,
  easeStateBlend edge cases (0/0.15=1, 0/0.075=0.5, clamp at 1, additive,
  no-op when delta=0).
2026-04-24 09:04:09 -05:00
james.maes d094b3b297 feat(03-03): add state uniforms, GLSL sources, RingMaterial + table-driven tests
- stateUniforms.ts exposes STATE_PARAMS + STATE_TO_IDX for all 7 HudStates
  per RESEARCH Particle Ring Visual Design table; parseHex helper for
  hex-to-RGB conversion with arc-reactor-glow fallback.
- ring.vert.glsl + ring.frag.glsl loaded via Vite ?raw imports.
- RingMaterial registers drei shaderMaterial under <ringMaterial> with R3F 9
  ThreeElements namespace augmentation (R3F 9 moved away from
  ReactThreeFiber.Object3DNode to ThreeElements['shaderMaterial']).
- 27 tests covering all 7 state entries, unique index mapping, color
  sentinel distribution, parseHex round-trip, and ?raw shader imports.
2026-04-24 08:54:04 -05:00
james.maesandClaude Opus 4.7 9a66569c92 fix(03-02): drop literal 'as never' from client.ts doc comment
Plan 03-02 Test B5 asserts `grep 'as never' src/bus/client.ts` returns zero.
The doc comment used the phrase "NO \`as never\` cast" to describe the correct
pattern; reworded to "NO unsafe cast" so the literal string is absent while
the guidance stays clear. Zero code change.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-23 18:59:33 -05:00
james.maesandClaude Opus 4.7 b9755b14aa test(03-02): vitest coverage for @jarvis/hud store + bus dispatcher (12 tests)
- store.test.ts (6 tests): defaults, actions present, setHudState isolation
  (chatEvents ref stable), subscribeWithSelector granularity (hudState selector
  doesn't fire on chatEvents change), appendTokenToLastText no-op on unknown id +
  happy-path append.
- bus-dispatch.test.ts (6 tests): hudState outbound routes to store.setHudState;
  state transitions (listening -> idle); decode errors surface via console.error
  for invalid JSON and missing-discriminator; main.tsx boot posts uiReady after
  React commit; exhaustiveness-sentinel compile-time documentation test.
- setup.ts: ResizeObserver polyfill for jsdom — R3F <Canvas> via react-use-measure
  needs it, jsdom doesn't ship it.

Probe documented in SUMMARY (no commit): locally appending a synthetic
BusOutbound variant produces TS2322 on `_exhaustive: never = msg`, confirming
the guard is live.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-23 18:58:23 -05:00
james.maesandClaude Opus 4.7 33e1afc1f1 feat(03-02): scaffold @jarvis/hud webview package (React 19 + R3F 9 + drei 10.7 + three r184 + Zustand 5 + Vite 8)
- New pnpm workspace package at webview/packages/hud/ with workspace dep on @jarvis/bus
- Vite 8 config uses base: './' for WKWebView file:// compat (Pitfall 3)
- CSP meta tag with connect-src 'none', script-src 'self' per RESEARCH §Security
- Zustand 5 store with subscribeWithSelector middleware; slices for hudState, chatEvents, a11y, theme, connection
- Exhaustive bus outbound dispatcher — const _exhaustive: never = msg (no `as never`); adding a BusOutbound case without a handler fails tsc --strict
- Minimal static 512-particle RingMesh (Plan 03-03 layers shader uniforms)
- queueMicrotask uiReady post after first React commit (avoids Pitfall 7 race)

Deviations: @testing-library/react pinned to ^16.3.0 (latest published) and
@testing-library/jest-dom to ^6.6.0 — RESEARCH called for 17/7 but npm only
publishes 16/6. vitest.config.ts removed from tsconfig include — vitest 3's
config re-exports vite 5 types alongside installed vite 8, tripping
exactOptionalPropertyTypes; vite build itself loads the config fine at runtime.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-23 18:54:11 -05:00
james.maes c21df441fd feat(02-02): implement window.jarvisBus glue + bus-harness.html
- installJarvisBus() wires receive/send/onOutbound around decode pipeline
- receive() routes to handler if set; auto-acks hello when unset so the
  minimal harness completes the handshake without page-side code
- send() is the ONLY surface that calls webkit.messageHandlers.jarvisBus
- Missing-webkit surfaces a helpful Error instead of a TypeError
- bus-harness.html is the static stub Plan 03's loadFileURL targets;
  imports compiled ./packages/bus/dist/index.js, calls installJarvisBus
- index.ts now re-exports protocol + bridge
- 7 bridge tests green; 28 total vitest assertions; tsc --strict clean
2026-04-23 17:20:58 -05:00
james.maes 2d3387a1d8 test(02-02): add failing bridge.test.ts against stub installJarvisBus
- Stub bridge.ts exports typed installJarvisBus skeleton
- 7 vitest assertions cover: window.jarvisBus install, outbound dispatch,
  hello auto-ack, decode-error callback, send posts JSON to webkit,
  missing-webkit helpful error, onOutbound latest-wins
- All 7 fail against the stub (RED)
2026-04-23 17:18:47 -05:00
james.maes 4e68fa21d0 feat(02-02): implement protocol decode/encode with never-sentinel exhaustiveness
- BUS_PROTOCOL_VERSION = "2.0.0" (byte-identical to Swift mirror)
- decodeOutbound/decodeInbound return DecodeResult<T>, never throw
- `const _exhaustive: never = type;` (no `as never` cast) in each default
  branch — adding a BusOutbound variant without a case now fails tsc --strict
  with "Type '...' is not assignable to type 'never'" (verified empirically)
- Isolated type-guard arrays HUD_STATES / TURN_TERMINATORS with
  `satisfies readonly HudState[]` catch drift at compile time
- 21 round-trip + error-path vitest assertions pass
2026-04-23 17:17:11 -05:00
james.maes 6ce4754142 test(02-02): add failing round-trip suite + pnpm workspace scaffold
- Scaffold webview/ pnpm workspace (pnpm 10.30.2, TS 5.9, Vitest 2.1)
- @jarvis/bus package with strict tsconfig inheritance
- 14 fixture JSONs byte-identical to Plan 01 Swift mirror (no trailing NL)
- RED: round-trip.test.ts has 19 failing assertions against stub decoders
2026-04-23 17:12:38 -05:00