Closes D-5 / D-6 (sqlite-vec dylib bundling) carry-forwards from the
2026-05-04 audit by adopting jkrukowski/SQLiteVec as a SPM dependency.
The package's CSQLiteVec target compiles sqlite3.c + sqlite-vec.c into
one statically-linked C target with `core_vec_init()` registering vec0
as an auto-extension. All sqlite3_* calls in Replay's SQLiteConnection
and Memory's MemoryStore resolve to this bundled SQLite (vec0 always
available) instead of Apple's stripped /usr/lib/libsqlite3.dylib.
The retired hand-rolled approach (dlsym pathway + JARVIS_VEC0_STUB_PATH
env-flag + Bundle.module placeholder text file) never worked on macOS
because Apple stripped sqlite3_load_extension from the system libsqlite3.
Memory init has been silently dormant since Phase 7 (2026-04 onward),
which is what made B-02 (history threading) appear permanently broken
to the user during dogfooding even after the 48ad8d6 tactical patch
landed.
Live verification 2026-05-11: fresh launch with empty jarvis.db creates
schema, persists turn rows across submits, B-02 multi-turn context
resolves correctly ("what breed is Toby?" → "Golden Retriever" after
prior turn established Toby is a Golden Retriever), and the new
get_memory_stats tool reports `turnsCount=6, vecVersion=v0.1.6,
sqliteVersion=3.51.0`.
Changes:
- packages/Replay/Package.swift
+ SQLiteVec SPM dependency (from: "0.0.14")
+ CSQLiteVec product wired into Replay target
- packages/Replay/Sources/Replay/SQLiteConnection.swift
- import SQLite3
+ @_exported import CSQLiteVec
All sqlite3_* call sites unchanged — linker now resolves to our
bundled sqlite3 with vec0 statically linked in.
- packages/Memory/Package.swift
+ SQLiteVec SPM dependency (named product so MemoryStore can import
CSQLiteVec directly for the core_vec_init() one-shot call)
- resources: PLACEHOLDER.txt copy (defunct vec0.dylib stub slot)
- packages/Memory/Sources/Memory/MemoryStore.swift
- import SQLite3 → import CSQLiteVec
+ _vecAutoExtensionRegistered static-let one-shot: calls
CSQLiteVec.core_vec_init() which registers vec0 via
sqlite3_auto_extension on the first MemoryStore construction.
Idempotent (static-let initializer fires exactly once).
- loadVecExtension(on:) dlsym/JARVIS_VEC0_STUB_PATH/Bundle.module
pathway (retired; ~70 lines removed)
Updated docstring to reflect the new hard-fail boot policy.
- packages/Memory/Sources/Memory/Resources/PLACEHOLDER.txt — deleted
- App/AppDelegate.swift installMemory()
Reverses Track-D D-1 (75a10be, 2026-05-04): MemoryStore.init failures
are now FATAL via Logger.critical + TCCAlertService.presentHardBlock
+ NSApp.terminate. The graceful-degradation path that kept the app
booting with memoryStore=nil hid the entire B-02/B-04/B-05 silent-
failure bug class. Aligns with the 2026-05-07 API-first pivot
thesis: "no silent failures".
`store` is now non-optional throughout installMemory; downstream
callers no longer need `if let store = store`.
- packages/MCP/Sources/MCP/InProcess/GetMemoryStatsTool.swift (new)
+ MemoryStatsDispatching protocol
+ MemoryStats Codable struct (turnsCount, factsTotal, factsActive,
sessionsCount, dbFileBytes, vecVersion, sqliteVersion,
lastTurnAtUnixMs, lastFactAtUnixMs)
+ GetMemoryStatsTool conforming to InProcessTool
- App/MCP/InProcessMemoryAdapters.swift
+ MemoryStatsStoreAdapter — bridges the protocol to MemoryStore's
public queryRowCount / querySingleString seams and FileManager
for the on-disk file size. Read-only, no confirmation.
- App/AppDelegate.swift installSelfKnowledgeTools()
+ Registers GetMemoryStatsTool with MemoryStatsStoreAdapter.
- packages/Memory/Tests/MemoryTests/*
Retired the JARVIS_VEC0_STUB_PATH XCTSkipUnless guards from 9 tests
(vec0 is always available now — no need to env-gate). Rewrote
MemoryStoreTests to drop the now-impossible "init throws when vec
missing" case and assert facts_vec virtual table creation +
vec_version() readback. Added seedTurnRow helper to
MemoryStoreApplyOpTests so the now-enforced
`facts.source_turn_id REFERENCES turns(id)` foreign key is honored
in tests that previously passed when FK enforcement was dormant.
Test summary: Memory full suite 91 tests, 10 skipped (env-gated real-
Ollama tests), 0 failures. App target builds clean. Substrate verified
end-to-end via live launch (vec_version: v0.1.6 logged at boot;
get_memory_stats returns actual schema row counts).
Pending D-7 carry-forward: ollama pull nomic-embed-text (~270MB) for
embedder; user already has qwen2.5-coder:32b-instruct-q8_0 which is
a higher-quality variant of the project's hardcoded
qwen2.5-coder:32b — small ModelID config delta to follow.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
- Composite ToolDispatcher: name-routes between InProcessToolRegistry (memory
+ four self-knowledge tools) and inner stdio dispatcher (MCPToolDispatcher).
- Sits INSIDE ConfirmingToolDispatcher's chain so confirmation gating, bus
emission, and observer hooks fire identically on both paths.
- In-process result bytes flow through the same SanitizeForModel.prepareForBoundary
+ ToolResultObserver pipeline as stdio results (audit-trail parity).
- Tools registered AFTER composite construction are still routable because
the composite holds the registry actor reference and queries by name on
every dispatch (resolves the installSelfKnowledgeTools-runs-after-installAgent
install-order wrinkle).
- Closes 5/5 cases in InProcessAwareDispatchRoutingTests.
- Lock-protected Set<String> mirror of registered tools' requiresConfirmation flags.
- InProcessToolRegistry.register(_:) now writes through to the cache so the
upcoming InProcessAwareToolDispatcher can answer requiresConfirmation(toolName:)
synchronously from a nonisolated context (ToolDispatcher protocol contract).
- OSAllocatedUnfairLock: low-overhead, available since macOS 13.
Plan 10-02b / B-01: MCPClient previously discarded the Tool definitions
returned by each helper's tools/list response, retaining only the
toolToServer name→server lookup. The Anthropic / Ollama provider
request bodies need name + description + inputSchema for each stdio
tool, but with the definitions lost there was nothing to feed the
catalog wiring at App/AppDelegate.swift:1274.
Changes:
- MCPClient gains a private `toolDefinitions: [String: Tool]` cache
populated from each helper's listTools() response alongside
toolToServer.
- New `toolCatalog() -> [ToolSchema]` re-encodes Tool.inputSchema
(Value, JSON-encodable) to bytes and projects to the AgentCore
ToolSchema shape. Empty descriptions degrade to a placeholder
("MCP tool '...' (no description provided by helper).") so we
never drop a tool from the catalog.
- shutdown() also clears toolDefinitions (lifecycle parity with
toolToServer / registry).
- Imports AgentCore for ToolSchema (asymmetric dep already declared
in Package.swift; mirrors MCPToolDispatcher.swift's usage).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Adds the substrate that the production fix at App/AppDelegate.swift:1274
needs to source the tool catalog from the in-process registry instead of
the hardcoded empty array.
Protocol change:
- InProcessTool gains `var toolDescription: String { get }` — a
human-readable one-line description surfaced in the Anthropic / Ollama
tools[] payload's `description` field. Named `toolDescription` (not
`description`) to avoid colliding with CustomStringConvertible.
All 7 production InProcessTool implementations updated with descriptions
calibrated to drive correct tool selection by the model:
- list_audio_devices, get_active_audio_route, get_self_state,
list_camera_devices (Phase 10-01 self-knowledge tools)
- search_memory, forget_fact, search_conversation (Phase 7 memory tools)
Registry change:
- InProcessToolRegistry.toolSchemas() projects every registered tool to
a ToolSchema(name, description, inputSchema) suitable for the LLM
request body. Returns name-sorted for prompt-cache determinism.
- Adds AgentCore import to InProcessToolRegistry for the ToolSchema type.
Test coverage:
- InProcessToolRegistrySchemasTests asserts all 7 tools project
correctly with non-empty descriptions and inputSchema bytes.
- All 85 existing MCP tests pass unchanged.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
- AppDelegate (App/): adds the missing "what is this" preamble + Lifecycle /
Note on size / See also sections to the existing bootstrap-chain docstring.
- VoiceController (packages/Voice): converts the // MARK comment-block header
to a proper /// docstring with Threading + Anti-patterns + See also sections.
- MCPClient (packages/MCP): adds a docstring covering the registry +
per-server restart mutex + relationship to ChildSpawnGate / ConfirmingToolDispatcher.
Comment-only changes; verified via swift build (Voice, MCP) and
scripts/check-app-builds.sh.
The other types listed in the documentation brief (BufferBroadcaster,
SpeechAnalyzerSTT, CameraCapture, FrameAttachController, MemoryStore,
MemoryExtractionOrchestrator, HybridSearch, AgentOrchestrator, LLMProvider,
BusOutbound, BusInbound) already carry substantive header docstrings —
left alone per the audit-grade-improvement-only constraint.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Per audit-2026-05-04 P2-14: VoiceController._forceState,
_testFireSpeechEnd, _testCurrentSttSessionId were declared `public`
solely so XCTest could see them. Same for MCPClient._testHandle and
MCPServerHandle._testProcessIdentifier. These leaked test-only entry
points into each package's public API — anyone importing Voice could
call _forceState(.speaking) from production code.
Voice (4 seams): public → internal. Voice tests already use
@testable import Voice, so no test-side change required.
MCP (2 seams): public → @_spi(Testing) public. MCP tests opt in
via @testable @_spi(Testing) import JarvisMCP. The Harness package
(non-test) consumes _testHandle / _testProcessIdentifier for the
MCPCrashRunner SIGKILL recipe — opt in via @_spi(Testing) import.
@testable alone does NOT expose @_spi symbols — the SPI must be
explicitly imported, which is the desired discipline (each consumer
acknowledges they're reaching past the public ABI).
6 test seams total moved off the public ABI. Voice + MCP tests
pass (87 + 85). Harness builds clean.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
[Rule 3 - Blocking] VllmMlxSidecar (Task 4) needs ChildSpawnGate but
packages/Vision can't depend on JarvisMCP without transitively pulling
AgentOrchestrator (which would violate VISION-03 boundary).
Solution: hoist ChildSpawnGate.swift into a new JarvisChildSpawn SPM
target inside the MCP package. JarvisMCP keeps the same singleton via
the new dep; Vision can now depend on JarvisChildSpawn alone.
- New target JarvisChildSpawn (only depends on swift-log)
- JarvisMCP gains JarvisChildSpawn as a dependency
- MCPServerHandle.swift / MCPClient.swift gain 'import JarvisChildSpawn'
- ChildSpawnGate uses Logger directly instead of MCPLogChannel.logger
(avoids dragging MCPLogChannel into the standalone target)
- Tests updated to '@testable import JarvisChildSpawn'
- All 3 ChildSpawnGate tests pass; full MCP build clean
Promote the FD_CLOEXEC sweep from "fatal-error in Debug on every
non-CLOEXEC FD" to "fatal-error only when the FD belongs to an open site
under our developer-owned prefixes; retrofit + log everything else."
The original spec assumed every long-lived FD in the process is opened
by our code, where a missing O_CLOEXEC is a developer regression worth
halting on. WKWebView violates that assumption — the GPU process
init opens system Metal libraries (e.g., AppleMetalOpenGLRenderer.bundle
default.metallib) without O_CLOEXEC, and ⌘R from Xcode inherits lldb's
debugger pipes, neither of which is actionable from our codebase.
Developer-owned prefixes (computed once via Bundle.main.bundlePath and
NSHomeDirectory):
- <Bundle.main.bundlePath>/
- ~/Library/Logs/Jarvis/
- ~/Library/Application Support/Jarvis/
For these, Debug builds still fatal-error so a regression at our open
sites is loud and immediate. Everything else — system frameworks,
anonymous pipes/sockets, lldb-inherited FDs — gets retrofitted with a
debug log line.
Bonus: Debug fatalError message now includes the offending FD's path
via fcntl(F_GETPATH), so future regressions name the file directly
instead of forcing a separate diagnostic round.
This unblocks ⌘R from Xcode against the Debug build. 79/79
JarvisMCPPackageTests still pass; the FD-leak baseline test reports
delta=0 across the suite.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Match the SDK's `Client.callTool(name:arguments:)` signature where
`arguments: [String: Value]?` is optional. Callers can now invoke
no-arg tools without manually passing `[:]`. MCPToolDispatcher now
preserves the JSON-RPC `arguments: null` vs `arguments: {}`
distinction by passing `nil` when the model emits no args bytes.
Updated MCPClientCalling protocol + MockClient conformer in
MCPToolDispatcherTests to match the optional shape. Added
test_callTool_withoutArguments_isAccepted that exercises the default
nil parameter.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Replaced `try? await Task.sleep + Task.isCancelled` with an explicit
do/catch that early-returns on CancellationError. Previously the
`try?` swallowed the cancellation error and a window opened between
sleep returning and Task.isCancelled being observed during which a
cancelled timer could still fire .timeout. Today the broker's
first-write-wins guard saves us, but a future contributor removing
that guard would silently introduce double-resolve.
Adds test_response_atTimerBoundary_firstWriteWins_noDoubleResolve —
loops 10x at a 50ms timeout, sends .deny at T+49ms, asserts the
broker resolves exactly once (deny OR timeout, both legal) and dismiss
fires exactly once. Catches double-resolve regressions.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Previously ConfirmationPresenter held broker as `weak var` and
ConfirmationPresenterHolder held its inner presenter as `weak var` —
a doubly-weak chain that could silently break Approve/Deny button
responses if either side deinit'd during a pending request. The user
would see the panel close on Approve, then the orchestrator would
hang for 60s until the broker's timer fired .timeout.
Switched both to strong references. The cycle motivation
(broker → presenter → broker) is broken by the holder indirection:
broker holds the holder, holder holds presenter strongly, presenter
holds broker strongly. The chain is acyclic when read top-down because
the broker does not hold the presenter directly.
Documents the lifecycle invariant on MCPRuntime: AppDelegate must hold
runtime strongly for the lifetime of the process; orchestrator must
not outlive the runtime.
Adds ConfirmationLifecycleTests with three guards:
- canonical hold pattern always resolves Approve
- wedged presenter (never calls broker.response) still resolves via
timeout — the timer is the load-bearing fallback
- source-level: ConfirmationPresenter declares `let broker:` (strong),
not `weak var broker:`
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
CR-03: BusGateway's toolUseId parameter is now String (matching the bus
schema's Phase 2 closed contract) instead of UUID. Previously the
dispatcher synthesized `UUID(uuidString: toolUse.id) ?? UUID()`, which
silently produced a fresh random UUID for every Anthropic 'toolu_…' or
Ollama free-form id — breaking correlation across bus / replay /
orchestrator. The original String id now flows verbatim from
toolCallStart through argsPreviewUpdate to toolCallEnd. The broker's
internal UUID is now a fresh-per-call value and does NOT replace the
bus-facing id.
WR-04: ConfirmingToolDispatcher now emits toolCallEnd(ok: true, …) on
the success path so bus consumers (HUD ToolCallCard) transition
Running → Completed. On dispatch error post-approval, toolCallEnd
fires with ok: false carrying the error description before re-throwing.
Adds three new tests (busToolUseId verbatim across emissions, non-UUID
id flows through, success path emits 3 events). Updates SpyBus and
SpyBus-equivalent in MCPRuntimeWiringTests to use String toolUseId.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Both initialize-failure throw arms in MCPServerHandle.start() (the
client.connect() catch and the helperMissingToolsCapability guard) now
call a new closeParentPipeEnds helper before throwing. Without this,
each failed start() leaked 3 parent-side pipe FDs (stdin write, stdout
read, stderr read) — Foundation's Pipe deinit doesn't close FDs when
the underlying FileHandle was created with closeOnDealloc:false (the
default), and terminateProcessImmediately only SIGKILLs the child.
Adds MCPServerHandleInitFailureFDLeakTests with four structural
assertions: helper exists, closes all three retained ends, both throw
arms call it, cleanup precedes the throw on both arms.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Closes MCP-04 (full args-redaction seal pre-approval) and SEC-08
(defense-in-depth args masking on the bus). Final wiring of AGENT-11's
timeout-as-deny semantics — .timeout outcome triggers a JarvisLogChannel.mcp
WARNING line + ConfirmationError.timedOut throw.
- Public actor ConfirmingToolDispatcher conforms to ToolDispatcher and
composes around an inner ToolDispatcher (production: MCPToolDispatcher).
Plan 04-04's orchestrator code is unchanged; the gating happens by
composition.
- Pre-approval bus emission's argsPreview is the literal
'{"awaitingApproval":true}' seal. Post-approval, a follow-up
updateArgsPreview carries the sanitized real args.
- BusGateway protocol abstracts the three bus surfaces the dispatcher
needs (start / update / end). App-level wiring implements it.
- defaultLogWarning routes to swift-log Logger(label: 'mcp') with a
'subsystem=ConfirmingToolDispatcher' metadata key. Tests inject a
recording closure.
- 9 tests: pass-through fast path; awaiting-approval seal ordering;
approve / deny / timeout / barge outcomes; inner-error propagation
after approval; sync requiresConfirmation delegation; explicit
args-redaction-before-approval invariant. test_OC_T1_timeoutLogsWarning
name fixed by plan medium-finding.
Closes MCP-04 (confirmation prompts use NSPanel + beginSheet, ZERO modal-
session calls).
- @MainActor final class ConfirmationPresenter conforms to
ConfirmationPresenting via nonisolated trampolines that hop to MainActor.
- Hidden owner NSPanel (alpha=0, .floating, .nonactivatingPanel) hosts the
beginSheet attachment; the per-id child panel renders SwiftUI content
(Approve/Deny buttons) via NSHostingView.
- Approve / Deny button handlers fire-and-forget broker.response(id:.approve)
and (.deny) — the only path to broker.response(.approve) is this native
AppKit surface (T-05-05-04).
- File contains zero matches for runModal / beginModalSession / NSApp.run /
NSApplication.shared.run outside doc comments. Plan 05-05 Task 5 lint
enforces this codebase-wide.
Closes MCP-09 (broker FSM, first-write-wins on .approve / .deny / .timeout
/ .barge) and AGENT-11 (60s timeout-as-deny via Task.sleep with sub-second
test-injection knob).
- ConfirmationOutcome.swift defines the four-outcome enum, matching
ConfirmationError throw-type, and ConfirmationPresenting protocol so the
broker can compile + test against an actor mock without pulling AppKit.
- ConfirmationBroker.swift is a public actor whose serial executor IS the
synchronization primitive; no locks, no atomics. The 'resolved' guard
inside response(id:outcome:) enforces MCP-09 first-write-wins.
- 9 tests cover the four-outcome happy paths, the sub-second timeout, the
late-hop no-op invariants (after .approve and after .timeout), the
concurrent-response race, the unknown-id no-op, and the dismiss-once
lifecycle invariant.
Concrete `ToolDispatcher` (declared in AgentOrchestrator) that proxies
to MCPClient, joins all .text content blocks, runs the SEC-07
sanitize+headTruncate pipeline, and returns prepared bytes.
Architectural notes baked into doc comments:
- Wrap step is intentionally absent. The orchestrator's
UntrustedWrapper.wrap(_:) runs AFTER dispatch returns. Calling a
wrap step here would double-wrap and corrupt the per-turn nonce
invariant (Plan 04-04 §dispatch loop).
- ToolResultObserver protocol exposes BOTH pre-sanitize raw bytes
AND post-sanitize prepared bytes for replay capture (SEC-07 audit
trail). Plan 05-05 wires the actual ReplayLog writer in
AppDelegate; this plan only exposes the callback.
- MCPClientCalling protocol extracted so unit tests can mock the
SDK boundary without spawning helper subprocesses. Production
conformance is the one-liner extension on MCPClient.
- requiresConfirmation(toolName:) is nonisolated — backed by a
ToolRegistry value-type snapshot captured in init. Satisfies the
protocol's sync requirement without an actor hop.
- Argument decoding tolerates empty / malformed JSON by returning
[:] (the helper's inputSchema handles missing fields with a
structured isError response).
8 dispatcher tests + 49 total MCP tests pass. AgentCore 134/134,
Replay 27/27, Bus 47/47 all still green (no upstream regressions).
Release build exits 0 with the new AgentCore SPM dep.
Sendable value type with register / server / requiresConfirmation /
toolNames. Last-write-wins on duplicate register; sorted toolNames for
deterministic enumeration. Captured by MCPToolDispatcher as a
`nonisolated let` snapshot at init so the protocol's sync
requiresConfirmation can be served without an actor hop.
5 tests pass.
SEC-07 boundary scrubber + head-truncator. Three public functions:
- sanitize(_:) — split on \n FIRST so LF survives, then per-line:
scalar-range filter strips C0 (except \t), DEL, bidi overrides
(U+202A..E), bidi isolates (U+2066..9), zero-width (U+200B..D,
U+2060), BOM (U+FEFF). Per-line UTF-8 byte cap at 4096 with
`…[line-truncated]` marker.
- headTruncate(_:capBytes:) — byte-count truncation with explicit
`\n…[tool-result-truncated at <N> bytes]` marker. Default cap
8192 matches AGENT-08's ToolResultPacker.modelFacingCapBytes.
- prepareForBoundary(_:capBytes:) — canonical fixed-order
composition, the SOLE entry point from MCPToolDispatcher.
Wrap step is intentionally absent — orchestrator owns it.
18 tests pass.
- MCPServerHandle.claimOrShareRestart(buildTask:): atomic claim-or-share
primitive that runs entirely under handle actor isolation. The closure-
based design ensures the Task is only constructed inside the actor's
serial execution, so concurrent callers can't race past the slot check
and stampede the spawn path.
- MCPClient.callTool: layers the per-server mutex on top —
1. await any in-flight restart; then
2. if still crashed, claim-or-share the slot (claimant runs start();
sharers await the same task);
3. dispatch through the SDK client.
Slot is cleared on both success and failure paths so a future call
after a failed restart can attempt again. No backoff/retry inside the
mutex — RESEARCH A5 contract.
- MCPServerHandle.start: pin PID at terminationHandler wire time so a
STALE handler from a prior helper (post-restart) cannot flip
isCrashed=true on the LIVE process; handleTermination(forPID:) early-
returns when self.process.processIdentifier != pid.
- MCPServerHandle.start: set FD_CLOEXEC on the parent-side pipe ends
(stdin write, stdout read, stderr read) immediately after Process.run().
Foundation creates Pipe FDs without O_CLOEXEC; without this retrofit
the Debug-build ChildSpawnGate fatalErrors when a subsequent spawn
sweeps the parent FD table. CLOEXEC at FD-creation time is the gate's
primary defense per RESEARCH Pitfall #8.
- MCPRestartTests: 4 tests
1. test_singleCrash_followedBy_callTool_succeedsAfterRestart
2. test_concurrentCallsDuringRestart_shareOneRestart (8-way contention,
tally side-channel verifies exactly 2 helper starts: initial + 1 restart)
3. test_inFlightCallTool_duringCrash_throws_serverCrashed (mock_slow
call SIGKILL'd mid-flight)
4. test_100_crashCycles_noFDLeak (baseline=4, final=4, delta=0)
Plan: 05-01 Task 3 of 3.
- MCPServerHandle actor: spawns helper via ChildSpawnGate.shared.prepare(),
sets process.environment = minimalEnvironment, launches with 3 Pipes,
closes child-side pipe ends in the parent (FD-leak guard for restart
cycles), wires StdioTransport with EXPLICIT FileDescriptor args (single-
line so the grep gate matches literally), connects SDK Client, asserts
Initialize.Result.capabilities.tools != nil, drains stderr to log channel,
installs terminationHandler -> handleTermination async path.
- MCPServerHandle.callTool wraps the SDK's (content, isError?) tuple back
into CallTool.Result so downstream consumers see the documented type.
Errors during a call are translated to JarvisMCPError.serverCrashed only
when isCrashed is set; otherwise rethrown verbatim.
- MCPClient actor: registry of MCPServerHandle keyed by server name,
toolToServer name lookup populated from each helper's tools/list.
Restart-mutex layering arrives in Task 3.
- ToolMetadata struct: name + server + requiresConfirmation. Plan 05-04
/05-05 consume this for dispatch routing + confirmation broker.
- MockHelper fixture (test-only, internal to JarvisMCPTests): SDK Server
with mock_echo + mock_slow tools, MOCK_HELPER_CRASH_AFTER env knob to
exit(137) after N calls, MOCK_HELPER_TALLY_PATH side channel for
restart-cycle counting.
- MockHelperBuilder: shells out to `swift build -c release` once per test
run, caches the binary URL.
- Tests: 4 happy-path tests (echo round-trip, spawn-failed, helperMissing,
toolMetadata) + 3 source-level Pitfall #1 regression guards.
Plan: 05-01 Task 2 of 3.