41 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 439e1ee2f5 ci: build hygiene — webview cleanup + OpenSSF Scorecard (#46, #178) (#194)
* ci(build-webview): sweep stale hashed chunks before rsync (#46)

Vite emits content-hashed asset names (e.g. `index-CXPb0I5z.js`), so the
active bundle filename rotates on every source/dependency edit. The
existing rsync runs without `--delete` (to preserve `bus-harness.html`
and `.gitignore`), so old chunks accumulated in
`App/Resources/webview/assets/` — observed 9 stale `index-*.js` /
`.js.map` pairs locally before this fix.

Add an explicit `rm -f` of the asset glob (JS + JS map + CSS + CSS map)
before the rsync drops the fresh bundle. The preserved files
(`bus-harness.html`, `index.html`, `.gitignore`) live one level up in
`$DEST`, not in `$DEST/assets`, so the rm cannot reach them.

Verified locally: `bash scripts/build-webview.sh` now leaves exactly one
`index-*.js` + `.js.map` in `assets/` (matching the reference in
`index.html`).

Refs #46

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

* ci(scorecard): add OpenSSF Scorecard workflow + README badge (#178)

Adds `.github/workflows/scorecard.yml` running `ossf/scorecard-action@v2.4.0`
on:
- Weekly cron (Mondays 06:00 UTC) for posture drift detection
- Push to develop for prompt regression visibility
- workflow_dispatch for ad-hoc runs

Permissions follow the canonical Scorecard set (`id-token: write`,
`contents: read`, `security-events: write`) with top-level `read-all` and
job-scoped writes. Results are published to the OpenSSF public dataset
(powers the README badge URL) and uploaded as SARIF to code-scanning.

The workflow consumes no untrusted user inputs in `run:` blocks, so it is
not exposed to workflow-injection patterns documented in
github.blog/security/.../how-to-catch-github-actions-workflow-injections.

README.md gains the standard Scorecard badge next to the existing
license/platform/swift badges.

Refs #178

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

---------

Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-14 09:29:48 -05:00
james.maes f1c60a31ad fix(ci): surface real xcodebuild errors on App target failure (#170)
Captures full xcodebuild output to a tempfile when check-app-builds.sh fails, so CI logs show the actual compile/link error instead of just the last 5 lines via -quiet | tail -5.
2026-05-13 09:36:11 -05:00
james.maesandClaude Opus 4.7 2d9a25a61e chore(gates): add check-applescript-confirmation.sh — defense-in-depth grep gate
P3-18 / security LOW-3: the run_applescript MCP tool MUST be registered
with requiresConfirmation: true so the HUD's confirmation panel gates
every AppleScript execution. Today the production registration lives
in App/MCP/MCPRuntimeWiring.swift:119 and is exercised by
MCPRuntimeWiringTests, but a grep gate adds defense in depth — every
line of source is covered, not just the path the test exercises.

Detection:
  For each non-comment, non-test line containing the literal
  `run_applescript`, walk down up to 5 lines (until the next `)` —
  the close of the enclosing register call), accumulate the call body,
  and assert requiresConfirmation: true is present. The window
  terminates early at the closing paren so a sibling register block
  with the flag set differently can't leak in via overlap.

Verified the gate catches a regression: temporarily flipped line 119
to `requiresConfirmation: false` — gate FAILed with the file:line
citation. Reverted; gate PASSes on clean tree.

Comment-only references to run_applescript (HudStateIntent.swift:19,
InProcessTool.swift:9) are filtered by is_comment_line, so they don't
trigger false positives.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-04 16:45:45 -05:00
james.maesandClaude Opus 4.7 6e249eeb76 chore(track-d 5-6): scripts/build-sqlite-with-extensions.sh + fetch-sqlite-vec.sh skeletons
D-5/D-6 are documentation-as-code for environment work the user owns
(custom SQLite build + vec0 fetch). Both scripts capture the full
outline — the curl, the SHA256 verify, the codesign — but require the
user to pin versions and hashes before running. Until those TODOs are
filled, both scripts exit 1 with a clear message rather than
half-running.

D-5 (build-sqlite-with-extensions.sh):
- Captures the clang flags needed for SQLITE_ENABLE_LOAD_EXTENSION=1 +
  FTS5 + RTREE + threadsafe=2.
- TODOs: SQLite version, amalgamation URL, SHA256, codesign identity.
- Output: build/sqlite/libsqlite3.dylib (codesigned, ad-hoc by default).

D-6 (fetch-sqlite-vec.sh):
- Pulls the prebuilt vec0.dylib from a pinned sqlite-vec GitHub release
  tag, SHA256-verifies, drops into App/Resources/vec0.dylib, codesigns.
- TODOs: release tag, SHA256, codesign identity.
- Asset naming follows sqlite-vec-${VERSION}-loadable-macos-aarch64.tar.gz;
  adjust if upstream renames.

Both scripts are mode 755 and verified to fail loudly when run as-is
(stamped TODO message + exit 1). No actual fetches or builds happen
until the user fills in the pins.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-04 14:29:26 -05:00
james.maesandClaude Opus 4.7 5cd46c974a chore(gates): add check-no-leftover-stubs.sh linter
F2 — fail the build if production code carries stub markers left over
from incremental plans. Catches the pattern that produced BLOCKER-INT-1
(NoopBusGateway shipping in `applicationWillFinishLaunching` while the
test wired a SpyBus): "test wires the real thing, production wires the
stub" divergences enumerated in `.planning/audit-2026-05-03/SYNTHESIS.md`.

Patterns detected:
  1. Comments containing the literal `Replaced in 0` (planning markers
     surviving past plan close — `Replaced in 06-05`, etc.).
  2. Comments containing `TODO: Plan ` or `TODO: 0X-`.
  3. Function bodies whose only non-whitespace content is exactly
     `// TODO: implement`, `// Placeholder`, or `// Stub` (a 2-line
     function detected by sed/awk inspection of open-brace +
     sentinel-comment + close-brace).
  4. Production instantiation of `Noop*(` or `*Stub(` types outside
     test code.

Allowlist: the `Dormant*` family (DormantVoiceBusEmitter,
dormantVoiceContinuation) are real producer-stream patterns, not stubs.
The naming convention is intentional: `Dormant` = "intentionally idle
until a producer arrives" (a legitimate state); `Noop` / `Stub` =
"placeholder until a plan replaces this." Two substrings are
allowlisted by exact-match.

Scope: scans `App/` + `packages/*/Sources/`, excludes `.build/`,
`Tests/`, `test-fixtures/`, and `.planning/source-material/`.

Self-tested against a synthetic fixture containing one of each pattern
— linter correctly reports 3 hits and exits 1. On the current `develop`
tree the linter is clean (PASS).

Modeled on `scripts/check-install-order.sh` and
`scripts/check-no-modal-presentation.sh` (set -euo pipefail; SRCROOT
fallback; allowlist-by-substring; exit 0/1 with `[name] PASS`/`[name]
FAIL` lines).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-04 12:39:35 -05:00
james.maesandClaude Opus 4.7 ee7f6d2dc7 fix(track-b 1-3): bundle voice models + WakeWordDAG typo + tier-1 TTS wiring
User feedback after deep audit: voice modality reported as "F — nothing
works". The audit identified five independent fatal breaks; this commit
closes the first three under TDD discipline. B-4 (AudioGraphOwner +
LiveSpeechAnalyzerBridge) and B-5 (voice loop e2e test) are the next
session's work.

**B-1 — voice models bundled at the runtime path:**
- scripts/copy-voice-models.sh — new postBuildScript that copies
  Resources/models/{openwakeword,silero}/ → Contents/Resources/Models/
  {openWakeWord,silero}/. Maps lowercase canonical-on-disk to capital-M
  runtime path AppDelegate.installVoice expects (was off-by-case +
  no copy phase at all). Hard-fails on PLACEHOLDER_… sha256 in
  MANIFEST.json so a forgotten fetch script is a build error, not a
  silent runtime failure.
- scripts/test-copy-voice-models.sh — 6 shell-level tests for the
  copy script (real-real bundle layout, PLACEHOLDER hard-fail,
  missing manifest, soft-degrade, capital-M path matches AppDelegate).
  All 6 pass.
- project.yml — adds the copy phase to Jarvis target's postBuildScripts.
- Resources/models/silero/MANIFEST.json — populated with real
  sha256 hashes by running scripts/fetch-silero-models.sh.

**B-2 — WakeWordDAG.swift:93 production typo:**
- packages/Voice/Sources/Voice/WakeWord/WakeWordDAG.swift — was
  `session.feedTest()` (test seam, always passes empty `[Float]` to
  hysteresis). Production path now calls `session.feed(samples:)` with
  the actual ring buffer slice. Without this, the wake word literally
  could never fire in production — every frame hit the hysteresis with
  zero samples regardless of mic input.
- packages/Voice/Tests/VoiceTests/WakeWordDAGTests.swift — 2 new
  tests: WD-1 verifies the DAG forwards actual ring contents to the
  session's classifier (RED on the bug; GREEN after the fix); WD-2
  verifies an empty ring doesn't trigger wasted classifier calls.

**B-3 — tier-1 TTS engine wired:**
- packages/Voice/Sources/Voice/TTS/TTSEngineActor.swift — `orpheus`
  parameter is now `OrpheusTTS?` (was non-optional). Production
  callers can wire just AVSpeechSynthesizer without first downloading
  Orpheus 3B weights from HuggingFace. Tier-2 requested with nil
  Orpheus degrades to tier-1 transparently via a new resolvedTier
  branch; defensive guard throws TTSError.sinkUnavailable if the
  branch ever leaks through.
- packages/Voice/Tests/VoiceTests/TTSEngineActorTier1Tests.swift —
  4 new tests: TET1-1 (construction with nil Orpheus), TET1-2 (real
  AVSpeechSynthesizer emits .started → .firstAudio → .finished),
  TET1-3 (.tier2 with nil Orpheus does NOT crash), TET1-4 (rapid-fire
  cancels prior synthesis).
- App/Voice/VoiceOutputWiring.swift — new factory enum that constructs
  the tier-1 engine. Lives outside AppDelegate.swift so the VISION-03
  Layer 3 grep gate (no PresenceSignalBus + TTS engine in same file)
  stays clean.
- App/AppDelegate.swift — `VoiceTTSAdapter(engine: nil)` becomes
  `VoiceTTSAdapter(engine: VoiceOutputWiring.makeTier1Engine())`.
  Voice subsystem now has a real synthesis path on every launch.

**Verification:**
- AgentCore: 215/215.
- Voice: WakeWordDAGTests 2/2, TTSEngineActorTier1Tests 4/4.
- Replay: 33/33.
- Bus: 57/58 (F-A2-01 pre-existing).
- webview/hud: 94/94.
- All 15 boundary grep gates: PASS (including check-presence-vision-
  isolation.sh — VoiceOutputWiring.swift renamed from TTSEngineWiring
  + comment text scrubbed of PresenceSignalBus references so the
  Layer 3 substring check no longer flags it).
- scripts/check-app-builds.sh: PASS.
- Models verified at bundle path Contents/Resources/Models/
  {openWakeWord,silero}/ with real .onnx files.

**Carry-forward to next session:**
- B-4: construct AudioGraphOwner in installVoice; start AVAudioEngine;
  route mic taps to wake-word DAG. Replace LiveSpeechAnalyzerBridge.feed
  body (`_ = chunk`) with the macOS 26 SpeechAnalyzer wiring.
- B-5: voice loop e2e — feed a known WAV file at the audio graph,
  assert STT result lands on the orchestrator.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-03 18:16:11 -05:00
james.maes 7dee5e0914 feat(scripts): add dev-reset.sh — wipe all Jarvis local state for clean first-run testing
Iterating on the onboarding wizard, TCC prompts, and hotkey binding
needs the next launch to be indistinguishable from a true first-run
install. Manually clearing 9 different state surfaces by hand is
error-prone — easy to forget the WebKit cache or a stale Anthropic
keychain entry, then debug a phantom 'why does it remember the API
key' for an hour.

scripts/dev-reset.sh wipes:
  1. Running Jarvis processes (pkill)
  2. ~/Library/Application Support/Jarvis/ (config, jarvis.db, replay.sqlite)
  3. ~/Library/Logs/Jarvis/                 (rotating bus/system/mcp logs)
  4. ~/Library/WebKit/<bundle-id>/          (WKWebView site data)
  5. ~/Library/Caches/<bundle-id>/          (WKWebView caches)
  6. ~/Library/Saved Application State/<bundle-id>.savedState/
  7. ~/Library/HTTPStorages/<bundle-id>{,*.binarycookies}
  8. ~/Library/Preferences/<bundle-id>.plist + UserDefaults
  9. Keychain: anthropic API key (security delete-generic-password)
 10. TCC: tccutil reset All <bundle-id> (microphone/camera/screen/etc.)

Defaults to dry-run; --yes / -y to apply. --quiet / -q for terse output.

Bundle id is auto-discovered from the live Info.plist (Debug build →
Release build → Xcode DerivedData), with the historical ids
com.koftwentytwo.jarvis and com.kingsrook.jarvis always included so
legacy state from older builds gets cleaned up too.

Verified: dry-run preview matches reality; --yes wipes everything;
post-reset verification shows ~/Library/Application Support/Jarvis,
WebKit caches, and the Anthropic Keychain entry all gone.
2026-05-02 07:27:50 -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.maes 75a4ed6991 feat(09-04): replace NullVoiceAdapters with real voice adapter triad + lock install order (D-09, D-10, BLOCKER-1, BLOCKER-2, WARNING-5)
- New App/Voice/{VoiceOrchestratorAdapter,VoiceTTSAdapter,VoiceBusEmitterAdapter,RejectReasonCopy}.swift
  replaces NullOrchestratorAdapter / NullTTSAdapter / NullBusEmitterAdapter (D-09).
- VoiceOrchestratorAdapter forwards submit/cancelAndSubmit to AgentOrchestrator,
  appends user-side text to TurnTranscriptStore (BLOCKER-1), enqueues a HUDBanner
  on every SubmitOutcome.rejected case via shared RejectReasonCopy (D-10).
- VoiceTTSAdapter wraps an optional TTSEngineActor (engine construction lands
  in a follow-on plan; nil-engine path no-ops gracefully).
- VoiceBusEmitterAdapter wraps an OutboundBatcher whose sink is the WebviewBridge
  so audio-level RMS reaches the HUD's RingMesh pulse.
- AppDelegate.installVoice now uses the production adapters; install order
  in applicationWillFinishLaunching reordered to vision -> agent -> voice
  with voiceInstallTask awaiting agentInstallTask?.value (WARNING-5).
- New broadcaster .voice subscriber in installAgent: per-turn assistantText
  accumulator + turnSourceWasVoice filter so text-originated turns NEVER drive
  emitTurnEnded on the voice adapter (BLOCKER-2).
- SubmitOutcome.superseded extended with newTurnId so cancelAndSubmit callers
  can append user text under the new turn id (BLOCKER-1 user-side closure on
  the cancel path).
- VoiceSubscriberTextTurnFilterTests proves text turns never trigger
  emitTurnEnded; VoiceOrchestratorAdapterTests covers RejectReasonCopy +
  banner id mapping + VoiceBusEmitterAdapter forwarding.
- New scripts/check-install-order.sh + scripts/check-no-null-voice-adapters.sh
  catch regressions structurally.
2026-05-01 20:39:19 -05:00
james.maes fdb56d2785 feat(09-01): installAgent + broadcaster wiring + BLOCKER-1 closure (INT-07-01)
- AppDelegate.installAgent() constructs AgentOrchestrator with the
  existing 7-arg ctor (visionRouter / presenceSnapshot stay nil for
  Plans 2 + 3), starts the OrchestratorEventBroadcaster, and creates
  the TurnTranscriptStore. Memory + transcript + devOverlay subscribers
  drain into MemoryExtractionCoordinator, the assistant-side
  accumulator, and a no-op consumer respectively.
- ConfigStore is now built in applicationWillFinishLaunching from the
  loaded launch + perTurn snapshots (was previously discarded).
- installMemory no longer calls coord.start; the wiring moved to
  installAgent step 5a where the broadcaster's memory child stream is
  available. Old agentOrchestratorEvents() nil-stub is gone.
- MemoryExtractionCoordinator gains a MemoryEnqueueing protocol seam
  so the BLOCKER-1 end-to-end test can spy on enqueue without driving
  the full extractor stack.
- New scripts/check-orchestrator-events-single-consumer.sh enforces
  D-05/D-08 (exactly one production for-await over orchestrator.events;
  WARNING-2 whitelist scoped to OrchestratorEventBroadcaster.swift).
  Wired as a project.yml preBuildScript and exercised in-process by
  PhaseNineGrepGateTests.
- MemoryWiringEndToEndTests proves enqueue receives non-nil userText +
  assistantText through real broadcaster + transcript store + memory
  coordinator wiring (BLOCKER-1 closure for INT-07-01).
- Test home note: MemoryWiringEndToEndTests lives in MemoryTests
  (not AgentOrchestratorTests as the plan suggested) because Memory
  depends on AgentOrchestrator via SPM and the cycle would block
  test compilation. Recorded in the plan SUMMARY.
2026-05-01 14:56:13 -05:00
james.maesandClaude Opus 4.7 5120a27b05 refactor(08-reviews): fold cross-AI suggestions into hardening substrate
Seven targeted fixes from the gemini / qwen3 / gemma4 cross-AI reviews of
Phase 8. Risk LOW per all three reviewers; these were ranked as 1.x
candidates rather than ship-blockers, so each is a forward-compatible
extension of an existing surface, not a behavioural change.

  1. Corpora/injection/manifest.json — add manifest_version=1; loader
     surfaces it via loadFromBundleWithVersion(); jarvis-eval prints
     "manifest_version=N" alongside the corpus count so each gate run
     records which corpus was exercised. (Gemma4 #3)

  2. MCPCrashRunner.swift — inline comments on each FD-whitelist pattern
     explaining why it persists across helper restarts (PIPE / FIFO /
     systm / KQUEUE / unix / /tmp/jarvis- / -wal / -shm). Future
     contributors won't accidentally tighten the whitelist. (Gemma4 #4)

  3. DriftReport.swift / DriftClassifier.swift — DriftItem now carries a
     `reason` string ("alwaysExcluded membership",
     "nondeterministicUnderSampling@temperature=0.7", etc.) so growth of
     the exclusion set is visible at every gate run. (Gemma4 #5)

  4. .planning/phases/03-hud/checklist.yaml — new P3-09 grep_positive
     guard asserting JarvisHUDPanel.swift sets `self.level` to one of
     statusBar/mainMenu/screenSaver/popUpMenu (occlusion regression
     guard). (Gemini #6)

  5. scripts/check-corpus-secrets.sh — extends the D-07 pre-commit hook
     to scan .replay / .evaluation / .sqlite session files. Uses
     `sqlite3` when available (queries events.tool_* rows directly) and
     falls back to plain grep on the binary blob otherwise. (Gemini #7)

  6. WakeHysteresisRunner.swift / main.swift — splits the single
     "passed" verdict into `pipelineStatus` (ok / notWired) and
     `performanceStatus` (pending / pass / warn / fail). Synthetic-only
     corpora always report PERFORMANCE: PENDING so an operator never
     mistakes a TTS-seed pass for a real-voice pass.
     (Gemini + Gemma4 #2)

  7. ReplayMCPAdapter.swift — substrate for side-effect-marker
     verification. Recorded `tool_result_full` envelopes may now carry
     an optional `side_effects: [String]` array; new public APIs
     `recordedSideEffects(toolUseId:)` and `sideEffectDelta(toolUseId:
     liveMarkers:)` let callers compare live vs recorded markers,
     mitigating the byte-equal false-green risk. Helper-side emission
     remains a follow-up. (Gemini #1, MEDIUM)

Tests: 63/63 across 9 Harness suites (3 new — manifest version exposure,
sampling-reason carries temperature, synthetic-corpus detection).
Build: clean.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-01 05:35:17 -05:00
james.maesandClaude Opus 4.7 268878b29d feat(08-04): shipping-gate + promote-replay + secret guard + R4-L7 integration test (Task 3)
Ships the four shell scripts the sweep manifest references plus the R4-L7
contract test that 08-01's threat model deferred (T-08-05).

Scripts:
- scripts/shipping-gate.sh: invokes the debug-built jarvis-eval all
  binary; --live requires JARVIS_LIVE_EVAL=1 (D-04 dual-gate). Debug
  configuration is intentional: MCPCrashRunner consumes #if DEBUG-gated
  test seams on MCPClient/MCPServerHandle, so a release build of
  jarvis-eval would not compile.
- scripts/promote-replay-session.sh: D-09 operator helper; copies
  recorded SQLite + writes sidecar JSON with temperature / model /
  replaySchemaVersion so DriftClassifier knows whether
  nondeterministicUnderSampling exclusions apply.
- scripts/check-corpus-secrets.sh: D-07 pre-commit guard greps Corpora/
  for sk-ant-, AKIA, ghp_, sk- patterns. Verified on positive
  (planted-secret) + negative (clean Corpora) inputs.
- scripts/precommit-template.sh: committed template the operator
  symlinks to .git/hooks/pre-commit; updates flow through
  automatically. Worktree's pre-commit hook also installed at
  .git/hooks/pre-commit.

ReplaySuppressionIntegrationTests (R4-L7):
- 9 tests asserting the TurnSource suppression contract:
  .replay(sessionId:) and .evaluation(scenarioId:) both have
  dispatchesToHUD=false / speaks=false; .text and .voice (controls)
  have both true; .memoryExtraction shares the suppressed policy.
- Asserted as a contract test (not a behavioral spy) because
  CONTEXT 5.1 forbids modifying production AgentOrchestrator/bus/TTS
  to inject spies. The properties are the architectural seam the
  first consumer reads; the test locks the policy table.

jarvis-eval `all` cross-subcommand dispatch refactored from direct
.init()+property assignment to .parse(argv) — the property-wrapper
backing storage of @Flag/@Option requires the parser path; direct init
trips the "Can't read a value from a parsable argument definition"
guard.

Shipping-gate.sh end-to-end smoke (recorded for SUMMARY):
- checklist: PASS (with manual coverage on 06-voice + 1 on 01-foundations)
- corpus-injection: PASS
- corpus-sse: PASS
- corpus-ndjson: PASS
- cap-recovery: PASS
- audio-rebuild: PASS (4/4 triggers automated)
- wake-corpus: FAIL (empty corpus — operator-recorded WAVs deferred)
- mcp-crash: FAIL (MockHelper fixture path resolution; pre-existing
  08-03 issue with packages/packages/ duplication)
- corpus-ndjson-live: SKIPPED (--live not set)
- 8-hardening checklist: PASS (15/15 items)

The infrastructure-failure-free run is the success criterion; the two
downstream failures (wake-corpus + mcp-crash) trace back to 08-02 and
08-03 respectively and are not Task 3 bugs.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-30 10:34:27 -05:00
james.maesandClaude Opus 4.7 1d6d4b268e feat(08-02): SSE + NDJSON fixture corpora + runners + capture script (Task 2)
Plan 08-02 Task 2 — OBS-04 pillars (b) Anthropic SSE and (c-fixture)
Ollama NDJSON / OpenAI-compat. Fixture-only; no network.

- Corpora/sse-anthropic/: 9 fixtures (.sse) covering CLAUDE.md Opus 4.7
  footguns: happy-text, text-then-tool-use (split input_json_delta),
  thinking-then-text, refusal (first-class stop_reason), mid-delta-disconnect
  (partial_tool_use_at_disconnect), ping-spam (swallow), unknown-event,
  cache-hit (cache_read_input_tokens), empty-input-json-delta.
- Corpora/ndjson-ollama/: 6 fixtures (.ndjson). 4 native + 2 OpenAI-compat.
  Locks the CLAUDE.md transport gotcha — `tool_calls` arriving on the chunk
  PRECEDING `done: true` (text-then-tool-call, parallel-tool-calls).
- SSEFixtureCorpus / NDJSONFixtureCorpus loaders + their manifests indexing
  expectedEventCount + expectedEventTags per fixture.
- SSEFixtureRunner / NDJSONFixtureRunner drive MockLLMProvider, byte-replay
  fixture data through the production AnthropicProvider / OllamaProvider
  decoders without `@testable import` and without modifying the providers.
- LLMEventTag stable case-name projection — payload-agnostic structural
  comparison between manifest and decoded stream.
- MockLLMProvider gains a `.ollamaOpenAICompat` FixtureKind so OpenAI-compat
  fixtures route to OllamaProvider(useOpenAICompat: true) and exercise the
  OpenAICompatDecoder. FixtureURLProtocol now keys its registry by request
  host; each MockLLMProvider picks a per-process-unique
  `fixture-<uuid>.test` host so parallel swift-testing runs do not trample
  each other's fixture registration (prior single-global state caused every
  fixture to receive happy-text bytes).
- swift-testing parameterized SSEFixtureCorpusTests + NDJSONFixtureCorpusTests
  with one @Test per fixture (per-arg diagnostics) plus footgun-coverage
  sentinel tests.
- jarvis-eval gains `corpus-sse` and `corpus-ndjson` subcommands.
- scripts/capture-anthropic-sse.sh — operator helper. CRITICAL D-07: Two
  layers of redaction. (1) sed pipeline rewrites any Authorization: Bearer
  / x-api-key: header line to <header>: <REDACTED> BEFORE the bytes hit
  disk. (2) Post-write grep for sk-ant-, AKIA, ghp_, sk-<32+> patterns
  with abort-and-delete on any match. Pre-commit hook (Plan 08-04) is the
  third layer.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-30 09:36:43 -05:00
james.maes 6f4f2fb6c2 feat(07-06): four cross-plan grep gates + AppDelegate install-order test (Task 3)
- scripts/check-single-memory-mutated-emit.sh — MEM-06 single-emission-site
  for ReplayEvent.memoryMutation (allowlist: MemoryStore.swift,
  Replay/ReplayEvent.swift, Replay/Schema.swift; tests + comments excluded).
- scripts/check-single-memory-used-emit.sh — D-05 single-emission-site for
  ReplayEvent.memoryRetrieval (same allowlist shape, MemoryStore.recordRetrieval
  is the sole emit site).
- scripts/check-presence-vision-isolation.sh — VISION-03 three-layer guard:
  Layer 1 (Vision/Package.swift declares no Voice or AgentOrchestrator dep),
  Layer 2 (no `import Voice` / `import AgentOrchestrator` and no TTSEngine /
  AgentOrchestrator.runTurn / cancelAndSubmit token references in
  packages/Vision/Sources/), Layer 3 (no App/ file co-locates a
  PresenceSignalBus reference with a forbidden TTS / runTurn token).
- scripts/check-embedding-dim-literal.sh — MEM-02 invariant: no literal 768
  in packages/Memory/Sources/Memory/ outside Constants.swift; comment-only
  lines tolerated.
- All four scripts wired as Jarvis pre-build phases in project.yml; the
  generated project.pbxproj reflects the updates.
- packages/Memory/Tests/MemoryTests/PhaseSevenGrepGateTests.swift mirrors
  the four shell gates as XCTest assertions plus a fifth assertion
  (testAppDelegateInstallOrder) verifying the call order in
  applicationWillFinishLaunching: installMemory before installVoice
  before installVision.
- App/Voice/NullVoiceAdapters.swift extracted from AppDelegate.swift to
  honor the Layer 3 boundary cleanly. NullOrchestratorAdapter and
  NullTTSAdapter are placeholder no-ops mirroring the prior structure;
  Phase 7 future wiring replaces them with real bridges.
- Negative-case demonstration completed: introducing a stray
  `.memoryMutation(...)` literal in App/AppDelegate.swift made
  check-single-memory-mutated-emit.sh exit non-zero; the violation was
  reverted before commit. Documented as evidence of gate sensitivity.
2026-04-29 18:58:11 -05:00
james.maesandClaude Opus 4.7 c9889705fb feat(07-04): create packages/Vision SPM scaffold + VISION-03 grep-gates
- New packages/Vision SPM target (macOS 14, swift-tools 6.0, strict-concurrency v6).
- Dependencies: Logging + AgentCore + swift-log only (NO Voice, NO AgentOrchestrator).
  This is the compile-time half of VISION-03; the textual halves are the two
  scripts below.
- VisionError (TCC + capture errors) and VisionDegradationReason (cameraDenied,
  midSessionRevoked) — analogs of Voice's Error / DegradationReason.
- scripts/check-vision-isolation.sh: fails build on any `import Voice` /
  `import AgentOrchestrator` under packages/Vision/Sources.
- scripts/check-presence-bus-no-tts-orchestrator.sh: fails build on any
  PresenceSignalBus reference inside packages/Voice/Sources or
  packages/AgentCore/Sources/AgentOrchestrator (non-comment lines only).
- Wired Vision into project.yml (XcodeGen) — workspace has no top-level
  Package.swift; mirrors the Memory wiring from 07-01 (same deviation
  pattern, same rationale).
- Updated NSCameraUsageDescription to the plan's wording (key was already
  present from earlier scaffolding).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-29 16:09:39 -05:00
james.maes 6d79d9b75b fix(check-app-builds): isolate CLI DerivedData to break the Xcode-IDE race
Resolves the launch fragility we previously deferred to Phase 8 in commit
fdd03d6 — turns out it wasn't an Xcode 26 codesign-chain bug, it was a
concurrent-DerivedData race between Xcode IDE and CLI builds.

Root cause
----------
When Xcode IDE is open with the Jarvis project, its background indexer/
builder touches Jarvis.app in the shared DerivedData *after* our
postBuildScripts (verify-entitlements --pre-codesign + codesign.sh +
verify-entitlements --post-codesign) complete. The post-IDE-touch state:

  1. Bundle signature → `invalid Info.plist (plist or signature have been
     modified)` per `codesign --verify`
  2. Info.plist's `JarvisEntitlementsVerified` marker → `false` (was YES
     after pre-codesign wrote it)
  3. AppDelegate's runtime entitlement probe hard-blocks at launch via
     `Logger.critical("JarvisEntitlementsVerified missing/false …")` —
     surfaced in OSLog as a Fault under com.koftwentytwo.jarvis:system

Fix
---
Use `-derivedDataPath "$REPO_ROOT/build"` so CLI builds run in their own
DerivedData, fully isolated from Xcode IDE. Build/sign/launch chain works
end-to-end; bundle is properly signed and the entitlement marker survives.

Verification
------------
- Fresh isolated build's Info.plist → JarvisEntitlementsVerified: true
- `codesign --verify` → --prepared / --validated (clean)
- `open <bundle>` → process launches, stays alive, AppDelegate progresses
  past the entitlement gate, ~/Library/Application Support/Jarvis/ is
  created (proves storage init ran, which only happens after the gate)

Implications for Phase 6 deferral
---------------------------------
The signed Phase-8 deferral document at
`.planning/phases/06-voice/06-HUMAN-UAT.md` listed the launch fragility
as a Phase 8 unblock-task. With this fix, the six HUMAN-UAT gates
(VOICE-07/09/10/12/13/14) are now actually runnable. The Developer ID
signing + notarization concerns Phase 8 still owns are different
(production distribution); this commit only addresses Debug development.

build/ is already in .gitignore (line 2) — no further changes needed.
2026-04-27 17:15:20 -05:00
james.maes bf5478490b feat(app): proper app icon set + sync helper LSMinimumSystemVersion to 14.0
App icon was missing — AppIcon.appiconset/Contents.json declared 10 size slots
but no PNG files filled in, so Jarvis showed the generic blank-app placeholder
in Dock/Finder/cmd-tab. This commit fills all 10 slots with a generated icon
derived from the existing menu-bar arc-reactor PDF, plus consolidates the
helper Info.plists onto the same MACOSX_DEPLOYMENT_TARGET pattern the main
target already uses.

Icon generation
---------------
- Render Icon-MenuBar-Template.pdf at 1024×1024 via qlmanage
- Composite via scripts/build-app-icon.swift (Core Graphics):
  - Squircle clip (radius = size × 0.2237 per Apple HIG)
  - Radial gradient backplate: #4FC3F7 cyan core → #0D47A1 navy → #061229
    near-black (Iron Man arc-reactor palette)
  - Inner glow ring at 35% radius for energized feel
  - Silhouette as luminosity mask, filled near-white with cyan tint
  - 2.5px cyan rim for definition against any wallpaper
- Resize to 7 unique pixel sizes (16/32/64/128/256/512/1024) via sips
- Map into the 10 AppIcon.appiconset slots (each idiom×scale combination
  gets its own filename for clarity, even though sizes overlap)
- AppIcon.icns is now produced by Xcode's actool at 52KB

Helper deployment-target sync
-----------------------------
The macOS 14 deployment-target bump in 722e25b (Phase 6 close-out, Voice
package's ORT 1.24.2 floor) updated the main Jarvis target via project.yml
but missed the three MCP helper Info.plists, which still hardcoded
LSMinimumSystemVersion=13.0. xcodebuild auto-corrected at build time but
emitted a warning per helper. Update each to use $(MACOSX_DEPLOYMENT_TARGET)
so future deployment-target changes propagate automatically (matches the
pattern the main App/Info.plist already uses).

Files
-----
- App/Assets.xcassets/AppIcon.appiconset/Contents.json — slot filenames
- App/Assets.xcassets/AppIcon.appiconset/icon_*.png — 10 icon PNGs
- scripts/build-app-icon.swift — reproducible icon-generation script
- mcp-servers/mcp-{clipboard,applescript,time}/Support/Info.plist — sync to $(MACOSX_DEPLOYMENT_TARGET)

Verification
------------
- xcodebuild Debug build: BUILD SUCCEEDED, zero LSMinimumSystem warnings
- AppIcon.icns present in Jarvis.app/Contents/Resources/ (52KB)
2026-04-27 17:07:00 -05:00
james.maes a73c9aa352 fix(build-webview): add Nix profile paths to PATH so script finds pnpm
The Xcode build phase 'Build webview bundle' couldn't find pnpm when it
was installed under any standard Nix profile location (~/.nix-profile/bin,
/etc/profiles/per-user/$USER/bin, /run/current-system/sw/bin) — the
hard-coded PATH only listed Homebrew, /usr/local, ~/.local/bin, and nvm.

Add the three canonical Nix profile paths to the PATH export so the App-build
gate works identically for users on Homebrew, npm-global, nvm, OR Nix.
Updates the FATAL hint to mention Nix as a third install option.

Surfaced during Phase 6 Wave 4 verification: scripts/check-app-builds.sh
failed identically on develop and worktree with `pnpm not found`. Root
cause was environmental (pnpm uninstalled since prior session) but the
script's PATH was the latent fragility.
2026-04-27 14:08:13 -05:00
james.maes 71e229d7de chore: merge executor worktree (06-03 vad-stt)
# Conflicts:
#	.gitignore
#	packages/Voice/Package.swift
2026-04-27 12:17:21 -05:00
james.maes b332214a7a feat(06-03): STTProvider + SpeechAnalyzerSTT + WhisperKitSTT + STTBackendSelector (VOICE-03/04)
- STTProvider protocol: transcribe(stream:) -> AsyncStream<PartialTranscript> + finalize()
- AudioChunk + PartialTranscript value types
- SpeechAnalyzerSTT: SpeechAnalyzerBridge seam; LiveSpeechAnalyzerBridge @available(macOS 26)
  + UnavailableSpeechAnalyzerBridge fallback; maps SFSpeechErrorCode.assetUnavailable → STTError.assetMissing
- WhisperKitSTT: LazyWhisperKitBridge defers async init; model='large-v3-v20240930_626MB' (anti-pattern guard)
  + WhisperKitBridge protocol seam for tests
- STTBackendSelector: make(backend:warningLogger:) — synchronous factory; 'speech_analyzer' default;
  unknown value falls back + emits warning via STTWarningLogger protocol
- STTError: backendUnavailable / assetMissing / finalizationFailed
- scripts/probe-speech-assets.sh: scaffold-time entitlement probe (AUDIT-R2-S5)
  uses ${VAR-} (no colon) for empty-string semantics per guardrail
- All tests pass: S1/S2 (SpeechAnalyzerSTT) + W1/W2 (WhisperKitSTT) + B1..B4 (selector)
- AVSpeechSmokeTests.swift stub created for Plan 06-04
2026-04-27 11:54:17 -05:00
james.maes a694e6bafd feat(06-03): SileroVAD v6.2.1 + ContractParityProbe + VADError (VOICE-02)
- SileroVAD actor: 512-sample chunk enforcement, opset-16 preferred with
  opset-15 fallback, stateful LSTM hidden vectors, VADDecision transitions
- VADInferenceEngine protocol + TestableVADOpset seam for test injection
- ORTVADEngine: real ORT-backed inference using OnnxRuntimeBindings
- ContractParityProbe: scaffold-time synthetic waveform parity gate
- VADError: invalidChunkSize / modelLoadFailed / opsetUnsupported / runFailed
- Resources/models/silero/MANIFEST.json (SHA-256 placeholder — run fetch script)
- scripts/fetch-silero-models.sh: downloads v6.2.1 ONNX models + updates hashes
- .gitignore: *.onnx excluded from git
- All S1..S6 tests pass (S5 skips when models absent — expected)
- Rule 3 deviation: Package.swift target deps updated (ORT + WhisperKit);
  macOS platform bumped to .v14 per ORT 1.24.2 requirement
2026-04-27 11:48:37 -05:00
james.maes 4d52126ade feat(06-02): ModelManifest SHA-256 verifier + fetch script (Task 1)
- ModelManifest.verify(modelDir:) reads MANIFEST.json via JSONDecoder,
  checks SHA-256 of each ONNX file via CryptoKit; fail-closed on mismatch
- WakeWordError: modelHashMismatch / missingModel / ortInitFailed
- Resources/models/openwakeword/MANIFEST.json placeholder (hashes populated
  by scripts/fetch-openwakeword-models.sh on first run)
- scripts/fetch-openwakeword-models.sh: downloads 3 ONNX files from
  dscripka/openWakeWord v0.5.1, computes and writes SHA-256 to MANIFEST.json
- .gitignore: add Resources/models/**/*.onnx (large gitignored binaries)
- ModelManifestTests M1..M4: all pass (4/4 GREEN)
2026-04-27 11:42:21 -05:00
james.maes 21644001b5 fix(codesign): Debug must sign WITHOUT runtime flag — AMFI rejects HR + adhoc
Debug builds were silently coming out (adhoc,runtime) despite project.yml
setting ENABLE_HARDENED_RUNTIME: NO and OTHER_CODE_SIGN_FLAGS: "" on the
Debug config. Two compounding bugs:

  1. project.yml's Debug-config OTHER_CODE_SIGN_FLAGS="" override didn't
     consistently propagate to the codesign.sh build-phase env (the
     parent Run Script env vs xcodegen-generated PBX project setting
     scope diverges).
  2. codesign.sh's ${OTHER_CODE_SIGN_FLAGS:-default} treats empty same as
     unset, re-introducing --options=runtime even if the override DID
     reach the script.

The combined effect: Hardened Runtime stayed ON, AMFI's strict
non-platform-Team-IDs-must-match rule fired against the adhoc-signed
Jarvis.debug.dylib, dyld aborted at __abort_with_payload (svc 0x209)
before any user code ran. App couldn't launch from Xcode (⌘R) or open.

Fix: explicit ${CONFIGURATION:-Release} == Debug branch in codesign.sh
that signs Debug with --timestamp=none and no runtime flag at all.
Result: Debug bundle is flags=0x2(adhoc) only — AMFI accepts it, app
launches, writes 'Jarvis launching' to system.log.

Also tried SWIFT_ENABLE_DEBUG_DYLIB: NO on Debug — the setting didn't
take in Xcode 26 (dylib still produced) but the codesign fix made that
moot. Kept the project.yml line as documentation; harmless if Xcode
ignores it.

Next blocker (Phase 1 self-check): app reaches main and logs
'Jarvis launching' but then hard-blocks on JarvisEntitlementsVerified
missing from Info.plist. That marker is supposed to be written by
verify-entitlements.sh --post-codesign. Investigate that script's
post-codesign Info.plist write path next session.
2026-04-26 07:18:21 -05:00
james.maes fa76fc64e6 fix(05-build): build-webview.sh PATH prepend for Xcode IDE GUI env
Xcode IDE GUI build phases don't inherit the user's shell PATH (only
xcodebuild from a terminal does). pnpm at /opt/homebrew/bin was
unreachable from Xcode → 'pnpm: command not found' →
PhaseScriptExecution failed.

Prepend /opt/homebrew/bin + /usr/local/bin + ~/.local/bin to PATH and
add an explicit pnpm-availability check with an actionable error
message. CLI builds unaffected (PATH was already correct there).

Verified: bash scripts/build-webview.sh still works from the terminal.
2026-04-26 05:54:41 -05:00
james.maes 5f9a767f9d fix(05-verify): G-01 extract await out of string interpolation autoclosure + add check-app-builds.sh gate
The CR-02 fix wired ME-04 closure correctly, but the success-path log
interpolated `await runtime.client.registeredToolNames().count` directly
inside a string literal. Swift 6 rejects 'await' inside autoclosures that
don't support concurrency; both Debug AND Release App-target builds
failed.

Masked because the Xcode 26 xctest upstream-blocker prevents the test
harness from compiling the App target — SPM tests passed (and report
'all green') while xcodebuild build never ran in the executor verify
loop. The verifier caught it on the goal-backward pass.

Fix: extract `await` to a `let toolCount` binding before the
interpolation. One-liner.

Add scripts/check-app-builds.sh as a manual/CI gate that exercises
`xcodebuild build -configuration Debug` of the App target. Wire as a
project gate (NOT a preBuildScript — that would loop). Documents the
upstream xctest-harness blocker as the rationale.

Verified: xcodebuild Debug BUILD SUCCEEDED post-fix; check-app-builds.sh
exits 0 against the repo.
2026-04-25 18:30:40 -05:00
james.maesandClaude Opus 4.7 3928c35ae1 fix(05-review): WR-07 verify helpers keep CODE_SIGNING_ALLOWED = NO
Helpers ship with CODE_SIGNING_ALLOWED = NO so codesign.sh can apply
per-helper identity (deepest-first walker, per-helper entitlements).
If a developer flips this to YES via Xcode UI, helpers get signed with
the parent target's identity and per-helper entitlements get stripped
(MCP-06 / R1 H-B2 hazard).

Adds two pbxproj lint rules in verify-codesign-settings.sh:
- Rule 4: any `CODE_SIGNING_ALLOWED = YES` anywhere fails the build.
- Rule 5: each of the three helper targets (mcp-time, mcp-clipboard,
  mcp-applescript) must declare `CODE_SIGNING_ALLOWED = NO` at least
  once in its buildSettings. Awk walks the pbxproj alphabetically
  (CODE_SIGNING_ALLOWED comes before PRODUCT_NAME inside each
  buildSettings block) and asserts the held value when hitting the
  PRODUCT_NAME line.

Verified by perturbing the local pbxproj to YES — script exits 1
with the expected error message.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-25 16:38:19 -05:00
james.maesandClaude Opus 4.7 102cb562f2 fix(05-review): WR-01 strip block comments and strings before grep
The pre-WR-01 lint stripped only LEADING `//` comment lines, so a
string literal like `let docs = "Use NSApp.run() to start"` or a
block comment `/* runModal is forbidden */` would false-positive
because the regex matched bare identifiers inside content the parser
wouldn't even reach.

Tightens both:
1. PATTERN now requires a call-site shape (`.method(` or `Type.method(`)
   so bare identifiers don't trigger.
2. Pre-strip pass via sed: strip line comments, block comments
   (whole-line delete), and string literals before grep.

Adds string-literal-modal-mentions.swift fixture covering:
- runModal inside a string literal
- runModal inside a block comment
- runModal in a trailing line comment
None of these should trigger; the lint now correctly accepts the
fixture (exit 0).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-25 16:23:59 -05:00
james.maes f0b8924b9d feat(05-05): modal-presentation lint + 2 fixtures + preBuildScript wiring
Closes the build-time half of MCP-04: scripts/check-no-modal-presentation.sh
fails the Jarvis build on any runModal / beginModalSession / NSApp.run /
NSApplication.shared.run call outside the two-file allowlist
(packages/Shell/Sources/Shell/TCCAlertService.swift +
packages/MCP/Sources/MCP/ConfirmationPresenter.swift). Wired as a
preBuildScript on the Jarvis target alongside the other build-breaking
lints (bus-protocol-version, no-evaluateJavaScript, single-writer-HUD).

Self-tests:
- whole-repo scan: exit 0 (only the two allowlisted call sites match)
- scripts/test-fixtures/allowed-non-modal-call.swift: exit 0
- scripts/test-fixtures/forbidden-modal-call.swift: exit 1 with the
  'forbidden modal-presentation call' error on stderr

Plan deviation: the plan's allowlist named 'App/TCC/TCCAlertService.swift'
but TCCAlertService actually lives at packages/Shell/Sources/Shell/. The
allowlist follows the file's real path.
2026-04-25 15:00:32 -05:00
james.maes 45328e3871 feat(05-03): bidirectional per-helper entitlement enforcement + 3 fixtures
- verify-entitlements.sh: refactor inline mcp-applescript helper grep into
  check_helper_entitlements_xml() driven off per-helper required+forbidden
  arrays. mcp-applescript MUST carry apple-events; mcp-time/mcp-clipboard
  MUST NOT (and forbidden list also rejects allow-jit / speech-recognition-
  assets / audio-input / allow-unsigned-executable-memory drift onto helpers)
- Unknown-helper case is a fatal error — 'no default-allow branch' so authoring
  drift cannot smuggle in an unconfigured helper
- Empty array guard via ${#array[@]} prevents bash 'unbound variable' under
  set -u when a helper has no required keys (Rule 1 fix discovered via Debug
  xcodebuild)
- New --verify-helper-fixture <plist> <helper-name> mode for fault-injection
  harness — feeds a fixture file in and runs check_helper_entitlements_xml
  without needing a real signed bundle
- 3 new fixtures cover the three threat-model arms:
    helper-applescript-good           T-pos:        mcp-applescript with apple-events
    helper-applescript-missing        T-05-03-02:   mcp-applescript MISSING apple-events
    helper-time-with-apple-events     T-05-03-01:   cross-helper drift (forbidden)
- test-verify-entitlements.sh: add run_helper_case() driver + 3 invocations,
  total fixture corpus now 6 cases (3 P1-05 + 3 P5-03)
- AppleScriptRunner.swift / MCPAppleScriptMain.swift: reword TCC-isolation
  comments so the literal subprocess CLI binary name does not appear in source
  (plan's anti-pattern grep '! grep -q osascript' enforces absence)
- Debug xcodebuild end-to-end: post-codesign walker reports
    'helper mcp-time / mcp-clipboard / mcp-applescript entitlements OK' x3
- Fault-injection harness: PASS=6 FAIL=0
2026-04-25 13:34:29 -05:00
james.maesandClaude Opus 4.7 b7255c57a7 test(03-05): end-to-end smoke script scripts/smoke-test-hud.sh
Post-build smoke (invoke after `xcodebuild build`) verifies the built
Jarvis.app has:
  * Contents/Resources/webview/ directory present (blue-folder wiring OK)
  * index.html present (scripts/build-webview.sh rsync OK)
  * assets/ subdirectory with >=1 *.js chunk (Vite build OK)
  * index.html references ./assets/ relative paths (Vite base: './')
  * index.html does NOT reference absolute /assets/ src|href paths
  * bus-harness.html still present (02-04 parity script invariant preserved)

Covers the gap left by the Xcode 26 `xcodebuild test` RunningBoard blocker
(.planning/debug/xctest-launch-runningboard-error-5.md). XCTest-level
assertions are structural (read repo tree); this script asserts the
BUILT .app shape, which is what the user's cold-launch actually sees.

Runs green against the Debug build produced by Task 1.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-24 15:54:53 -05:00
james.maesandClaude Opus 4.7 cabc27299a feat(03-05): AppDelegate wires HudStateCoordinator + bridge; lint activated; W1-W6 + B1-B3 tests
- App/HUD/HudStateBridge.swift (new): @MainActor public func busHudState(
  from: HudState) -> Bus.HudState. RawValue round-trip with assertionFailure
  on drift, .idle fallback in Release. Option (b) from the plan — avoids
  name-collision between App.HudState and Bus.HudState via module-qualified
  Bus.HudState in the helper's scope.
- App/AppDelegate.swift:
    * New stored properties: hudStateCoordinator, dormantAgentContinuation,
      dormantVoiceContinuation, dormantConfirmContinuation (all internal so
      @testable import Jarvis can assert W5 retention), webviewEntryFilename
      (internal test seam for W6).
    * installBus(): constructs HudStateCoordinator with emit closure that
      bridges App.HudState -> Bus.HudState via busHudState() and dispatches
      to bridge.send(.hudState(...)) on the MainActor. Creates three dormant
      AsyncStream producers (Phase 4/5/6 replace them) and retains their
      continuations on self so coordinator.{agent,voice,confirm}Task stay
      live. markReady() fires from onHandshakeArmed so the HUD promotes
      .booting -> .idle as soon as the webview is responsive.
    * Load target: bus-harness.html -> index.html (via
      webviewEntryFilename property so W6 can assert post-install value).
      XCTest-host short-circuit from 02-03 deviation #5 preserved, updated
      to reference the property-driven filename.
- scripts/check-single-writer-hudstate.sh: allowlist extended to
  {HudStateCoordinator.swift, HudStateBridge.swift, AppDelegate.swift} +
  {BusOutbound.swift, Protocol.swift}. Rationale comment updated: Phase 4+
  drives the coordinator via its three input AsyncStreams, not new
  .hudState(...) call sites. Already wired as a preBuildScript by Task 1.
- App/Tests/AppTests/HudStateCoordinatorBusWiringTests.swift (new):
    * W1 test_coordinatorConstructedInInstallBus — delegate.hudStateCoordinator
      non-nil after applicationWillFinishLaunching.
    * W2 test_emitClosureBridgesToBusSend — FakeJSEvaluator observes the
      closure producing a decoded BusOutbound.hudState(.thinking) after
      coordinator.markReady() + agent.yield(.thinking). Proves the exact
      closure shape AppDelegate installs routes rawValues end-to-end.
    * W3 test_markReadyCalledOnHandshakeArmed — coordinator pinned at .booting
      pre-handshake, promotes to .idle after bridge.handleHelloAck.
    * W4 test_appHudStateRawValueMatchesBusHudState — table-driven bidirectional
      rawValue parity over HudState.allCases and Bus.HudState.allCases. Catches
      drift before busHudState()'s assertion fires at runtime.
    * W5 test_dormantStreamsHeldByDelegate — all three continuations non-nil.
    * W6 test_installBusLoadsIndexNotHarness — webviewEntryFilename == "index"
      post-installBus.
- App/Tests/AppTests/WebviewBundleLoadTests.swift (new):
    * B1 test_indexHtmlInRepoWebviewDir — structural (#filePath traversal to
      repo root), catches missing committed index.html.
    * B3 test_indexHtmlReferencesRelativeAssets — content-grep for "./assets/"
      present + "src=\"/assets/" absent. Catches Vite misconfig or absolute-
      path drift.
    * B-extra test_assetsDirectoryGitignored_documentedInSmokeScript — doc-only,
      asserts the split of responsibility between XCTest (committed index.html)
      and scripts/smoke-test-hud.sh (built-app assets/*.js).

Tests are repo-structural / evaluator-observation only; xcodebuild test
launch still blocked on Xcode 26 RunningBoard error 5 per 02-03 precedent.
Verification is xcodebuild build (succeeds) + scripts/smoke-test-hud.sh
(next task). This follows the 02-03 SUMMARY deviation #5 pattern.

xcodebuild build -configuration Debug: BUILD SUCCEEDED.
bash scripts/check-single-writer-hudstate.sh: PASS (allowlist active).
packages/Bus swift test: 47/47 green.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-24 15:53:58 -05:00
james.maesandClaude Opus 4.7 a9a1b86ef9 feat(03-05): build-webview.sh + project.yml pre-build wiring + commit index.html
- scripts/build-webview.sh (exec): pnpm install (content-hash-cached) +
  @jarvis/bus build + @jarvis/hud build + rsync dist/ -> App/Resources/webview/
  with --exclude bus-harness.html (preserved for 02-04 parity script) and
  --exclude .gitignore (preserves the assets/ ignore pattern).
- project.yml: adds two preBuildScripts after check-no-evaluate-javascript:
    * Check single-writer HUD state (HUD-08)
    * Build webview bundle
  Ordering stays: existing parity/protocol/no-eval checks → single-writer
  lint → webview build → (postBuildScripts: pre-codesign verify → codesign
  → post-codesign verify → pbxproj lint).
- App/Resources/webview/.gitignore: ignore assets/ (regenerated every build).
- App/Resources/webview/index.html: committed entry point (R3F bundle).

Assets are NOT committed — they rebuild from webview/packages/hud/ on every
xcodebuild via the new pre-build script.

xcodebuild build -configuration Debug succeeds end-to-end; built app
has Contents/Resources/webview/index.html + assets/index-*.js with
relative (./assets/) paths.

packages/Bus swift test: 47/47 green.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-24 15:47:24 -05:00
james.maes 79092263bb feat(03-01): HudStateCoordinator single-writer + intents + lint
Implements HUD-08 precedence ladder as the sole Swift-side writer of
HudState. Three producer streams (agent, voice, confirmation) feed a
MainActor-isolated resolver; one emit closure is the only path out.

- HudStateIntent.swift: AgentHudIntent/VoiceHudIntent/ConfirmHudIntent
  Sendable enums. SystemHudIntent intentionally omitted — markReady()
  is a one-shot direct call, not a stream.
- HudStateCoordinator.swift: @MainActor final class. Ladder per
  HUD-08 verbatim: awaitingConfirmation > speaking > listening >
  thinking > idle > booting > reconfiguring. Ready-gate tweak on the
  .booting step (RESEARCH Open Q #4 pre-authorized) so boot-time
  silent inputs don't prematurely resolve to .idle.
- Bus-agnostic by design: takes @escaping @MainActor (HudState) -> Void
  emit closure so Plan 03-05 can wire it to WebviewBridge without this
  plan importing Bus or WebKit.
- Weak-self capture + deinit cancel on three for-await loops prevents
  the T-03-03 leak.
- HudStateCoordinatorTests.swift: 19 methods covering every precedence
  pair, booting gate, idempotent emit, cleared-confirm restore,
  weak-self no-leak, interleaved producers.
- scripts/check-single-writer-hudstate.sh: executable; exits 0 on
  current codebase; exits 1 on planted .hudState() construction
  outside the allowlist. Pre-build wiring deferred to Plan 03-05.
2026-04-23 18:55:04 -05:00
james.maes 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 0517926f61 fix(01): split entitlements per-config + address AMFI rejection on ad-hoc Debug
The debugger agent diagnosed the JarvisAppTests xctest launch failure
(Runningboard error 5 / Launchd job spawn failed). Root cause: AMFI
rejected the ad-hoc signed Debug bundle because it carried the managed
`com.apple.developer.speech-recognition-assets` entitlement. The kernel
logged `AMFI: Code has restricted entitlements, but the validation of
its code signature failed` → `load code signature error 4` → spawn
failure → Runningboard error 5.

**Changes:**

- Split `App/Jarvis.entitlements` → `Jarvis.Debug.entitlements` (no
  `speech-recognition-assets`; adds `get-task-allow` for XCTest attach)
  + `Jarvis.Release.entitlements` (full managed set, unchanged).
- `project.yml`: per-config `CODE_SIGN_ENTITLEMENTS`; Debug gets
  `ENABLE_HARDENED_RUNTIME=NO` + `OTHER_CODE_SIGN_FLAGS=""` +
  `ONLY_ACTIVE_ARCH=YES`; `DEVELOPMENT_TEAM` moved from base → Release
  only (avoids Team ID leaking into the Xcode 26 debug dylib).
- `scripts/codesign.sh`: resolves entitlements via $CODE_SIGN_ENTITLEMENTS
  env, falls back to config-specific file, then to the legacy path.
  Also signs `Contents/MacOS/*.debug.dylib` now that Xcode 26 emits it.
- `scripts/verify-entitlements.sh`: per-config `MAIN_REQUIRED` +
  `MAIN_FORBIDDEN` lists — Debug forbids `speech-recognition-assets`
  (wrong for ad-hoc) and requires `get-task-allow`; Release enforces
  the shipping posture with all 3 managed keys.

**Residual known issue** (documented in `.planning/debug/xctest-launch-runningboard-error-5.md`
and `01-HUMAN-UAT.md` §4): Xcode 26 still emits `Jarvis.debug.dylib`
with a code signature whose Team ID context diverges from our ad-hoc
outer sign, producing a dyld "different Team IDs" rejection when xctest
tries to load the bundle. Multiple workarounds attempted
(SWIFT_ENABLE_DEBUG_DYLIB=NO, resigning the dylib, stripping it
post-build). `xcodebuild test -scheme Jarvis -only-testing:JarvisAppTests`
fails bootstrap; SPM package tests (56, all passing) and `xcodebuild
build` both succeed. Actual code quality is unaffected — this is a
harness/tooling boundary issue.

Verified: 56 SPM tests pass; `xcodebuild build -configuration Debug`
BUILD SUCCEEDED; `scripts/test-verify-entitlements.sh` self-test
PASS=3 FAIL=0.
2026-04-23 17:06:10 -05:00
james.maes 4446afbc45 fix(01-WR-12): check codesign exit code explicitly in verify-entitlements.sh
Phase 1 REVIEW.md WR-12: both `--post-codesign` greps used
`codesign … 2>&1 || true`, which merged stderr into stdout AND
swallowed the exit code. In the helper check this was the worst
case: a helper whose binary was deleted between signing and verify
would emit error text that doesn't contain "apple-events", and the
grep-for-absence would coincidentally pass — letting an unsigned
helper ship.

scripts/verify-entitlements.sh
- Main app `codesign -d`: split streams (stdout → EXTRACTED, stderr →
  /tmp/codesign-main.err), check exit status explicitly, dump stderr
  and bail if codesign failed.
- Per-helper `codesign -d`: same pattern. /tmp/codesign-helper.err
  is cleaned up on both success and failure.
- Drop `-depth` from the helper find — it's relevant for signing
  (deepest-first) but pointless for verify-only.

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

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

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

Task 3 acceptance: xcodebuild -list shows EntitlementProbe scheme +
JarvisEntitlementProbeTests target; EntitlementProbe build-for-testing
succeeds with the full codesign + entitlement verification chain.
2026-04-22 14:42:30 -05:00
james.maes faf00042c4 feat(01-05): wire codesign + verify scripts into Jarvis target build phases
Append four PBXShellScriptBuildPhase entries to the Jarvis target (via
project.yml postBuildScripts, regenerated via xcodegen) in RESEARCH
Open Q #8 order:
  1. (existing) Create Contents/Helpers directory
  2. Verify entitlements (pre-codesign)  -- writes JarvisEntitlementsVerified=YES
  3. Codesign bundle (deepest-first)     -- scripts/codesign.sh
  4. Verify entitlements (post-codesign) -- greps signed entitlements
  5. Verify codesign settings (pbxproj lint)

The pre-codesign phase writes the verified flag to Info.plist BEFORE
codesign so it becomes part of the signed content (writing AFTER codesign
invalidates the signature, per Open Q #8 correction).

The Codesign phase falls back to CODE_SIGN_IDENTITY (then '-') when
CODE_SIGNING_ALLOWED=NO (Xcode does not set EXPANDED_CODE_SIGN_IDENTITY
in that case). Debug's CODE_SIGNING_ALLOWED=NO escape hatch from Plan 01
stays in place — it is what lets our script be the authoritative
codesigner without Xcode's own step fighting the managed
speech-recognition-assets entitlement on a dev machine without a
provisioning profile. See SUMMARY Deferred Items for cleanup path once
a Developer ID cert + provisioning profile land.

Debug build now runs the full four-phase Run Script sequence and emits:
  pre-codesign: JarvisEntitlementsVerified=YES written
  post-codesign: entitlement verification passed
  verify-codesign-settings: pbxproj lint passed

Also: [Rule 1 - Bug] fix verify-entitlements.sh --post-codesign to pass
--xml to codesign -d --entitlements -. macOS 26's default output is a
'[Dict] [Key] ...' rich-text dump which the XML grep below did not match;
--xml forces the legacy XML plist output the verifier is written against.
2026-04-22 14:28:39 -05:00
james.maes 52f2bdee0d feat(01-05): add build-time codesign + entitlement verification scripts
- scripts/codesign.sh: deepest-first walker (find -depth) signing nested
  Contents/Helpers/**/*.app with per-helper .entitlements, then main app
  last. Never uses --deep (TN2206/Pitfall #3 guard).
- scripts/verify-entitlements.sh: two-mode script per RESEARCH Open Q #8.
  --pre-codesign validates source + writes JarvisEntitlementsVerified=YES
  to unsigned Info.plist so the flag becomes part of the signed content.
  --post-codesign greps signed entitlements (via codesign -d --entitlements -)
  for MAIN_REQUIRED/MAIN_FORBIDDEN + per-helper rules; never writes. Strips
  XML comments before grep (self-invalidating-grep defense).
- scripts/verify-codesign-settings.sh: pbxproj linter for CodeSignOnCopy = YES,
  --deep, and missing --options=runtime/--timestamp.
- scripts/test-verify-entitlements.sh: fault-injection self-test harness
  that feeds good/broken/forbidden fixtures to --verify-fixture mode and
  asserts exit codes.
- scripts/test-fixtures/{good,broken,forbidden}-entitlements.plist: fixtures
  exercising the happy path, missing allow-jit, and forbidden apple-events.

Self-test: PASS=3 FAIL=0. Linter passes against current pbxproj.

MCP-06, SEC-02, SEC-03, SEC-08 scaffolded.
2026-04-22 14:21:33 -05:00