Commit Graph
17 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 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 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 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 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