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