Skip to content

feat(ios-runner): key runner startup failures on typed reasons - #2688

Open
thymikee wants to merge 8 commits into
mainfrom
apex/2680-typed-build-failure-reasons
Open

thymikee wants to merge 8 commits into
mainfrom
apex/2680-typed-build-failure-reasons

Conversation

@thymikee

@thymikee thymikee commented Sep 19, 2026

Copy link
Copy Markdown
Member

Summary

build-for-testing was the one Apple failure shape that reached callers as prose: the catch in runner-artifact.ts threw COMMAND_FAILED with a hint chosen by substring-matching the message plus a JSON dump of details, so nothing downstream could switch on the cause.

classifyRunnerStartupFailure is now the only classifier reachable from that catch, and it extends RUNNER_ERROR_RULES instead of adding a third table. BootFailureReason was rejected: it is provision-kit's boot vocabulary, shared with Android boot, and a build that never produced a binary has no boot to classify. One table is also the only place a row and its reason cannot disagree.

Rows carry buildFailure: { reason, hint }, so reason and advice cannot disagree — signing_no_development_team, signing_provisioning_profile_missing, bundle_identifier_already_registered, signing_unspecified, devtools_security_developer_mode_disabled, and build_failed_unclassified, which keeps RUNNER_CACHE_RECOVERY_HINT instead of claiming a cause. Matching reads only what xcodebuild published — message, stdout, stderr — never the argv we were handed, and never this PR's own emitted reason. signing_style_conflict is withdrawn: conflicting provisioning settings stay unclassified until a capture names the disagreeing setting. resolveRunnerBuildFailureHint is deleted.

A named profile has to earn its reason, and in the same line (#2688 review). The shipped sniffer's bare provisioning profile trigger is a phrase a failing build prints while talking about something else — the codesign invocation, a settings dump, a note about the profile it used — and a second phrase anywhere in the log is no better, because a profile note above an unrelated expired-certificate warning is two lines that never met. So the table gains toolTextLineIncludesAll, the profile rows require Xcode's own complaint on the profile's line (IDEProvisioningErrorDomain naming the profile, doesn't include, has expired), and a mention that says nothing keeps the cache-recovery advice. signing_no_profiles_for_bundle_id shares the PROFILE_UNUSABLE advice constant rather than spelling the same hint out a third time.

$ agent-device prepare ios-runner --platform ios --json
{"hint":"Configure signing in Xcode or set AGENT_DEVICE_IOS_TEAM_ID …",
 "details":{"reason":"signing_no_development_team"}}

Closes #2680. 86 files, 443 production lines.

Validation

At bcfed35b4c (rebased onto main fc59620932): build, format, check:quick, check:layering, check:gate-manifest, check:command-docs, check:production-exports and apple-runner (504) pass, as does the unit-core cli-schema, help-conformance and eager-closure ratchet lane and fallow audit --base origin/main over the stack. Restoring whole-details matching fails 3 tests — the argv fixture, its focused negative, and self-match; reverting the expired row to a whole-log has expired match fails profile-note-above-an-expired-certificate. check:affected --run stays with CI.

Risk: live evidence pending — commands in .device-evidence/CHECKLIST.md. Fixtures declare captured, shipped-sniff-trigger or invented-shape with xcodeVersion: unobserved, so nothing transcribed reads as observed; the three narrowed profile rows are invented-shape until Phase B records which line xcodebuild prints, and the checklist now asks for the line breaks because which line carried which phrase is the evidence.

@github-actions

github-actions Bot commented Sep 19, 2026

Copy link
Copy Markdown
PR Preview Action v1.8.1

QR code for preview link

🚀 View preview at
https://callstack.github.io/agent-device/pr-preview/pr-2688/

Built to branch gh-pages at 2026-09-21 06:54 UTC.
Preview will be ready when the GitHub Pages deployment is complete.

@github-actions

github-actions Bot commented Sep 19, 2026

Copy link
Copy Markdown

Size Report

Metric Base Current Diff
Installed (including dependencies) 4.67 MB 4.67 MB +3.1 kB
Package (unpacked) 4.67 MB 4.67 MB +3.1 kB
Package (download) 1.39 MB 1.39 MB +1.0 kB

Startup median (7 runs, lower is better):

Scenario Base Current Diff
CLI --version 28.1 ms 27.9 ms -0.2 ms
CLI --help 78.5 ms 82.5 ms +4.0 ms

@thymikee

Copy link
Copy Markdown
Member Author

Reviewed at 6342a18.

The typed reasons change the error envelope on the physical-device build-for-testing route, but every new fixture is transcribed, not captured, and the PR body says live evidence is still pending. #2680 lists device evidence with a commit SHA as a done criterion. Until then we do not know that real Xcode output matches these rows and does not fall through to build_failed_unclassified. Could you run agent-device prepare ios-runner --platform ios --device <iPhone> --json on the pushed head twice: once with AGENT_DEVICE_IOS_TEAM_ID unset (expect details.reason: signing_no_development_team), and once with a bundle id that another team owns (expect bundle_identifier_already_registered)? Please paste both JSON envelopes with the hint, logPath and xcodebuild -version, and mark the matching fixtures as captured.

Could .device-evidence/CHECKLIST.md go? It adds a top-level directory that is not on main, and a doc comment in runner-startup-failure-fixtures.ts points to it; the commands fit in the PR body.

I did not find a smaller design: the new rows reuse RUNNER_ERROR_RULES, and the change is about 198 net production lines. Optional cleanups: put the duplicated bundle-id and profile hint strings in constants, and let buildDevToolsSecurityRefusal throw once with the reason the probe already knows, not build an AppError only to classify and rebuild it.

CI is green. GitHub reports a conflict with main, so the branch needs a rebase. Next step: the two live device runs on the rebased head.

@thymikee

Copy link
Copy Markdown
Member Author

Reviewed at 98f2870. This follows up on the earlier review at 6342a18 (#2688 (comment)).

The two live physical-device runs asked for at 6342a18 are still missing, and #2680 lists them as a done criterion. Every build-for-testing fixture in runner-startup-failure-fixtures.ts still has provenance shipped-sniff-trigger or invented-shape, with xcodeVersion: 'unobserved'. This delta also cut the fixture sentences down to the bare trigger substrings. So nothing shows that real Xcode output reaches these rows instead of falling through to build_failed_unclassified. If current Xcode words a signing failure differently, the physical-device prepare route would publish build_failed_unclassified and a generic hint for it.

Can you run this on a physical iPhone at the pushed head: node --experimental-strip-types src/bin.ts --json prepare ios-runner --platform ios --device <iPhone>, once with AGENT_DEVICE_IOS_TEAM_ID and AGENT_DEVICE_IOS_PROVISIONING_PROFILE unset, and once with AGENT_DEVICE_IOS_BUNDLE_ID set to a bundle id that another team owns? The first should fail with COMMAND_FAILED and details.reason: 'signing_no_development_team', plus a hint and logPath. The second should fail with details.reason: 'bundle_identifier_already_registered'. Please paste both JSON errors with the commit SHA and xcodebuild -version, then update the two matching fixtures with the captured text and mark them captured.

Not blocking: the PR description says 86 files and 443 production lines, but this diff touches 11 files with +264/-43 production lines, and the earlier question about .device-evidence/CHECKLIST.md is still open because main has no .device-evidence directory.

CI is green on all 5 checks. The branch conflicts with main and needs a rebase.

Next: post the two physical-device prepare ios-runner --json captures with reason, hint, logPath, SHA and xcodebuild -version, mark those fixtures captured, and rebase onto main.

@thymikee
thymikee added this pull request to stack #2697 September 19, 2026 18:54
@thymikee
thymikee force-pushed the apex/2680-typed-build-failure-reasons branch from 98f2870 to 85bd4de Compare September 19, 2026 19:44
@thymikee

Copy link
Copy Markdown
Member Author

At 85bd4de this is still the same rebase plus a test-helper extraction; it adds no new captures. Every build-for-testing fixture in packages/platform-apple/src/runner/tests/runner-startup-failure-fixtures.ts#L56 is still shipped-sniff-trigger or invented-shape with xcodeVersion 'unobserved', so nothing shows real Xcode output actually reaches these rows. Two rows are unproven in opposite directions: signing_provisioning_profile_unusable matches the bare substring "provisioning profile" anywhere in message/stdout/stderr, and no fixture drives it, so a build log that merely mentions a profile can now publish signing_provisioning_profile_missing, which installation.md tells consumers to branch on; meanwhile real signing prose that differs from the trimmed trigger text falls through to build_failed_unclassified with the generic recovery advice. On the physical-device prepare route this means a caller can get a typed reason that names the wrong cause, or no cause, while the docs invite branching on it.

Can you run, on this pushed head, node --experimental-strip-types src/bin.ts --json prepare ios-runner --platform ios --device <iPhone> twice: once with AGENT_DEVICE_IOS_TEAM_ID and AGENT_DEVICE_IOS_PROVISIONING_PROFILE unset (expecting COMMAND_FAILED with details.reason signing_no_development_team), and once with AGENT_DEVICE_IOS_BUNDLE_ID set to an identifier another team owns (expecting bundle_identifier_already_registered)? Please paste both full JSON envelopes (reason, hint, logPath), the commit SHA, and xcodebuild -version, mark the two matching fixtures as captured with the real sentences, and say which of the two profile rows the captured text actually hits so the bare "provisioning profile" row is either narrowed or backed by a fixture.

Not blocking: the rebase onto 0ebd254 (#2689) left the PR body's "declared twice" rationale stale now that BootFailureReason has one declaration in provision-kit/src/boot-diagnostics.ts#L5 with runner/host.ts only delegating, so the PR body could drop that clause and keep the vocabulary argument, but this can be taken or left.

I did not run any test locally, so the claim that the moved assertions still fail without the fix rests on reading the hunks, not a run. I could not verify real Xcode wording for any rule since no capture exists in the branch. Repo Guards job 105956422148 shows CANCELLED with no failed-step log, which looks like a run superseded by the rebase push, but the diff does touch what that job exercises (a new top-level .device-evidence/ directory, a new platform-apple module, and docs), so the cancellation cannot be read as unrelated; it simply carries no result and needs a re-run on 85bd4de.

Before this can merge: post the two physical-device prepare captures with reason, hint, logPath, commit SHA, and xcodebuild -version, mark those fixtures captured, and get Repo Guards to run to completion on 85bd4de.

@thymikee
thymikee force-pushed the apex/2680-typed-build-failure-reasons branch from 85bd4de to 9621905 Compare September 20, 2026 11:52
@thymikee

Copy link
Copy Markdown
Member Author

Both points addressed, rebased onto main d688926167. New head: 9621905975.

The bare provisioning profile row is gone. You were right that it is the shipped sniffer's trigger, and that a name is not a complaint: xcodebuild prints that phrase in a note: about the profile it used, in a settings dump, and in the codesign invocation. Three rows now require the profile plus the complaint Xcode attaches to it — IDEProvisioningErrorDomain, doesn't include, has expired — sharing one hint constant so the rows cannot drift into three fixes for one lever. profile-mentioned-while-compiling is the negative fixture: a benign mention above a compile error keeps build_failed_unclassified and the cache-recovery hint. The argv-names-a-provisioning-profile fixture still proves the argv half.

All three narrowed rows are invented-shape, not captured: I know the wording families, not this Xcode's sentence for them, so xcodeVersion stays unobserved and .device-evidence/CHECKLIST.md §#2680 now asks the coordinator to record which of those lines actually prints. Un-captured stays un-claimed.

PR body corrected. The BootFailureReason "declared twice" clause is removed — that was true of the runner/host.ts mirror before #2689, not of main today. The argument left standing is the one that survives: it is provision-kit's boot vocabulary shared with Android, and a build that never produced a binary has no boot to classify.

Gates at the new head: build, format, check:quick, check:layering, check:gate-manifest, check:command-docs, check:production-exports, apple-runner (503) and the unit-core help-conformance/eager-closure lane pass.

Not done, and not runnable from here: the live prepare ios-runner captures in §#2680 (items 1–4) are still owed by the device-evidence coordinator, so no fixture in this PR says captured.

@thymikee

Copy link
Copy Markdown
Member Author

This is a follow-up on the earlier review (85bd4de, #2688 (comment)). Reviewed at 9621905.

Row 471 in https://github.com/callstack/agent-device/blob/9621905/packages/platform-apple/src/runner/runner-contract.ts#L471 only requires the substring 'expired', but toolTextIncludesAll scans the whole captured build log (execFailureDetails attaches full stdout/stderr, see packages/host-kit/src/internal/exec.ts#L703), so the pair can match across unrelated lines. A log with the benign note: Using provisioning profile "X" line plus any unrelated expiry sentence, such as an expired signing certificate or Your session has expired, satisfies both substrings and publishes signing_provisioning_profile_missing. A caller on the physical-device prepare ios-runner route that branches on details.reason would then be told to reinstall a profile for a certificate or session problem, which is the same wrong-lever failure this PR set out to remove. Can a profile row require a phrase that can only be a complaint about the profile itself, since the matcher is not line-scoped? Concretely, can row 471 require "has expired" (keeping "doesn't include" and the IDEProvisioningErrorDomain pair), with a negative fixture that pairs the benign profile note with an unrelated expired-certificate line and expects build_failed_unclassified?

All build-for-testing fixtures, including the three new profile rows, carry provenance 'invented-shape' or 'shipped-sniff-trigger' with xcodeVersion 'unobserved' (packages/platform-apple/src/runner/tests/runner-startup-failure-fixtures.ts#L151), so nothing at 9621905 shows real xcodebuild output reaching these rows instead of falling through to build_failed_unclassified. Can this be validated on a physical iPhone at 9621905 by running node --experimental-strip-types src/bin.ts --json prepare ios-runner --platform ios --device <iPhone> for each case, pasting the full JSON envelope (details.reason, hint, logPath), xcodebuild -version, and the verbatim xcodebuild error lines: no AGENT_DEVICE_IOS_TEAM_ID/AGENT_DEVICE_IOS_PROVISIONING_PROFILE set, expecting signing_no_development_team; AGENT_DEVICE_IOS_BUNDLE_ID owned by another team, expecting bundle_identifier_already_registered; and AGENT_DEVICE_IOS_PROVISIONING_PROFILE set to a profile that doesn't cover the bundle id, reporting which of IDEProvisioningErrorDomain, "doesn't include", or "has expired" actually printed? Once these are captured, the matching fixtures can be marked captured.

Not blocking: the signing_no_profiles_for_bundle_id row at runner-contract.ts#L438-441 spells out the same reason+hint pair as literals instead of using the PROFILE_UNUSABLE constant added at line 276, so it could point at PROFILE_UNUSABLE too, but this can be taken or left.

No tests or device commands were run for this review; the claim that a profile-mentioned-while-compiling log matched the pre-delta bare row rests on reading the removed code, not on a run. The real Xcode wording for all three narrowed rows is unverified, so whether "doesn't include" arrives with an ASCII apostrophe, versus a typographic one that would fall through row 463 to the catch-all, is unknown. The author's gate list (build, format, check:*, apple-runner 503, unit-core) was not re-verified at this head.

Smoke Tests failed at test/integration/ios-simulator-e2e/live-assertions.ts#L52, with id="automation-longpress" not visible after scrolling, after the runner built and 14 other assertions passed; this looks unrelated, since the head commit touches only the build-failure rule rows, fixtures and CHECKLIST, which run only when xcodebuild build-for-testing throws, and the runner-session.ts step rename plus the DevToolsSecurity preflight import return early for kind !== 'device', with no hunk touching snapshot, scroll, or selector resolution.

The path to merge is narrowing row 471 to "has expired" with a cross-line negative fixture, then posting the three physical-device prepare captures at 9621905 and marking the matching fixtures captured.

@thymikee
thymikee force-pushed the apex/2680-typed-build-failure-reasons branch from 9621905 to bcfed35 Compare September 20, 2026 13:53
@thymikee

Copy link
Copy Markdown
Member Author

Both items in, rebased onto main fc59620932. New head: bcfed35b4c.

The expiry row no longer reads a bare expired, and one line is now the unit of evidence. Narrowing expiredhas expired alone would not have fixed what you pointed at, so I went one step further: toolTextIncludesAll is a whole-log AND, and a whole-log AND cannot express "this complaint is about this profile". A note: about the profile the build used, plus The certificate "Apple Development: …" has expired. three lines below it, still satisfies ['provisioning profile', 'has expired']. The rules table therefore gained a sibling axis, toolTextLineIncludesAll (packages/platform-apple/src/runner/runner-contract.ts:188), matched line by line over the same tool-text haystack, and all three profile rows use it: IDEProvisioningErrorDomain naming the profile, doesn't include, has expired. Nothing else in the table changed its axis.

The cross-line negative is in and it is the proof, not decoration. profile-note-above-an-expired-certificate pairs a benign profile note with an unrelated expired-certificate warning and asserts build_failed_unclassified with cache-recovery advice. Reverting just that row to toolTextIncludesAll makes it fail (× a build-for-testing failure publishes build_failed_unclassified for profile-note-above-an-expired-certificate); the row as shipped passes. profile-xcode-signing-error was reworded while I was there: it now puts the domain and the profile on the same line, because Xcode prints the profile inside the domain's own description and two lines that never met are two facts, not one complaint.

Constant reuse done. signing_no_profiles_for_bundle_id points at PROFILE_UNUSABLE (runner-contract.ts:276); the reason and hint literals are gone. That row keeps toolTextIncludesAll deliberately — "No profiles for 'com.example' were found" is the complaint and the subject in one sentence.

Fixtures stay honest: all three narrowed rows are still invented-shape with xcodeVersion: 'unobserved'. .device-evidence/CHECKLIST.md §#2680 now tells the coordinator to paste the line breaks, since which line carried which phrase is what would promote them.

Gates at the new head: build, format, check:quick, check:layering, check:gate-manifest, check:command-docs, check:production-exports, apple-runner (504) and the unit-core help-conformance/eager-closure lane pass; fallow audit --base origin/main is clean across the stack.

Still owed, and outside what I can run: the §#2680 prepare captures (no-team, foreign bundle id, missing profile, pinned-profile pairing). No fixture here says captured.

@thymikee

Copy link
Copy Markdown
Member Author

bcfed35 looks good and the code verdict is clean. Narrowing the profile rules so a name and its complaint must share one line is the right answer to the cross-line match from the last round, and it reuses the existing haystack instead of adding a second source of truth. CI is green with 20 checks passing, and the unit lane the new fixtures drive covers the changed classification path. No conflicts are known.

Not blocking: bundle_identifier_unavailable still pairs its two phrases across the whole log rather than one line (https://github.com/callstack/agent-device/blob/bcfed35/packages/platform-apple/src/runner/runner-contract.ts#L415), so one row is left on the old axis even though the rule table is what enumerates those sites; and the profile-xcode-signing-error fixture was edited to repeat the profile name inside the IDEProvisioningErrorDomain line (https://github.com/callstack/agent-device/blob/bcfed35/packages/platform-apple/src/runner/__tests__/runner-startup-failure-fixtures.ts#L161) rather than proved against a capture. Both can be taken or left.

One limit worth recording: the new fixture's pre-fix red state comes from reading the 9621905 rule table against the fixture output, not from a run, and the real xcodebuild wording for the IDEProvisioningErrorDomain, "doesn't include" and "has expired" diagnostics is still unobserved in this repo. Whether those phrases really share one line on a live signing failure is what the Phase B device capture in the checklist would settle, and that capture is also what would promote the three profile fixtures from invented shape to captured.

@thymikee thymikee added the ready-for-human Valid work that needs human implementation, judgment, or maintainer merge label Sep 20, 2026
…dule

runner-session.ts is 1,181 lines, so the next behavior owed a split first. The
host developer-tools security probe is a self-contained preflight: it reads a
macOS setting for this machine, not the iPhone's Developer Mode toggle, and
today nothing in its name or its home says which machine it speaks for.

Pure move. The startup step key is renamed to say what it actually verifies, and
the existing coverage in runner-session.test.ts carries unchanged because the
probe consumes the same runner host port.
A `build-for-testing` failure was the one Apple failure shape with no typed
reason: `runner-artifact.ts` threw COMMAND_FAILED with a hint picked by
substring-matching the lowercased message plus a JSON dump of the details, and
nothing downstream could switch on which signing problem it was. `AGENTS.md`
keys behavior on typed reasons and details, never error text, so the build path
was the exception to the rule.

`classifyRunnerStartupFailure` is now the one classifier reachable from that
catch, and it reads `RUNNER_ERROR_RULES` rather than adding a second table: a row
either carries recovery verdicts, a `buildFailure` reason and hint, or both. The
reason and the hint beside it therefore cannot disagree, and an unproven cause is
never claimed -- `build_failed_unclassified` keeps the cache-recovery hint it
already gave. The host `DevToolsSecurity` refusal, which had a hint and no reason
at all, now publishes its own reason keyed on the typed status it read, and that
reason says host so it can never be read as the device's Developer Mode toggle.

`resolveRunnerBuildFailureHint` is gone, not kept as a pass-through. Reasons are
recorded as fixtures carrying the tool output, the command, the Xcode, and how
the line reached the file; each one is driven through the real build catch and
asserted on the normalized envelope, because every case is COMMAND_FAILED and the
reason is the assertion.
…ce needs it

A static edge from `runner-session.ts` to the new module grew three Apple facade
closures by one module each (app-lifecycle, doctor, runner-operations), which the
eager-import-closure ratchet refuses: the runner subtree is eagerly evaluated to
answer a simulator request, and this preflight only ever runs for a physical
iOS device. Function-scoped import keeps the preflight where it belongs without
paying for it on the paths that can never use it.
…ublished

The startup rules read the whole `details` bag as JSON, and `execFailureDetails` puts
`cmd`/`args` in that bag. A caller who pinned a profile therefore handed the profile rule
its trigger: an unrelated Swift compile error classified as
`signing_provisioning_profile_missing` and lost the cache-recovery hint. The same bag holds
the `reason` and `hint` this classifier publishes, so a re-wrapped failure would match its
own verdict. `runnerToolText` now builds the one haystack a startup rule may read — our
message plus the tool's stdout and stderr — and the argv case has a fixture that proves a
pinned profile is not evidence (#2680).

Reading only `details` also dropped the message on the floor, so a failure the exec layer
raised as a plain `Error` (which the catch wraps with `String(err)`) became unclassified
while the same sentence in `stdout` classified. The message is in the haystack now, and a
`message-only` fixture keeps both carriers at parity.

`signing_style_conflict` is removed rather than kept as a guess: the hint named a
`CODE_SIGN_STYLE` env lever that does not exist and claimed a cause nothing captured. The
conflicting-settings line keeps a row so the profile row below cannot answer it with
missing-profile advice, but it publishes `build_failed_unclassified` until a capture shows
which setting disagrees and which lever clears it. Versioned help and the installation docs
no longer list the withdrawn reason, and `.device-evidence/CHECKLIST.md` gained the capture
that would let a follow-up claim it.

Provenance got honest vocabulary. `OBSERVED_ON` stamped invented sentences as observed on
Xcode 26.2, and one invocation was recorded as the producer of seven configurations that
cannot coexist; nothing here was captured, so entries now say `shipped-sniff-trigger` (the
matched substrings shipped before #2680, sentence reconstructed) or `invented-shape`, carry
`xcodeVersion: 'unobserved'`, and omit `command` until a run records one.
…ure assertion body

#2689 derived the runner host port from the modules it fronts, so `ExecResult` is read
from `@agent-device/host-kit/command` now rather than restated in `runner/host.ts`.

The per-fixture assertion body had grown past the complexity the Fallow audit allows a
changed file: the envelope checks move into `assertFailureEnvelope` and the tool-output
check into `assertToolOutputReachable`, so each recorded shape is still asserted through
the same path and the test body reads as one call.
One bare `provisioning profile` substring was the shipped sniffer's trigger, and it is a phrase a
failing build prints while talking about something else: the codesign invocation, a settings dump, a
note about the profile it used. Each row now requires the profile plus the complaint Xcode attaches
to it — its `IDEProvisioningErrorDomain` diagnostic, "doesn't include", "has expired" — and a
mention that says nothing keeps the cache-recovery advice it already had.
A whole-log AND proves two phrases exist, not that one qualifies the other: a note about the profile
the build used, three lines above an unrelated expired-certificate warning, published
`signing_provisioning_profile_missing` for a profile that was fine. The profile rows now read one
line at a time and the expiry row asks for Xcode's own `has expired` phrase. The rules table grows a
`toolTextLineIncludesAll` axis beside `toolTextIncludesAll`, and the bundle-identifier row that also
paired two phrases moves onto it so no startup row is left reading a whole log; its cross-line
negative rides along. `signing_no_profiles_for_bundle_id` shares the `PROFILE_UNUSABLE` advice
instead of spelling it out a third time.

`.device-evidence/CHECKLIST.md` belongs to the merged #2682 lane, so this stack's capture sheet lives
beside it as `CHECKLIST-runner-failures.md`.
@thymikee
thymikee force-pushed the apex/2680-typed-build-failure-reasons branch from bcfed35 to 15808ae Compare September 20, 2026 16:48
@thymikee

Copy link
Copy Markdown
Member Author

Rebased onto main @ 921a2cb3ac (post-#2682). New head: 15808ae228 (was bcfed35b4c).

The one nit — the bundle-identifier row was the last whole-log AND. bundle_identifier_unavailable moved from toolTextIncludesAll: ['app identifier', 'not available'] to toolTextLineIncludesAll, so the identifier and its availability have to meet in one sentence. It is the only multi-phrase row left on the whole-log axis; a grep for toolTextIncludesAll: [...,...] in runner-contract.ts now comes back empty.

A cross-line negative rides along, app-identifier-and-availability-in-different-lines: one line faults the identifier, another says something else is not available, and neither pairs them. Red with the row back on the whole-log axis, green as shipped, and marked invented-shape / xcodeVersion: 'unobserved' like the other un-captured shapes.

The .device-evidence/CHECKLIST.md collision is resolved in #2682's favour. That file is the merged lane's sheet and this PR no longer touches it; the runner-failure capture sheet lives beside it as .device-evidence/CHECKLIST-runner-failures.md, and the fixture provenance comment points there.

Gates at this head: check:quick, check:layering, check:production-exports clean; vitest --project apple-runner 54 files / 543 tests; test:unit 1363 files / 10767 tests.

Still owed, and unchanged in that: the physical-device captures in .device-evidence/CHECKLIST-runner-failures.md are coordinator Phase B on the connected iPhone. Nothing here was verified on hardware at this head.

@thymikee

Copy link
Copy Markdown
Member Author

At 15808ae the code delta is clean and all 20 checks pass, so nothing in the code is holding this back.

What is still missing is the Phase B physical-iPhone capture, unchanged since the bcfed35 round — and this delta is what makes it load-bearing. The four invented-shape rows still carry xcodeVersion: 'unobserved', while bundle_identifier_unavailable and the two profile rows now require their phrases on a single line. If real xcodebuild output wraps App Identifier away from not available, a failure that used to be classified falls through to build_failed_unclassified, and only a real run settles which way it wraps. Please run agent-device prepare ios-runner --platform ios --device "<iPhone>" for each invented-shape row in .device-evidence/CHECKLIST-runner-failures.md, then paste the raw xcodebuild stdout with its line breaks intact plus xcodebuild -version, showing that App Identifier + not available, and Provisioning profile + (IDEProvisioningErrorDomain | doesn't include | has expired), each land on one line. A re-wrapped or paraphrased transcript will not prove it.

For that reason I have taken ready-for-human back off. My round-1 comment asked for the same captures and should not have applied it ahead of them; it goes back on with the captures, not with a code change.

Two things I did not check here: the apple-runner suite was not run, so the claim that the new cross-line fixture is red under the pre-delta rule is read from the old and new match functions rather than executed; and the single-phrase whole-log rows and the profile rows themselves are outside this delta, so they keep their earlier verdict.

Not blocking: the PR body still points at .device-evidence/CHECKLIST.md, which is now #2682's sheet rather than this stack's CHECKLIST-runner-failures.md, and still says "86 files, 443 production lines" where this head measures 11 files and +320.

@thymikee thymikee removed the ready-for-human Valid work that needs human implementation, judgment, or maintainer merge label Sep 20, 2026
@thymikee

Copy link
Copy Markdown
Member Author

Phase B — signing-failure captures on a physical host (coordinator-run)

Host: thymikee-iphone (iPhone 17 Pro), Xcode 26.2 / Build 17C52, Apple Development identity team 7XSDF3H789. Head 15808ae228. Runner build path forces a device build with -allowProvisioningUpdates.

Result: only signing_provisioning_profile_missing is reachable with a real build on this account

With a clean derived path (fresh AGENT_DEVICE_IOS_RUNNER_DERIVED_PATH, no cache reuse), prepare ios-runner --platform ios fails and the classifier publishes:

reason: signing_provisioning_profile_missing
hint:   Install/select a valid iOS provisioning profile, or set AGENT_DEVICE_IOS_PROVISIONING_PROFILE.

Verbatim xcodebuild errors (the matched source text):

AgentDeviceRunner.xcodeproj: error: No Accounts: Add a new account in Accounts settings. (target 'AgentDeviceRunner')
AgentDeviceRunner.xcodeproj: error: No Accounts: Add a new account in Accounts settings. (target 'AgentDeviceRunnerUITests')
AgentDeviceRunner.xcodeproj: error: No profiles for 'com.callstack.agentdevice.runner' were found: Xcode couldn't find any iOS App Development provisioning profiles matching 'com.callstack.agentdevice.runner'. (target 'AgentDeviceRunner')
AgentDeviceRunner.xcodeproj: error: No profiles for 'com.callstack.agentdevice.runner.uitests.xctrunner' were found: ... (target 'AgentDeviceRunnerUITests')

So the signing_provisioning_profile_missing row is now proven against real Xcode 26.2 output (No profiles for '<id>' were found), and the reason it emits matches the classifier.

Happy path (control): signing succeeds

With the default derived path and automatic signing against the signed-in account, the same command builds and starts successfully (com.callstack.agentdevice.runner + .uitests.xctrunner signed with team 7XSDF3H789). So signing works end-to-end on this machine.

Two rows are NOT inducible on this account, and why

  • signing_no_development_team — unsetting AGENT_DEVICE_IOS_TEAM_ID does not fail: automatic signing resolves team 7XSDF3H789 from the installed identity/profile and the build succeeds. A bogus TEAM_ID/account-less build does not surface a team-specific error; it short-circuits earlier at No Accounts, which classifies as signing_provisioning_profile_missing.
  • bundle_identifier_already_registeredAGENT_DEVICE_IOS_BUNDLE_ID=com.apple.TestFlight (and a fresh derived path) does not reach the App-ID-registration stage: either the build succeeds under -allowProvisioningUpdates, or, without account context, it dies earlier at No Accounts / No profiles ... were found. The registration-conflict text this row keys on is never produced here.

Implication for the two invented-shape fixtures: these two rows cannot be upgraded from synthetic to captured on a valid-signing host with an active account. Reproducing them needs either an Apple account that is signed in but has no development team (for the no-team row) or a bundle id registered to a different team with automatic signing able to reach registration (for the already-registered row). I can capture both if pointed at such an account/machine; otherwise I recommend documenting them as environment-gated with the mechanism above and keeping the synthetic fixtures for the classifier unit coverage.

@thymikee

Copy link
Copy Markdown
Member Author

Thank you — these captures close the gap I held this on at 15808ae. The real Xcode 26.2 output proves the No profiles for '<id>' were found row, and it also answers the worry behind my request: that diagnostic arrives as one long error: line rather than wrapped, so the sibling rows in the same provisioning family are far less likely to split the way I feared.

Your account-level explanation for signing_no_development_team and bundle_identifier_already_registered is the right call, and I would take the option you suggest. Please record that environment gating beside those fixtures so the rows read as blocked rather than unexamined, and include bundle_identifier_unavailable, doesn't include and has expired in the same note, since each of them needs the same kind of differently-configured account to induce.

I put ready-for-human back on, as I said it would go back with the captures.

@thymikee thymikee added the ready-for-human Valid work that needs human implementation, judgment, or maintainer merge label Sep 20, 2026
… the rest need

Co-Authored-By: Apex <noreply@callstack.com>
@thymikee

Copy link
Copy Markdown
Member Author

Reviewed at 12de0a8. The delta does what I asked at 15808ae. The No profiles for '<id>' were found fixture now carries the real Xcode 26.2 output. The rows that need a differently configured account stay marked as not captured, and the account gating is written beside them. The classifier does not change in this delta.

The Smoke Tests failure is in the iOS simulator scroll check (the long-press test element did not become visible after scrolling). This PR changes only runner startup-failure classification, so the failure looks unrelated.

There are no conflicts, and ready-for-human stays on.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ready-for-human Valid work that needs human implementation, judgment, or maintainer merge

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Key runner build failures on typed reasons instead of signing-substring hints

1 participant