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.
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.
- 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.
- 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.