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
|
[](https://www.npmjs.com/package/arcane-cli)
|
|
15
15
|
[](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** ยท **๐ <!--count:spells-->41<!--/count:spells--> spells** ยท **๐ค <!--count:agents-->12<!--/count:agents--> agents** ยท **โ๏ธ <!--count:governance-->25<!--/count:governance--> governance standards** ยท **๐ markdown-native** ยท **๐
|
|
33
|
+
**๐ opinionated lifecycle** ยท **๐ <!--count:spells-->41<!--/count:spells--> spells** ยท **๐ค <!--count:agents-->12<!--/count:agents--> agents** ยท **โ๏ธ <!--count:governance-->25<!--/count:governance--> governance standards** ยท **๐ markdown-native** ยท **๐ 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
|
-
|
|
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*.**
|
|
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
|
|
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://
|
|
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)
|
|
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
|
|
18
|
-
That scope is what keeps every step concrete and copy-pasteable.
|
|
19
|
-
|
|
20
|
-
|
|
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
|
|
115
|
-
|
|
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
|
|
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.
|