Phase 1 REVIEW.md WR-11: `FeatureFlags.isEnabled(_ key: String)` fell
through to `false` on any typo — `"orpheusTtsEnabled"` (wrong case),
`"orpheus_tts_enabled"` (wrong separator), etc. Phase 6 (Orpheus /
WhisperKit) wiring depends on these flags; a silently-disabled
feature caused by a typo is nearly impossible to debug.
packages/Config/Sources/Config/FeatureFlags.swift
- Add `public enum FeatureFlag: String, CaseIterable, Sendable` with
cases `orpheusTTSEnabled`, `whisperKitSTTEnabled` (both known keys
per REQUIREMENTS.md).
- Add `isEnabled(_ flag: FeatureFlag)` as the canonical typed path.
- Rename the string-keyed lookup to `isEnabledDynamic(_:)` so
production callers get a compile error on typos — the dynamic
string path is retained only for the dev overlay's "list all
flags" affordance.
packages/Config/Tests/ConfigTests/PerTurnSnapshotTests.swift
- Update two existing tests to use the typed-enum API.
- New `test_featureFlagEnumRawValueMatchesDecoder` — regression guard
that the enum cases' rawValue matches the JSON key the decoder
sees, so a rename to `orpheusTtsEnabled` fails the build, not
silently breaks feature detection.
No production callers of the old string-keyed `isEnabled` exist
outside tests — no migration needed elsewhere.
Phase 1 REVIEW.md WR-04: `ConfigError.futureSchema(version:)` and
`ConfigError.unknownSchemaVersion(Int)` both carried a single Int
whose meaning depended on context — `futureSchema(2)` when migrating
`from: 2, to: 1` reports "2" but users read it as "your target is 2"
rather than "you have v2, this build only supports v1". For
multi-step migrations, `unknownSchemaVersion` also reported an
intermediate version, not the user's file version.
packages/Config/Sources/Config/ConfigError.swift
- `futureSchema(have:supports:)`: `have` is the user's version,
`supports` is `SchemaMigrator.currentVersion`.
- `unknownSchemaVersion(have:supports:)`: `have` is the intermediate
source version at the failing step, `supports` is the migration
target. Callers can report "config is at v3, Jarvis supports up to
v2" unambiguously.
packages/Config/Sources/Config/SchemaMigrator.swift
- Propagate `to:` through `migrateStep` so the thrown
`unknownSchemaVersion` carries both numbers.
packages/Config/Tests/ConfigTests/SchemaMigratorTests.swift
- Update both negative-path assertions to the new enum shape.
Phase 1 REVIEW.md WR-03 (AGENT-05): `OllamaConfig.validateHost` only
string-matched the URL host against `{127.0.0.1, localhost, ::1}` — a
hijacked `/etc/hosts` or resolver that points `localhost` to a
non-loopback IP would pass validation, violating AGENT-05's "local
only" intent. IPv6 bracket handling (`[::1]`) was also inconsistent
across macOS versions.
packages/Config/Sources/Config/OllamaConfig.swift
- Normalize IPv6-bracketed hosts (`[::1]` → `::1`) before the string
allowlist check.
- For IP literals (127.0.0.1, ::1), skip resolution — loopback by
construction.
- For `localhost`, run `getaddrinfo` with AF_UNSPEC + AI_ADDRCONFIG
and assert every resolved address is loopback:
* IPv4: top octet == 0x7f (127.0.0.0/8)
* IPv6: 15 zero bytes followed by 0x01 (::1)
- Fail closed on `getaddrinfo` errors or unknown address families.
Existing tests still pass (15/15) — no test changes needed because
`127.0.0.1` short-circuits the getaddrinfo path and the existing
config fixtures all use IP literals, not localhost.
- Split config into LaunchSnapshot (frozen security keys: ollama,
applescript, toolBlocklist, confirmationPolicy, logging) and
PerTurnSnapshot (mutable: provider, tts, stt, featureFlags) per SEC-05
- OllamaConfig.init(from:) rejects non-localhost hosts at decode time
with ConfigError.invalidOllamaHost (AGENT-05)
- AppleScriptPolicy has no skipAllowlist/bypass/regex field (SEC-08);
reflection test ConfigSplitTests.test_noSkipAllowlistKey enforces
- FeatureFlags lives in PerTurnSnapshot only (OBS-05); reflection test
LaunchSnapshotTests.test_featureFlagsNotInLaunchSnapshot enforces
- SchemaMigrator: currentVersion=1, v1->v1 pass-through, future
versions throw ConfigError.futureSchema
- ConfigLoader.loadSnapshots: peek schemaVersion, migrate, decode both
snapshots; wraps DecodingError into ConfigError.malformed
- ConfigStore actor holds launch + streams perTurn updates (file-
watcher wiring is Plan 03 scope)
- Bundled default-config.json resource
- Tests: 15/15 pass covering all behaviors listed in the plan
- Deletes Plan 01 placeholders
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
- packages/Keychain — zero SPM deps (Security.framework at link time only)
- packages/Logging — depends on apple/swift-log 1.5.3+; library product is
named JarvisLogging to avoid collision with swift-log's own Logging module
- packages/Config — depends on Keychain + Logging (product JarvisLogging)
- packages/Shell — depends on Config + Logging; links AppKit,
ServiceManagement, IOKit, Carbon for hotkey/launch-at-login surfaces
Every target (main + test) opts into .swiftLanguageMode(.v6) per D-04 so
data-race errors are compile-time failures, not warnings.
Placeholder sources (one per package) plus one trivial XCTest per package
per D-03 so each package builds standalone and has a passing test. Real
sources land in Plan 02 (Keychain/Config/Logging) and Plan 04 (Shell).
Bus/LLM/MCP/Voice/Memory packages NOT pre-created per D-01 — they arrive
in their owning phases (P2/P4/P5/P6/P7) to keep rebuild scope tight.