arcane-cli 1.12.2 โ†’ 1.12.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -9,7 +9,7 @@
9
9
 
10
10
  **Cast spells to ship software** โ€” the methodology layer for AI-assisted development.
11
11
 
12
- AI agents can write your code; Arcane gives them the discipline to *plan it, govern it, test it, review it, and **ship** it.*
12
+ AI agents can write your code; the Arcane framework gives them the discipline to *plan it, govern it, test it, review it, and **ship** it.*
13
13
 
14
14
  [![npm version](https://img.shields.io/npm/v/arcane-cli?style=flat-square&color=6f9fd8&label=npm)](https://www.npmjs.com/package/arcane-cli)
15
15
  [![npm downloads](https://img.shields.io/npm/dm/arcane-cli?style=flat-square&color=6f9fd8)](https://www.npmjs.com/package/arcane-cli)
@@ -30,7 +30,7 @@ npm install -g arcane-cli # then: spell init
30
30
 
31
31
  <div align="center">
32
32
 
33
- **๐Ÿ” opinionated lifecycle** &nbsp;ยท&nbsp; **๐Ÿ“œ <!--count:spells-->41<!--/count:spells--> spells** &nbsp;ยท&nbsp; **๐Ÿค– <!--count:agents-->12<!--/count:agents--> agents** &nbsp;ยท&nbsp; **โš–๏ธ <!--count:governance-->25<!--/count:governance--> governance standards** &nbsp;ยท&nbsp; **๐Ÿ“ markdown-native** &nbsp;ยท&nbsp; **๐Ÿ”Œ any AI client / tracker**
33
+ **๐Ÿ” opinionated lifecycle** &nbsp;ยท&nbsp; **๐Ÿ“œ <!--count:spells-->41<!--/count:spells--> spells** &nbsp;ยท&nbsp; **๐Ÿค– <!--count:agents-->12<!--/count:agents--> agents** &nbsp;ยท&nbsp; **โš–๏ธ <!--count:governance-->25<!--/count:governance--> governance standards** &nbsp;ยท&nbsp; **๐Ÿ“ markdown-native** &nbsp;ยท&nbsp; **๐Ÿ”Œ tested with Claude Code, GitHub Copilot and Codex ยท GitHub or Azure DevOps**
34
34
 
35
35
  </div>
36
36
 
@@ -87,7 +87,7 @@ AI coding agents are great at *generating code*. They're terrible at *shipping s
87
87
 
88
88
  Ask one to "build a feature" and you get a pile of plausible code with no plan, no tests you trust, no review, no governance, and no repeatable path from idea to a merged, deployed change. The gap isn't code generation โ€” it's **methodology**.
89
89
 
90
- **Arcane is that missing layer.** It's a structured, opinionated lifecycle โ€” the **Spell Loop** โ€” plus the governance, agent roles, and templates that make AI-driven development reproducible instead of chaotic. You stay in the driver's seat and cast `spell`s; the framework keeps the work on rails.
90
+ **The Arcane framework is that missing layer.** It's a structured, opinionated lifecycle โ€” the **Spell Loop** โ€” plus the governance, agent roles, and templates that make AI-driven development reproducible instead of chaotic. You stay in the driver's seat and cast `spell`s; the framework keeps the work on rails.
91
91
 
92
92
  ---
93
93
 
@@ -109,7 +109,7 @@ Plus session and operational spells โ€” `/spell-open-session`, `/spell-close-ses
109
109
 
110
110
  ## What's in the box
111
111
 
112
- Arcane isn't a prompt snippet โ€” it's a full framework. Everything installs into your repo as plain, reviewable markdown. Each spell is authored once, in `.arcane/spells/`, and reaches Copilot, Claude Code and Codex through thin generated shims โ€” one source of truth, no per-client copies to drift apart.
112
+ It isn't a prompt snippet โ€” it's a full framework. Everything installs into your repo as plain, reviewable markdown. Each spell is authored once, in `.arcane/spells/`, and reaches Copilot, Claude Code and Codex through thin generated shims โ€” one source of truth, no per-client copies to drift apart.
113
113
 
114
114
  | Layer | What you get |
115
115
  | --- | --- |
@@ -146,6 +146,40 @@ Arcane isn't a prompt snippet โ€” it's a full framework. Everything installs int
146
146
 
147
147
  ---
148
148
 
149
+ ## What the framework does
150
+
151
+ Every line says how it holds, because a rule nobody checks should not read like one that is checked. *CLI:* the `spell` command, or a hook it installs, does it. *Spell step:* your AI client does it when the work runs through the spells. *Convention:* the standards ask for it; nothing checks it.
152
+
153
+ **The memory lives in your repository.**
154
+ - One command installs your standards into any repository: governance documents, spell prompts and agent definitions, with a manifest that records a hash of every file it placed. *CLI*
155
+ - Every session opens by rebuilding context from the repository and closes by writing a dated journal entry and a handoff the next session reads first. *Spell step*
156
+ - Drift is caught at session open, from decision ids that skip a number to installed files that differ from the framework's copies; high-severity drift stops the session until you decide. *Spell step*
157
+
158
+ **Your edits survive updates.**
159
+ - A file you edited is three-way merged with the new version, with conflict markers where the two disagree. Leftover files are reported, and removed only when you ask and only when a recorded hash, or for older installs git history, shows them untouched. *CLI*
160
+ - Updates refuse to run over uncommitted changes or outside a git repository. *CLI*
161
+
162
+ **Done means checked.**
163
+ - At close, every piece of background work is classified succeeded, failed, pending, dispatched or unverifiable; only "succeeded" may be called done. *Spell step*
164
+ - After a merge, the spell checks that each closed issue's change is actually in the merged diff. *Spell step*
165
+
166
+ **Gates that block, and gates that ask.**
167
+ - No commit without your approval in an interactive session: the spell shows the diff and the message, fingerprints that exact pair, and re-checks it right before committing. *Spell step*
168
+ - Blocked means blocked: a pre-push hook refuses, and every remote's push URL is pointed at a sentinel, so `--no-verify` does not help. A remote added later stays live until you re-apply the block; `spell doctor` names it. *CLI*
169
+
170
+ **Agents earn trust.**
171
+ - Define your agents once; the CLI writes them where Claude Code, GitHub Copilot and Codex each expect them. *CLI*
172
+ - Six trust levels per agent per repository, from spectator to archmage. From level four an agent may commit and merge on its own; below that, it queues a pull request for a person. *Spell step ยท Convention*
173
+ - Every agent commit made through the spells names its agent, model, model source and provider in trailers, and the approving person is the committer. *Spell step*
174
+
175
+ **What it does not do.**
176
+ - It does not run your agents or call an AI model. The spells are instructions your AI client follows; the CLI installs them, keeps them current and checks the repository.
177
+ - It does not intercept raw git. A commit you type by hand skips the spells' approval and test gates.
178
+ - The credential scan looks for known patterns: it can find a leak, never prove a commit is clean.
179
+ - The push controls stop an accidental push, not a determined operator.
180
+
181
+ ---
182
+
149
183
  ## Meet the agents
150
184
 
151
185
  Twelve legendary agents ship with Arcane โ€” **the Arcanos**. Each is a **role** with a **code name**; within Arcane the two are aliases: summon `Merlin` or summon `the architect` and you get the same agent. Every namesake is public domain โ€” golden-age stage magicians, myth, and classic literature. Run `spell agents init` and choose how they're named โ€” the **Arcanos** roster below, plain **generic** role labels, **random** names, or your own **custom** universe.
@@ -328,7 +362,7 @@ If a repository that opted in ever lands on a machine with no tier, `spell updat
328
362
 
329
363
  ## Philosophy
330
364
 
331
- - **Opinionated about *how*, pluggable about *which*.** Arcane is strict about the lifecycle, governance, and commit discipline โ€” and adapter-friendly about your tools (any AI client, any tracker, GitHub or Azure DevOps).
365
+ - **Opinionated about *how*, pluggable about *which*.** The framework is strict about the lifecycle, governance, and commit discipline โ€” and adapter-friendly about your tools (tested with Claude Code, GitHub Copilot and Codex; GitHub or Azure DevOps).
332
366
  - **Markdown is the source of truth.** Everything is plain, reviewable, version-controlled text. No lock-in, no opaque state.
333
367
  - **Reproducible by design.** Spinning up a new project should mean running `spell init`, not redoing months of process work.
334
368
 
@@ -336,7 +370,7 @@ If a repository that opted in ever lands on a machine with no tier, `spell updat
336
370
 
337
371
  ## Built in public
338
372
 
339
- Arcane is built *with Arcane*. From this repo's first commit forward, development happens in the open โ€” planned, implemented, reviewed, and shipped using its own spells, with each session logged. Want to see how the methodology actually works? Watch the commit history.
373
+ The Arcane framework is built *with itself*. From this repo's first commit forward, development happens in the open โ€” planned, implemented, reviewed, and shipped using its own spells, with each session logged. Want to see how the methodology actually works? Watch the commit history.
340
374
 
341
375
  ---
342
376
 
@@ -364,5 +398,5 @@ npm run typecheck:all # type-check src/, scripts/, test/ and the root configs
364
398
  > The *Arcane* name, logo, and agent art are brand assets of Code Magician LLC; the source code is MIT-licensed.
365
399
 
366
400
  <div align="center">
367
- <sub>Built by <a href="https://github.com/codemagicianhq">Code Magician</a> ยท <code>spell</code> your way to shipped.</sub>
401
+ <sub>Built by <a href="https://codemagician.ai">Code Magician</a> ยท <code>spell</code> your way to shipped.</sub>
368
402
  </div>
@@ -13,7 +13,8 @@ Durable facts about how the Apple App Store and Google Play review consoles and
13
13
  actually behave โ€” true regardless of which build tool produced the binary. These are platform
14
14
  properties, not Expo/EAS-specific tooling behavior; the EAS-specific workflow that consumes these rules
15
15
  lives in `spell-eas-store-deploy`. Sourced from two real dogfooding runs (a first-ever App Store
16
- submission, then a from-zero Google Play listing to a live production release) โ€” every rule here was hit
16
+ submission, then a from-zero Google Play listing to a live production release) and, for the two release
17
+ gates (MR-15, MR-16), a consumer's later store release reported on 2026-10-18 โ€” every rule here was hit
17
18
  for real, not derived from documentation alone.
18
19
 
19
20
  ## Executive Summary
@@ -30,6 +31,10 @@ for real, not derived from documentation alone.
30
31
  - Two platform-specific binary/UI constraints (notification icon requirements, a native dialog's button
31
32
  cap) have no equivalent on the other platform and are easy to carry over incorrectly from iOS-first
32
33
  development.
34
+ - Two release gates hold across releases rather than within one: every store version still on phones
35
+ keeps an update path (MR-15), and every build machine whose Xcode can change is pinned to one (MR-16).
36
+ Both failures are silent: older binaries simply stop receiving updates, or a binary built from
37
+ unchanged code crashes at launch.
33
38
 
34
39
  ---
35
40
 
@@ -51,6 +56,8 @@ for real, not derived from documentation alone.
51
56
  | MR-12 | Audit the permissions the built artifact actually declares, not what the source code appears to use. |
52
57
  | MR-13 | Android push-notification icons need a dedicated monochrome asset shipped in a native build; there is no OTA path for this. |
53
58
  | MR-14 | Android's native `AlertDialog` silently caps at three buttons; a menu built on `Alert.alert` past that cap loses the overflow, typically the dismiss action. |
59
+ | MR-15 | Every store version still in use needs an update path; audit the binary versions still on phones before each store release. |
60
+ | MR-16 | Pin the Xcode on any build machine where it can change between builds; fail the build when another version is active. |
54
61
 
55
62
  ---
56
63
 
@@ -215,6 +222,40 @@ the *Cancel* row, leaving a dialog with no way to dismiss it. iOS has no equival
215
222
 
216
223
  **Rule (MR-14): For any Android menu that could exceed three options, use a real bottom sheet dismissible at least three ways (an explicit cancel row, a scrim tap, and the hardware back gesture) instead of `Alert.alert`. Enforcement: explicitly advisory prose (ARC-023) โ€” spell-eas-store-deploy's Known Pitfalls list cites this rule for awareness, but the fix is a UI code change with no console or platform state for the deploy workflow to check before proceeding; catching it depends on code review, not this spell.**
217
224
 
225
+ ---
226
+
227
+ ## Release gates
228
+
229
+ ### MR-15: Every store version still in use needs an update path
230
+
231
+ A store release does not retire the binaries before it. Phones keep running an older store version for
232
+ weeks or months (automatic updates are off, the device is short of storage, the user ignores the store),
233
+ and nothing in either store moves them. An over-the-air update system delivers a new JavaScript bundle
234
+ only to binaries whose native compatibility version matches the one the update was published for
235
+ (expo-updates calls it the runtime version), and a project that derives that version from the app
236
+ version gives every store version its own. So once a new store version ships, updates published from
237
+ current code reach only the new binary. Every older binary receives nothing new, and nothing reports it:
238
+ not the device, not the store console, not the publish step. A fix shipped as an update never reaches
239
+ those phones, and a breaking server change can leave them failing with no way forward short of the store.
240
+ An inventory taken from the published updates misses exactly the versions at risk: one that never
241
+ received an update leaves no trace there. And the version a release replaces still receives updates
242
+ until the moment the release ships.
243
+
244
+ **Rule (MR-15): Before each store release, list every binary version still in use, from the store releases, not from the updates published to them, and confirm each has an update path, judging each as it will stand once the release ships. An update path is either updates still published for the version's native compatibility version or an in-app prompt that sends it to the store; a version with phones on it and neither is stranded. Before the release ships, give each stranded version one (a backported update, which can carry the prompt) or record the operator's decision to leave it. Enforcement: structured spell gate (ARC-023) โ€” spell-eas-store-deploy's runtime audit is the first step of every repeat deployment, and the release report names each stranded runtime with the route chosen for it; no executable check reads the audit's result.**
245
+
246
+ ### MR-16: Pin the Xcode wherever it can change between builds
247
+
248
+ Xcode stamps its toolchain into every iOS binary it builds (`DTXcode` and `DTSDKName` in the app's
249
+ `Info.plist`), and the toolchain is part of what ships: a binary built with an Xcode the project's
250
+ dependencies were not built for can crash at launch while the same JavaScript runs fine as an
251
+ over-the-air update on a binary built earlier. A build service that selects a versioned image for each
252
+ build pins the toolchain with it. A self-hosted build Mac pins nothing unless told to, and neither does a
253
+ hosted CI runner whose default Xcode moves with its image: when the Xcode changes, the next build of an
254
+ unchanged commit uses a different toolchain and no step fails. The crash then looks like a code defect,
255
+ and reading the code first is the slow way to learn that it is not one.
256
+
257
+ **Rule (MR-16): On any build machine whose Xcode can change between builds (a self-hosted Mac, or a hosted CI runner whose default Xcode moves with its image), pin the Xcode per build (installed side by side and selected with `DEVELOPER_DIR`), fail the build when the pinned toolchain is missing or another version is active, and move the pin only on purpose, never through an automatic update. When a store build crashes at launch but the same code runs elsewhere, compare `DTXcode` and `DTSDKName` of the working and the crashing binary before reading any code. Enforcement: structured spell gate (ARC-023) โ€” spell-eas-store-deploy's toolchain step has the build job carry a guard step that fails the run on a missing or mismatched Xcode, and reads that guard's result in the build's own run before a store build continues; the guard itself is an executable check in the consuming project's own pipeline, and no Arcane tooling verifies that it exists.**
258
+
218
259
  ## Related
219
260
 
220
261
  - [[.arcane/governance/external-verification-standards|External Verification Standards]] โ€” `EV-01`
@@ -14,10 +14,15 @@ agent: agent
14
14
  - Roughly 80% of the material (credentials, `eas.json` profile shape, build-number handling, OTA rules)
15
15
  is shared between the two stores; only the store-console work genuinely diverges. Covered once in a
16
16
  shared preamble, then two store-specific sections.
17
- - **Deliberately scoped to EAS Build + EAS Submit** โ€” no local Xcode, Xcode Cloud, or Fastlane coverage.
18
- That scope is what keeps every step concrete and copy-pasteable.
19
- - Every lesson here was hit for real across two dogfooding runs (a first-ever App Store submission, then
20
- a from-zero Google Play listing to a live production release) โ€” none is general knowledge restated.
17
+ - **Deliberately scoped to EAS Build, EAS Submit and EAS Update** โ€” no local Xcode, Xcode Cloud, or Fastlane coverage.
18
+ That scope is what keeps every step concrete and copy-pasteable. The only local toolchain it touches is
19
+ a build machine running `eas build --local`, and only to pin it (EAS Update lifecycle, part 3).
20
+ - An **EAS Update lifecycle** section covers what a store release leaves behind and what surrounds it: the
21
+ runtimes older store versions keep running, an in-app store update prompt, toolchain pinning for
22
+ machines running `eas build --local`, and when to announce a version as the latest.
23
+ - Every lesson here was hit for real, across two dogfooding runs (a first-ever App Store submission, then
24
+ a from-zero Google Play listing to a live production release) and a later consumer store release (the
25
+ EAS Update lifecycle) โ€” none is general knowledge restated.
21
26
  - Durable, store-specific platform facts are documented once in
22
27
  [`mobile-release-standards.md`](../../.arcane/governance/mobile-release-standards.md) and cited here
23
28
  by `MR-nn` ID; verification discipline is documented once in
@@ -42,6 +47,7 @@ account, not a local mistake.
42
47
  | `{EAS_PROJECT_SLUG}` | The EAS project ID/slug. | Resolve from `app.json`'s `expo.extra.eas.projectId` or `eas init` output; if unset, ask. |
43
48
  | `{ANDROID_PACKAGE}` | The Android application ID / package name. | Resolve from `app.json`'s `expo.android.package`; if unset, ask. |
44
49
  | `{ORG_NAME}` | The Expo/EAS organization name, if the project belongs to one. | Resolve from `eas.json` or the EAS dashboard; if not applicable, omit. |
50
+ | `{XCODE_VERSION}` | The Xcode version a build machine running `eas build --local` is pinned to, as `xcodebuild -version` prints it (for example `26.2`). | Use the Xcode of the EAS build image the build profile selects (`build.<profile>.ios.image`, or the default image for the project's SDK): read `DTXcode` from an IPA EAS built with that profile (`2620` is Xcode 26.2), never one older than App Store Connect accepts for uploads; if no EAS-built IPA exists yet, ask. |
45
51
 
46
52
  ## Context files
47
53
 
@@ -111,8 +117,9 @@ This section is common setup both stores' deployments depend on. Do it once.
111
117
  pipeline, run the update job against that commit (for example with a `--commit-id <sha>` argument,
112
118
  where the pipeline takes one); from the CLI, run `eas update` from a checkout of that commit. Read
113
119
  `eas update`'s `Commit`, `Runtime version` and `Platform` lines, then prove the alignment by reading
114
- the published update back (`eas update:view <group-id>`), never from pipeline status or the publish
115
- step's own output alone (`EV-01`, `EV-02`).
120
+ the published update back (`eas update:view <group-id> --json`, whose `gitCommitHash` and
121
+ `runtimeVersion` name that commit and runtime; the human-readable view prints no commit), never from
122
+ pipeline status or the publish step's own output alone (`EV-01`, `EV-02`).
116
123
  - **๐Ÿ”ด Bundler transform caches do not key on `EXPO_PUBLIC_*` env vars.** On a persistent (self-hosted)
117
124
  build runner, the cache survives between runs, so an update published to one channel can ship
118
125
  another channel's inlined config โ€” for example, a production build silently pointing at a development
@@ -144,6 +151,177 @@ expired App Store Connect session shows as a redirect carrying `authResult=FAILE
144
151
 
145
152
  ---
146
153
 
154
+ ## EAS Update lifecycle (both stores)
155
+
156
+ A store release changes which phones an update can reach. This section covers what each release leaves
157
+ behind and what surrounds it, in four parts: the runtimes older store versions keep running (part 1, a
158
+ step of every store release), an in-app prompt that moves phones to the store (part 2, built into the app
159
+ once), the toolchain of a machine running `eas build --local` (part 3, set up once per machine and
160
+ checked before every store build), and when to announce a version as the latest (part 4, after the store
161
+ offers it).
162
+
163
+ ### 1. OTA lifecycle and stranded runtimes
164
+
165
+ - **Audit the live runtimes before every store build (`MR-15`).** With `runtimeVersion.policy:
166
+ appVersion`, every store version is its own runtime, and an update reaches only phones whose binary has
167
+ the same runtime version: a phone that never installs the next store version receives nothing new,
168
+ forever, with no error anywhere. List the store versions from each store console's release history (or
169
+ the release tags, where every store release is tagged), then read, for each, its runtime and commit, how
170
+ many phones it still has, and the last update it receives:
171
+
172
+ ```bash
173
+ eas channel:view <channel>
174
+ # the EAS branch the store channel points to
175
+ eas build:list --platform <ios|android> --app-version <version> --distribution store --status finished --json
176
+ # one store version's builds, with runtime.version and gitCommitHash
177
+ eas channel:insights --channel <channel> --runtime-version <runtime> --days 30
178
+ # the phones on one runtime, running its embedded bundle or an update (eas-cli 18.8.0 or later)
179
+ eas update:list --branch <eas-branch> --runtime-version <runtime> --limit 1 --json
180
+ # the latest update group that runtime receives, if any
181
+ ```
182
+
183
+ A runtime that never received an update has no update group, `eas update:list` shows only the most
184
+ recent groups (25 unless `--limit` says otherwise), and `eas build:list` returns at most 50 builds per
185
+ page, newest first, with TestFlight and other test builds among them, so neither the update list nor a
186
+ page of the newest builds can be the inventory. A binary built with `eas build --local` has no EAS build
187
+ record: its commit comes from the pipeline run or tag that produced it. Judge each runtime as it will
188
+ stand once this release ships: the version it replaces still receives updates today and stops receiving
189
+ updates the moment the release is out. For each runtime that will still have phones and no update path,
190
+ choose its route before the release ships: the backport recipe below, the store update prompt (part 2,
191
+ backported the same way), or the operator's decision to leave it stranded, written into the release
192
+ report.
193
+ - **Never publish to an old runtime from a checkout whose native dependencies differ from that binary.**
194
+ This is the OTA/native alignment rule above (Cross-platform lessons), applied to a runtime that is no
195
+ longer current. When it is broken, the publish succeeds and the phones on that runtime download an
196
+ update that calls native code their binary does not have. expo-updates rolls an update back by itself
197
+ only when it crashes before its content first appears, and then does not launch it again on that
198
+ device; a crash after that point, such as a screen calling the missing module, comes back every time
199
+ that screen is reached, until a corrected update is published to that runtime.
200
+ - **When a runtime is already crashing on a bad update, stop the damage first**, with the operator's
201
+ go-ahead, since each command publishes to phones in production: republish that runtime's last good
202
+ update group (`eas update:republish --group <last-good-group-id>`), or send the runtime back to the
203
+ bundle embedded in its binary
204
+ (`eas update:roll-back-to-embedded --channel <channel> --runtime-version <runtime>`). Then backport the
205
+ fix.
206
+ - **The backport recipe for a stranded runtime:**
207
+ 1. Find the commit that runtime actually runs. The audit's `eas update:list` line names its latest update
208
+ group, and `eas update:view <group-id> --json` gives that group's `gitCommitHash` (the human-readable
209
+ view prints no commit). A runtime that never received an update runs the bundle embedded in its store
210
+ build: take the `gitCommitHash` of that build's `eas build:list` entry, or, for a binary built with
211
+ `eas build --local`, the commit of the pipeline run or release tag that produced it.
212
+ 2. Create a git branch from that commit and add only the files the fix needs, using only primitives that
213
+ binary has: no new native module, and no API from a newer SDK or a newer native dependency.
214
+ 3. Build the workspace libraries first (in a monorepo, the packages the app imports), then typecheck
215
+ against that tree, not the trunk's.
216
+ 4. With the operator's go-ahead (this publishes to phones in production), commit, then publish from that
217
+ git branch to the store channel:
218
+ `eas update --channel <channel> --environment <environment> --message "<what it fixes>"`, with the
219
+ store build's environment (usually `production`): the flag loads that environment's server-side
220
+ variables, which the publish otherwise leaves out, and clears the bundler cache. Never use `--auto`,
221
+ which names the EAS branch after the git branch, a branch no channel points to. The `Commit` line
222
+ `eas update` prints must not end in `*`, which marks an uncommitted tree.
223
+ 5. Prove it by reading the update back: `eas update:view <new-group-id> --json` must show the stranded
224
+ runtime's `runtimeVersion`, the git branch's `gitCommitHash`, and the `branch` that
225
+ `eas channel:view <channel>` names. The pipeline's "succeeded" is not evidence (`EV-02`).
226
+ 6. Mark the git branch publish-only (its name and its first commit say so) and never merge it: it exists
227
+ to publish from, and a merge would bring that old tree back into the trunk.
228
+
229
+ ### 2. A store update prompt
230
+
231
+ Built into the app and its API once; it moves phones off old store versions instead of waiting for them.
232
+
233
+ - **The server holds the versions:** a "latest version" and a "minimum version" per platform, editable by
234
+ an operator (an admin screen), empty by default. The client sends its platform and version, and the
235
+ server answers `ok`, `update` (an invitation with "not now", snoozed for a few days) or `required` (a
236
+ screen with only an update button, which cannot be closed).
237
+ - **Clients identify themselves on every request.** From the release that introduces it, the client sends
238
+ its platform and version as request headers, and the API answers `426 Upgrade Required` to anything
239
+ below the minimum version, except the version-check endpoint itself. Older clients that send no headers
240
+ pass. This is the emergency brake for a breaking API change, and it answers "which app versions still
241
+ call this route?" before any `/v2` is needed.
242
+ - **Mount the prompt outside the app's error boundary.** A 426 can make a screen throw, and an error
243
+ boundary whose fallback replaces the whole tree replaces the update screen with it.
244
+ - **Word the mandatory screen as something new, not something broken:** "there is a new version", not
245
+ "this version no longer works".
246
+ - **Backport the prompt to every stranded runtime** (part 1): for those phones it is the only way to the
247
+ store.
248
+
249
+ ### 3. Toolchain pinning for self-hosted build agents
250
+
251
+ Applies when `eas build --local` runs on a machine whose Xcode can change between builds: a Mac you
252
+ manage, or a hosted CI runner whose default Xcode moves with its image. EAS's own build servers run the
253
+ image the build profile selects.
254
+
255
+ - **Pin the Xcode per build (`MR-16`).** Install the pinned version beside any newer one as
256
+ `/Applications/Xcode-{XCODE_VERSION}.app` (on a hosted runner, point `DEVELOPER_DIR` at that version
257
+ among the image's preinstalled Xcodes instead), set `DEVELOPER_DIR` to its `Contents/Developer` for the
258
+ whole build job so that every step uses it, the build included, and add a bash guard step that fails
259
+ the run when the path is missing or `xcodebuild -version` reports another version:
260
+
261
+ ```bash
262
+ set -euo pipefail
263
+ expected="Xcode {XCODE_VERSION}"
264
+ if [ ! -d "${DEVELOPER_DIR:?DEVELOPER_DIR is not set}" ]; then
265
+ echo "Pinned Xcode is not installed: $DEVELOPER_DIR" >&2
266
+ exit 1
267
+ fi
268
+ version_output="$(xcodebuild -version)" # captured whole, never piped into head
269
+ read -r first_line <<< "$version_output"
270
+ if [ "$first_line" != "$expected" ]; then
271
+ echo "Expected $expected, xcodebuild reports: $first_line" >&2
272
+ exit 1
273
+ fi
274
+ ```
275
+
276
+ Avoid `xcodebuild -version | head -1` under `pipefail`: `head` can exit before `xcodebuild` has finished
277
+ writing, the SIGPIPE that ends `xcodebuild` fails the pipeline, and the step dies (exit 141).
278
+ - **Before a store build on such a machine, read the guard's result in that build's own run.** Confirm that
279
+ the job ran the guard step and that it passed; stop the release if the step is missing or failed.
280
+ - **Move the pin only with the EAS build image the build profile selects** (`build.<profile>.ios.image`,
281
+ or the default image for the project's SDK). An IPA that EAS built with that profile records the Xcode
282
+ it used as `DTXcode` in its `Info.plist` (`2620` is Xcode 26.2); pin that version, and never one older
283
+ than App Store Connect accepts for uploads.
284
+ - **When a store build crashes at launch but the same JavaScript runs fine via OTA**, compare `DTXcode` and
285
+ `DTSDKName` in the working and the crashing IPA's `Info.plist` (unzip each IPA, then
286
+ `plutil -p Payload/<App>.app/Info.plist` on macOS) before reading any code, and build the same commit in
287
+ the EAS cloud: if that build works, the toolchain is the difference, not the code (`MR-16`).
288
+
289
+ ### 4. When to set "latest"
290
+
291
+ - **Set the server-side "latest version" for a platform only when that store's listing actually offers
292
+ the version.** Set early, it sends users to a store page that still offers the old build.
293
+ - **Raise the "minimum version" only to a version every user can install now:** on the App Store once the
294
+ lookup below reports it, on Google Play once the production rollout reaches 100%. A minimum above that
295
+ leaves users on a screen that cannot be closed, whose only button opens a store page that still offers
296
+ the old build, while the API refuses their every request with 426. A version that raises the iOS
297
+ deployment target or Android's `minSdkVersion` never installs on devices below it, so a minimum at that
298
+ version locks those devices out for good, unless the client also sends its OS version and the server
299
+ exempts them.
300
+ - **App Store:** the public lookup, `https://itunes.apple.com/lookup?bundleId={IOS_BUNDLE_ID}`, returns the
301
+ listed version as `results[0].version`. Show it beside the admin field as a reference, with a warning
302
+ when the typed version is newer. It reads one storefront, the United States unless `&country=<cc>` names
303
+ another (`resultCount: 0` means the app is not listed there), and it answers from a cache (a `max-age`
304
+ of about 2 to 14 hours in checks on 2026-10-22), so a version released in the last several hours can
305
+ still read as the old one: treat the warning as a reason to check the listing by hand.
306
+ - **Google Play publishes no equivalent lookup**, so Play is a manual check of the listing before either
307
+ field is set, and a staged rollout counts only at 100%: until then, some users are still offered the
308
+ previous build.
309
+
310
+ **Observed in one consumer session (reported 2026-10-18), not re-verified since** โ€” re-check before relying
311
+ on any of them: phones on five earlier store versions had received no update for weeks before anyone
312
+ noticed; one runtime crash-looped on an update published from a checkout with a different native SDK, and
313
+ the session described expo-updates rolling back to the previous update on every launch while the latest
314
+ update on the channel kept breaking it (compare the documented behavior in part 1); two production iOS
315
+ binaries crashed at launch after the build Mac's Xcode updated itself, while the same JavaScript ran fine
316
+ via OTA; and a 426 made the app's feed crash, so the error boundary's fallback replaced the update screen
317
+ until the prompt was mounted above the boundary.
318
+
319
+ Enforcement: parts 1 and 3 are structured spell gates through `MR-15` and `MR-16` (ARC-023); parts 2 and
320
+ 4 are explicitly advisory prose (ARC-023) โ€” the prompt and the server-side fields are the app's own code,
321
+ which no step of this spell can read.
322
+
323
+ ---
324
+
147
325
  ## Apple App Store
148
326
 
149
327
  ### Initial one-time setup
@@ -162,6 +340,8 @@ expired App Store Connect session shows as a redirect carrying `authResult=FAILE
162
340
 
163
341
  ### Repeat deployments
164
342
 
343
+ Start with the runtime audit (EAS Update lifecycle, part 1; `MR-15`), then build and submit:
344
+
165
345
  ```bash
166
346
  eas build --platform ios --profile production
167
347
  eas submit --platform ios --profile production --latest
@@ -217,6 +397,8 @@ each require one independently.
217
397
 
218
398
  ### Repeat deployments
219
399
 
400
+ Start with the runtime audit (EAS Update lifecycle, part 1; `MR-15`), then build and submit:
401
+
220
402
  ```bash
221
403
  eas build --platform android --profile production
222
404
  eas submit --platform android --profile production --latest
@@ -269,7 +451,8 @@ console error stating otherwise (`EV-06`).
269
451
 
270
452
  ### Reporting the release
271
453
 
272
- End every release run with a short report. The store-console steps the agent cannot reach โ€” sending
454
+ End every release run with a short report that includes the runtime audit's result (each runtime still on
455
+ phones and the route chosen for it). The store-console steps the agent cannot reach โ€” sending
273
456
  staged Google Play changes for review, answering an App Store review question, confirming a manual
274
457
  release โ€” are required operator actions:
275
458
 
@@ -11,6 +11,9 @@ agent: agent
11
11
  - This prompt runs tests, validates coverage against thresholds, and generates evidence artifacts.
12
12
  - Used after `spell-implement` to verify quality before `spell-review`.
13
13
  - Enforces the testing standards from ADR-050 (80% line / 95% critical path coverage).
14
+ - For anything a browser renders, step 4a adds a check in a real browser (captures at three widths, in both
15
+ color schemes and with reduced motion, plus a console read), because a passing suite cannot see a page
16
+ that renders blank.
14
17
  - See [[.arcane/governance/testing-standards|Testing Standards]] for framework details.
15
18
 
16
19
  ---
@@ -48,6 +51,49 @@ Workflow:
48
51
  - Map each acceptance criterion from the story to a specific test.
49
52
  - Flag any acceptance criteria without corresponding test coverage.
50
53
 
54
+ 4a. **Visual verification (HTML and UI deliverables)** โ€” run this step when the scope changes anything a
55
+ browser renders: a page, a template, a stylesheet, a UI component. A passing suite is not evidence that
56
+ a page renders; a page cut short arrives blank, and only a browser shows it. For each changed page or
57
+ screen:
58
+ - **Serve it over local HTTP, never `file://`,** with the headers production sends: the project's own
59
+ dev or preview server, or a static server configured with the same `Content-Type` (adding no charset
60
+ production does not send) and the same security policy. Check that the page declares its character
61
+ set (`<meta charset="utf-8">` within its first 1024 bytes): a page that does not can read correctly
62
+ from disk and still garble when served without a charset, and a content security policy sent as a
63
+ response header never applies to a `file://` load.
64
+ - **Capture it headless at 1440, 390 and 320 px wide, in dark and in light, each with motion allowed and
65
+ with reduced motion:** twelve full-page captures per page. Before each capture, reduced motion
66
+ included, wait until `document.getAnimations()` reports none whose `playState` is `"running"`, with a
67
+ time limit; a page that still moves content under reduced motion (a slide, a zoom, parallax) is a
68
+ finding of its own, while a fade or a progress indicator may stay. For a page that
69
+ animates without end, capture with `page.screenshot({ animations: "disabled" })` instead; the evidence
70
+ says which method was used. Any headless browser that can set the viewport and emulate both media
71
+ features will do; in Playwright that is `page.setViewportSize`,
72
+ `page.emulateMedia({ colorScheme, reducedMotion })` and `page.screenshot({ fullPage: true })`.
73
+ - **Read the console for every capture.** Script errors, uncaught exceptions and content-security-policy
74
+ violations each count, and the bar is zero. In Playwright, listen to `page.on("console")` and
75
+ `page.on("pageerror")` before the page loads, so nothing logged during the load is missed.
76
+ - **Compare before and after.** Capture the same matrix from the base branch, or from the last release,
77
+ in the same session, on the same machine and browser, with time and data held still (fixture data, a
78
+ fixed clock such as Playwright's `page.clock`), and compare each capture with its counterpart. An
79
+ unchanged page then captures identically, so a difference the change did not intend is a finding,
80
+ like a failing test. A page new in this change has no before set: review its after set by eye.
81
+ - **Keep the captures in the design record:** the feature's folder, `features/<feature>/visual/YYYY-MM-DD/`
82
+ (outside a feature, `assets/screenshots/YYYY-MM-DD/`), named `<page>-<width>-<scheme>-<motion>.png` and
83
+ linked from the story's `testEvidence`. Keep each after capture that differs from its before capture,
84
+ that before capture, and one reference capture per page; record the rest by hash, noting any
85
+ dimension the page does not respond to (dark and light on a page with one color scheme). Never keep a
86
+ capture that shows a secret, a token or personal data (universal agent rules, Screenshot Curation):
87
+ capture with fixture data, or mask the region with `page.screenshot({ mask: [locator] })`. In a
88
+ repository whose `.arcane.json` sets `content_sensitivity: "sensitive"`, keep no captures: record each
89
+ capture's verdict and a sanitized summary with no recoverable detail (universal agent rules, Sensitive
90
+ Repositories).
91
+ - **No headless browser available:** say so and record the visual check as `NOT RUN`, never `PASS`; the
92
+ story stays open until someone runs it.
93
+ - Enforcement: structured spell gate (ARC-023) โ€” this spell's Rules keep a story with a rendered
94
+ deliverable from passing until this step's result is recorded; no executable check reads the
95
+ captures.
96
+
51
97
  5. **Generate test evidence**:
52
98
  ```markdown
53
99
  ## Test Evidence โ€” [Story ID or Scope]
@@ -67,6 +113,13 @@ Workflow:
67
113
  | ----------- | ----------- | ----------------- |
68
114
  | [criterion] | [test name] | PASS/FAIL/MISSING |
69
115
 
116
+ ### Visual Verification (step 4a, when the scope renders in a browser)
117
+ - Served from: [local URL, with production's headers; never file://]
118
+ - Captures: [12 per page: 1440/390/320 px ร— dark/light ร— motion/reduced, and the wait method]; kept in [design-record path]: [differing pairs, one reference per page]; the rest by hash
119
+ - Console: [0 errors, or each error and policy violation]
120
+ - Before/after: [no unintended difference, or each one]
121
+ - Status: PASS / FAIL / NOT RUN โ€” [reason]
122
+
70
123
  ### Uncovered Areas
71
124
  - [file:lines with no coverage]
72
125
 
@@ -77,12 +130,14 @@ Workflow:
77
130
  6. **Update stories.json** โ€” if testing a specific story, update the `testEvidence` field.
78
131
 
79
132
  7. **Report** โ€” present the test evidence and recommend next steps:
80
- - All pass + coverage met โ†’ recommend `spell-review`.
133
+ - All pass + coverage met (and step 4a passed, where it applies) โ†’ recommend `spell-review`.
81
134
  - Failures found โ†’ recommend fixing via `spell-implement`.
82
135
  - Coverage gap โ†’ recommend adding tests before review.
83
136
 
84
137
  Rules:
85
138
  - Never mark a story as passing if tests fail or coverage is below the threshold.
139
+ - Never mark a story whose scope a browser renders as passing until step 4a's result is recorded (its
140
+ captures, or in a sensitive repository their sanitized summary); a `NOT RUN` visual check keeps it open.
86
141
  - Include actual test output, not just summaries.
87
142
  - If no test framework is set up, flag this as a blocker and recommend setup.
88
143
  - Critical path code (auth, payments, data mutations) must meet the 95% threshold.