arcane-cli 1.10.2 → 1.11.1

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
@@ -221,7 +221,7 @@ spell add agent-policies # or install any component à la carte
221
221
  spell add spells-build compliance-standards # or several, in order
222
222
  ```
223
223
 
224
- `spell add` takes one or more names. It checks each component's files before writing any of them: if one already exists, it prints one line naming the file and `--force`, exits 1, and writes nothing from that component. With several names it stops at the first failure and says which were added and which were not attempted; a name that is already installed is skipped. A component that keeps your own file (`.mcp.json`) leaves an existing one alone and creates a missing one. `--dry-run` lists each file as identical, differing or missing, or as one that would be kept or refused, and writes nothing.
224
+ `spell add` takes one or more names. It checks each component's files before writing any of them: if one already exists, it prints one line naming the file and `--force`, exits 1, and writes nothing from that component. With several names it stops at the first failure and says which were added and which were not attempted; a name that is already installed is skipped. A component that keeps your own file (`.gitattributes`, for example) leaves an existing one alone and creates a missing one. `--dry-run` lists each file as identical, differing or missing, or as one that would be kept or refused, and writes nothing.
225
225
 
226
226
  ### What `spell update` installs
227
227
 
@@ -154,7 +154,7 @@ GitHub Issues has no configurable work-item-type hierarchy the way Azure DevOps
154
154
  - **Creation:** `gh issue create --title "{title}" --body "{body}" [--label <name>]` — prints the issue URL on success; the trailing path segment is the issue number.
155
155
  - **Fetching:** `gh issue view {id} --json title,body,labels`.
156
156
  - **Closing:** `gh issue close {id} --reason completed [--comment "{note}"]` (`--reason` also accepts `not planned` or `duplicate`).
157
- - **Commit/PR linkage:** `Fixes #{id}` / `Closes #{id}` in a commit message or PR body auto-closes the issue on merge (GitHub-native behavior, no extra command needed) — prefer this over a manual `gh issue close` call when the fix lands via a PR.
157
+ - **Commit/PR linkage:** `Fixes #{id}` / `Closes #{id}` in a commit message or PR body auto-closes the issue on merge (GitHub-native behavior, no extra command needed) — prefer this over a manual `gh issue close` call when the fix lands via a PR. Write one keyword per issue (git-conventions.md, Optional Footer), and confirm each issue's state after the merge (`spell-create-pull-request` Step 6).
158
158
 
159
159
  ### External Provider TODOs
160
160
 
@@ -126,7 +126,7 @@ Changes:
126
126
  Use for:
127
127
 
128
128
  - **Breaking changes:** `BREAKING CHANGE: removed support for plaintext token auth`
129
- - **Issue/ticket references:** `Closes #535` or `Fixes #127`
129
+ - **Issue/ticket references:** `Closes #535` or `Fixes #127` — one keyword per issue, each on its own footer line (`Closes #535`, then `Closes #536`), never `Closes #535 and #536`, because GitHub closes only the issue directly after a keyword
130
130
 
131
131
  Most commits in this repo won't need footers.
132
132
 
@@ -399,6 +399,35 @@ refusing to delete a worktree-attached one, are the enforcement ARC-028 R3/R7 re
399
399
 
400
400
  This applies to all actors — humans, interactive tools (Copilot, Claude), and autonomous agents. The `spell-commit-work` and `spell-close-session` prompts enforce this check.
401
401
 
402
+ **Links inside the worktree — check before `git worktree remove --force`.** A worktree can hold a
403
+ directory link into another tree, most often a `node_modules` linked to the primary checkout's copy to
404
+ skip a reinstall. Before removing a worktree with `--force`, list its links and remove each link (not
405
+ its target) first:
406
+
407
+ ```bash
408
+ # POSIX: find does not follow links, so this lists them without walking into their targets
409
+ find <path> -type l
410
+ rm "<link>" # removes the link only
411
+ ```
412
+
413
+ ```powershell
414
+ # Windows: a recursive listing that never descends into a junction or symlink
415
+ # (Windows PowerShell 5.1's Get-ChildItem -Recurse can follow them into the real tree)
416
+ function Find-Links($p) { Get-ChildItem -Force -LiteralPath $p | ForEach-Object {
417
+ if ($_.Attributes -band [IO.FileAttributes]::ReparsePoint) { $_.FullName }
418
+ elseif ($_.PSIsContainer) { Find-Links $_.FullName } } }
419
+ Find-Links "<path>"
420
+ cmd /c rmdir "<link>" # a directory junction or symlink only; a file symlink: cmd /c del "<link>"
421
+ ```
422
+
423
+ What the evidence covers: on Windows, `--force` was observed once (a consumer session, 2026-10-01) to
424
+ follow a `node_modules` junction and empty the real directory, 194 entries to 0; that has not been
425
+ re-tested, and the PowerShell commands above have not been run. On Linux with git 2.43.0, a symlink to a
426
+ 194-entry directory was tested on 2026-10-04 and removed as a link, with its target intact. Unlink first
427
+ on every platform anyway: the check is one command, and the failure deletes real files.
428
+ **Enforcement: explicitly advisory prose (ARC-023) — `spell-close-session` step 10 points here; no check
429
+ lists or unlinks links before a worktree removal, and `spell-commit-work` does not remove worktrees.**
430
+
402
431
  ### Content-Verified Branch Deletion (TODO.md merged-branch-cleanup finding)
403
432
 
404
433
  Ancestry (`git branch --merged`, plain `git branch -d`) is not a reliable "is this landed?" test on this repo. Two of this repo's own sanctioned states defeat it identically: **Rebase-and-fast-forward** (ARC-009 §7) and any **pre-ARC-009 squash merge** both rewrite commit SHAs, so a fully-landed branch stays permanently invisible to `--merged` and refused by `-d` — not a rare edge case; every branch merged the sanctioned way hits this. **Enforcement: structured spell gate (ARC-023) — `spell-commit-work` Step 10 and `spell-close-session`'s branch-deletion step both require running this content-verification procedure (steps 1-6 below) before a branch deletion; `spell-close-session` runs it as an idempotent sweep every session, and `spell-open-session`'s stale-branch check applies the same procedure to its read-only candidate list.** Verify by content instead, for any local branch except `<trunk>` and any branch currently attached to another worktree (`git worktree list`):
@@ -486,8 +515,14 @@ reported in output so a human sees it happened. The known-issues rows below (`MC
486
515
  `MCP create_pull_request`) are concrete instances of this general rule, not special cases of their own.
487
516
  **Consumer hardening:** set a per-server `"timeout"` (milliseconds) in `.mcp.json` so a hang aborts on a
488
517
  predictable schedule instead of running to the client's own default idle limit (as long as 30 minutes).
518
+ In Claude Code the value goes inside each server's entry under `mcpServers` — `"timeout": 30000` is 30
519
+ seconds — and must be 1000 or more; a smaller value is ignored (Claude Code MCP docs, checked 2026-10-02;
520
+ other clients have their own keys, so check theirs before copying this one). `spell doctor` warns on an
521
+ entry without one and never edits the file: `.mcp.json` is yours. Arcane ships no example `.mcp.json` —
522
+ the `mcp-config-template` scaffold was retired on 2026-10-02 because a root `.mcp.json` is live
523
+ configuration that clients execute, not documentation, and its placeholder server failed on every session start.
489
524
  Origin: a real ops session lost roughly an hour to two consecutive 30-minute MCP hangs before the
490
- documented `az` fallback was used (2026-08-14). **Enforcement: explicitly advisory prose (ARC-023) — this fail-fast behavior and the `.mcp.json` timeout hardening both depend on the acting agent actually following them; no check in this repo verifies an agent stopped retrying a downed MCP server or that a given `.mcp.json` entry sets a timeout.**
525
+ documented `az` fallback was used (2026-08-14). **Enforcement: explicitly advisory prose (ARC-023) for the fail-fast behavior — no check in this repo verifies an agent stopped retrying a downed MCP server. The timeout hardening has an advisory check: `spell doctor`'s "MCP config (per-server timeout, I12)" row warns on a `.mcp.json` entry without a `"timeout"` (exit code unchanged) and edits nothing.**
491
526
 
492
527
  **Known issues:**
493
528
 
@@ -70,6 +70,12 @@ Workflow:
70
70
  - Identify the exact line(s) where the bug manifests.
71
71
  - Determine *why* the bug exists (data mismatch, missing case, race condition, etc.).
72
72
  - Document the root cause concisely.
73
+ - **"It worked before" and the code is unchanged: diff the environment before the code.** When the report is a regression and the code the symptom points at has not changed since the last build that worked, the runtime around it moved. Before patching that code:
74
+ 1. Compare the dependency set and any platform or SDK version of the last good build against the current one, for example `git show <last-good-commit>:package.json` against the current file, or the two lockfiles.
75
+ 2. If a dependency in that diff plausibly owns the behavior, read its actual implementation (for a native module, the native source under `node_modules`, not its README), and check its issue tracker and release notes for the versions in the diff.
76
+ 3. Only then design the fix. Prefer the upstream fix plus a stopgap over a workaround in the calling code.
77
+
78
+ A run of fixes to the calling code that each address a real but narrower cause, none of which resolves the report, is the signal that this check was skipped.
73
79
 
74
80
  4. **Implement the fix** — write the minimal code change:
75
81
  - Fix only the bug — no refactoring, no feature additions.
@@ -57,7 +57,7 @@ Workflow:
57
57
  - `### Lessons Learned` — one heading per lesson with enough context to prevent the mistake again. Use narrative, not bullet fragments.
58
58
  - `### Open Items Carried Forward` — items that remain undone from this session, including every `pending`/`dispatched`/`failed`/`unverifiable` item from step 1b.
59
59
 
60
- 2b. **Lesson routing: send each lesson where it can be acted on.** A lesson left only in the journal never reaches the people who could fix its cause. Skip this step when step 2 wrote no `### Lessons Learned`, or when the invocation includes `--no-route`. With `--no-route`, the lessons stay in the journal only; say `Lesson routing skipped (--no-route)` in the closure report.
60
+ 2b. **Lesson routing: send each lesson where it can be acted on.** A lesson left only in the journal never reaches the people who could fix its cause. An assistant's private memory is a convenience for the current assistant only and is never a destination for a lesson. Another assistant, or the same one in another environment, never sees it. Saving a lesson there and reporting it as saved is not routing it. Skip this step when step 2 wrote no `### Lessons Learned`, or when the invocation includes `--no-route`. With `--no-route`, the lessons stay in the journal only; say `Lesson routing skipped (--no-route)` in the closure report.
61
61
  - **Classify each lesson** written in step 2 into exactly one class:
62
62
  - **Framework-shaped:** the fix belongs in Arcane itself, meaning a spell prompt, template, governance doc, playbook, or CLI behavior this repository installed rather than authored. This is `spell-feedback` Step 3's definition. Destination: `FEEDBACK.md`, queued for upstream.
63
63
  - **Product idea:** the lesson suggests something to build in this repository's own product. Destination: `spell-save-idea`.
@@ -118,10 +118,10 @@ In `ai-context/system-prompt-context.md`, replace the existing `## Next Session
118
118
  - **Next concrete action:** The exact first thing to do at next session start. Be specific: file, command, or decision. Never write "continue work." Name where step 4b registered it (e.g. `path ("the item's own quoted title")` — a stable locator naming the actual file and its bold title, never a bare line number that drifts the moment that file is next edited).
119
119
  - **Active files:** Files with uncommitted changes or the focus of the last edit (comma-separated).
120
120
  - **Branch:** Output of `git branch --show-current`.
121
- - **Blockers:** Known unresolved blockers or dependencies. Write "None" if clear.
121
+ - **Blockers:** Known unresolved blockers or dependencies. Write "None" if clear. Write any claim about the local environment (a failing hook, a broken tool, a flaky test) with the date it was observed and a one-line, read-only command that re-verifies it (no install, network fetch, write or secret access): `<claim> — observed YYYY-MM-DD — re-verify: <command>`. An undated environment claim reads as a standing fact, and the next session repeats it after it stops being true.
122
122
  - **Pending Verification:** One line per non-`succeeded` item from step 1b: `<what> — <state> — <verification action>`. States: `dispatched`, `pending`, `failed`, `unverifiable` (never list a `succeeded` item here — it belongs in `Last completed step` / TODO.md / the journal instead, worded per the gating above). The verification action is required for `dispatched`, `pending`, and `unverifiable` — "N/A" is only valid for `failed`, where the state is already resolved and there is nothing left to check. Write "None" only if step 1b found every dispatched item resolved to `succeeded` or `failed` before this handoff was written.
123
123
  - **Needs you:** One line per action only the operator can take that this session leaves open: `<what to do> — <why the agent cannot do it>`. This block is committed before steps 9–10 run, so write it with them in view and name now the actions they will leave open: the pull request step 9c opens needs a human merge, and when `push_policy` (read it now, per step 9c's push-policy check) is `blocked`, or `guarded` in an autonomous run, the push itself waits on the operator. The closure report's `## ⚠ Needs you` block (step 11) lists the same items, plus any that arose after this handoff was committed. Write "None" when nothing needs the operator. Never leave such an action only in `Blockers`, `Notes` or `Carry Forward`; it may be repeated there, but it always appears here.
124
- - **Notes:** Anything time-sensitive, fragile, or contextual that would be lost if not stated explicitly. Never the sole carrier of durable content (session handoff durability) — if it names unfinished work, that work must also be registered per step 4b; name its location here.
124
+ - **Notes:** Anything time-sensitive, fragile, or contextual that would be lost if not stated explicitly. Never the sole carrier of durable content (session handoff durability) — if it names unfinished work, that work must also be registered per step 4b; name its location here. An environment claim here follows the `Blockers` rule: date it and give its re-verify command.
125
125
  ```
126
126
 
127
127
  Rules:
@@ -197,6 +197,7 @@ Rules:
197
197
  - Skip this entire step for local-only and read-only sessions.
198
198
  - Resolve `<remote>` and `<trunk>` from observed Git/provider state: use the usable authenticated remote selected in step 9 and the merged PR's target branch (falling back to that remote's default branch). Never assume `origin` or `main`.
199
199
  - Verify through the detected provider that the PR is merged before changing branches.
200
+ - Then confirm each issue the PR's body closes is closed (`gh issue view <number> --json state` on GitHub). A merge closes only the issue directly after each closing keyword, so list any issue still open under the closure report's `## ⚠ Needs you`, or close it with a comment naming the merged PR.
200
201
  - **Determine the isolation primitive first (ARC-028 R8).** Run both of these and compare:
201
202
  ```bash
202
203
  git rev-parse --path-format=absolute --git-common-dir
@@ -215,7 +216,7 @@ Rules:
215
216
  - First establish whether any working tree currently holds `<trunk>`: run `git worktree list` and look for `[<trunk>]`. That, not the mere fact of being in a worktree, is what determines the two rules below — a **bare** repository with worktrees attached (a common agent-fleet layout: one bare clone, one worktree per agent) usually has no checkout holding `<trunk>` at all, and there `git switch <trunk>` and `git branch -d` both succeed normally. Verified: they do.
216
217
  - **If a working tree holds `<trunk>`, do not run `git switch <trunk>`.** It will fail — Git refuses to check out one branch in two worktrees (`fatal: '<trunk>' is already used by worktree at '<path>'`). Ending on the session branch is correct here, not a lapse; the branch is already merged and the worktree is about to be removed.
217
218
  - **Do not run `git branch -d <branch>` while this worktree is attached to it.** It will fail (`error: cannot delete branch '<branch>' used by worktree at '<path>'`), from inside the worktree and from the primary alike. Both refusals are the guardrail ARC-028 R3/R7 leans on, not an obstacle to work around — never reach for `-D`, `--force`, or manual deletion of `.git/worktrees/<name>` to get past them. (`-D` does not bypass this either; the refusal is about attachment, not about merge state.)
218
- - Report the worktree path and the branch name, and name **where** removal must happen: `git worktree remove <path>` then `git fetch --prune <remote>`, run after the same-vantage-point check from a checkout that is not this one. A worktree cannot safely remove itself. If `git worktree list` shows a working tree holding `<trunk>`, that is the vantage point; if the repository is **bare**, run it from the bare repository itself (`git -C <bare> worktree remove <path>`) — do not report the work as belonging to a "primary checkout" that does not exist.
219
+ - Report the worktree path and the branch name, and name **where** removal must happen: `git worktree remove <path>` then `git fetch --prune <remote>`, run after the same-vantage-point check from a checkout that is not this one. Before any `git worktree remove --force`, list and unlink any junctions or symlinks inside it first (git-conventions.md Post-Merge Cleanup, "Links inside the worktree"). A worktree cannot safely remove itself. If `git worktree list` shows a working tree holding `<trunk>`, that is the vantage point; if the repository is **bare**, run it from the bare repository itself (`git -C <bare> worktree remove <path>`) — do not report the work as belonging to a "primary checkout" that does not exist.
219
220
  - If this worktree was created with `--detach` there is no branch to name; report the path and the detached HEAD sha instead.
220
221
  - Verify the merge landed with `git log --oneline -3 <remote>/<trunk>` (the remote-tracking ref), since local `<trunk>` is not checked out here to fast-forward.
221
222
 
@@ -170,7 +170,14 @@ Workflow:
170
170
  - Enforcement: structured spell gate (ARC-023) — for `guarded`, this check is the only thing that holds a push for confirmation. A `blocked` push is also refused by the pre-push hook and the sentinel push URL that `spell init` or `spell block-push` install, and `spell doctor` reports a `blocked` policy whose controls are missing.
171
171
  <!-- fragment:push-policy-check:end -->
172
172
 
173
- When the check allows it, run `git push origin <branch>`.
173
+ **PR-state check — before a branch push.** Run it after the push-policy check, before the push below and before the step 9b `--force-with-lease` push. It does not cover Step 10's `git push origin --delete <branch>`, which runs because the PR merged. Ask the provider for the branch's pull requests: `gh pr list --head <branch> --state all --json number,state` on GitHub, `az repos pr list --source-branch <branch> --status all` on Azure DevOps, or the provider's MCP equivalent. Judge by the most recent one:
174
+ - **`OPEN`, or no pull request:** proceed.
175
+ - **`MERGED` (`completed` on Azure DevOps):** do not push: stop and name the merged PR. A push there recreates the remote branch the merge deleted and carries already-merged commits with it; put the follow-up work on a fresh branch from `<trunk>` instead. This is stricter than the warning ARC-035 decision 4 gave this repository's own pre-push hook, because a merged branch is never a live target.
176
+ - **`CLOSED` without merging (`abandoned` on Azure DevOps):** name the PR and ask before pushing, since new commits there reach no open review. In an autonomous run, do not push; report it.
177
+ - **The provider cannot be queried:** say so. Push only a branch this session created and opened no pull request for; otherwise ask, or in an autonomous run report the push as pending.
178
+ - Enforcement: structured spell gate (ARC-023) — only this spell runs the check. This repository's own `.husky/pre-push` warns on a closed or merged PR without blocking, and consumer repositories get no such hook.
179
+
180
+ When the checks allow it, run `git push origin <branch>`.
174
181
 
175
182
  a1. **MCP fail-fast / fallback.** If an MCP tool used anywhere in this step (e.g. `create_pull_request`) fails abnormally once — a hang, an idle-timeout abort, a transport error, or an empty response where data is clearly expected — treat that server as down for the rest of this session. Do not retry it blindly; fall back to the raw CLI paths below (`gh pr create` / `az repos pr create`) and report the downgrade. Full rule: `.arcane/governance/git-conventions.md` → Known issues.
176
183
 
@@ -162,6 +162,8 @@ Documentation-only PR. No functional code changed.
162
162
  <Same as above — include only when this PR is one epic of a tracked program.> · category: docs
163
163
  ```
164
164
 
165
+ **Closing issues: one keyword per issue.** GitHub reads a closing keyword only for the issue reference directly after it, so `Closes #A and #B` closes #A and leaves #B open. In either template, write one keyword per issue, each in its own sentence: `Closes #A. Closes #B.` (the same for `Fixes` and `Resolves`), never `Closes #A and #B`. Write `Part of #N` for an issue this PR does not finish.
166
+
165
167
  **About `## For the record`** — include it **only when this PR is one epic of a tracked program**
166
168
  (one with a `docs/plans/<slug>/PLAN.md`); omit the heading entirely otherwise, since an empty
167
169
  section is worse than none. Write the sentence that program's completion report will show for this
@@ -228,6 +230,7 @@ so they need no edit.
228
230
  - If no reviewers were resolved (none passed, or names the provider could not match): `⚠ No reviewers assigned — add them in the PR or re-run with --reviewers`.
229
231
  - If `--draft`: note it was created as a draft.
230
232
  - Delete the body temp file (`<git-dir>/arcane-pr-body.md`) once the PR is confirmed created and verified.
233
+ - **Issues the body closes:** list each one by number. The PR has not merged yet, so add one line to the `Needs you` block: `After merging <PR URL>, confirm #A and #B are closed (gh issue view <number> --json state)`. `spell-close-session` step 10 runs the same check once it confirms the merge. Never assume the merge closed them.
231
234
  - **Branch topology** — per the generated state diagrams convention (rule 8, ARC-036), built only from
232
235
  Step 1's already-gathered branch name and commit list (`git log origin/<target>..HEAD --format="%h %s" --reverse`).
233
236
  Skip entirely (the applicability guard) if that commit list is empty — Step 0.4 already stops before
@@ -99,6 +99,20 @@ This section is common setup both stores' deployments depend on. Do it once.
99
99
  natively; it cannot rely on an OTA update being fetched, because of the next rule.
100
100
  - **OTA updates apply on the next launch, not the current one** — the client downloads in the background
101
101
  and swaps on relaunch. Do not diagnose a "stuck" update until after two full quit-and-reopen cycles.
102
+ - **🔴 An OTA update must match the installed binary's native dependencies.** With
103
+ `runtimeVersion.policy: appVersion`, expo-updates refuses an OTA only when the app version differs. A
104
+ native dependency change merged to trunk before its binary ships is invisible to that check: an OTA
105
+ published from trunk HEAD carries the new module's JavaScript onto binaries still running the old
106
+ native code, and the publish reports success. **Bump the app version in the same PR as any native
107
+ dependency change** (or move to a runtime-version policy that tracks native changes, such as
108
+ `fingerprint`); that is the rule, because users keep running the old store build long after the new
109
+ one ships. Until a bump lands, publish OTAs only from the last commit whose native dependency set
110
+ (`package.json`, the lockfile and the app config's plugins) matches every binary still in use: in a
111
+ pipeline, run the update job against that commit (for example with a `--commit-id <sha>` argument,
112
+ where the pipeline takes one); from the CLI, run `eas update` from a checkout of that commit. Read
113
+ `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`).
102
116
  - **🔴 Bundler transform caches do not key on `EXPO_PUBLIC_*` env vars.** On a persistent (self-hosted)
103
117
  build runner, the cache survives between runs, so an update published to one channel can ship
104
118
  another channel's inlined config — for example, a production build silently pointing at a development
@@ -109,6 +123,25 @@ This section is common setup both stores' deployments depend on. Do it once.
109
123
  - **When a feature is removed or renamed, both stores' screenshots and descriptions are in the blast
110
124
  radius.** Store metadata outlives the code it depicts, and it's the first thing a reviewer opens.
111
125
 
126
+ ### Console automation (both stores)
127
+
128
+ - **Every mutating save is verified by reloading the page and re-reading the saved state** — character
129
+ counters, the attached-build row, draft fields — in Play Console and App Store Connect alike (`EV-01`).
130
+ Both consoles can silently drop synthetic input (seen in Play Console; see its Known pitfalls), so a
131
+ click that "succeeded" proves nothing.
132
+ - **Large uploads are a human step.** Browser-automation upload tools may cap file size below a release
133
+ bundle (one observation below), so plan for it: the operator drags the artifact into the console's upload box, and the agent
134
+ handles the rest of the page (release name, notes, review, submit).
135
+ - **The agent never types credentials.** When a console session expires mid-task, stop and ask the
136
+ operator to sign in again, then re-read the page before continuing.
137
+
138
+ **Observed in one consumer session (2026-09-28), not re-verified since** — re-check against the live
139
+ console before relying on any of them: the session's upload tool capped files at 10 MB against a
140
+ ~94 MB `.aab`; Play's default release name is `<versionCode> (<version>)`; Play release notes go
141
+ inside the listing's default-language tags (for example `<es-419>…</es-419>`); with managed publishing
142
+ off, "Submit for review" is the last human action and Google's approval publishes the release; an
143
+ expired App Store Connect session shows as a redirect carrying `authResult=FAILED`.
144
+
112
145
  ---
113
146
 
114
147
  ## Apple App Store
@@ -33,6 +33,8 @@ From the input and recent context, determine:
33
33
 
34
34
  Do not ask for anything you can infer from the conversation.
35
35
 
36
+ Private assistant memory is never a destination for a lesson (`spell-close-session` step 2b). Anything worth keeping goes into the repository's journal, decision record or runbooks, and, when framework-shaped, upstream through `FEEDBACK.md` (Step 5).
37
+
36
38
  ## Step 2 — Prompt for Structured Feedback
37
39
 
38
40
  Ask all of this at once (not one at a time):
@@ -38,6 +38,7 @@ Check `ai-context/system-prompt-context.md` for a `## Next Session Handoff` sect
38
38
  - If present and **not yet marked consumed** (no `> ✓ Consumed:` line): surface it immediately as `## Picking Up From Last Session` at the very top of your output, before all other sections — include `Last completed step`, `Blockers`, and `Notes` verbatim alongside the rest (session handoff durability: these three are written by close-session but were previously never read back). Use the `Next concrete action` field as the first item in `## Next Session Plan`. Use the `Active task`, `Active files`, and `Branch` fields to pre-populate the State Snapshot.
39
39
  - **The handoff's `Needs you` field comes first.** If it lists anything other than "None", surface every item in the `## ⚠ Needs you` block (see the output structure below) **above** `## Picking Up From Last Session`, the one exception to "before all other sections" in the bullet above. Where you can check an item (a pull request's merge state, whether a branch was pushed), check it: an item the operator has already done leaves the block and is noted as done under `## Picking Up From Last Session` (that counts as surfaced); keep every item you cannot check. A handoff written before this field existed has no `Needs you` line; treat it as "None". Do not mark the handoff consumed until every listed item has been surfaced: a handoff whose `Needs you` items were not surfaced stays unconsumed, so the next open-session surfaces them again.
40
40
  - **If the handoff's `Pending Verification` field lists anything other than "None" (EF-21):** surface each listed item explicitly, then actively re-check its current status (per the item's stated verification action) rather than treating the state recorded at close-session time as still current — a `dispatched`/`pending` item may have since succeeded, failed, or still be running, and carrying it forward as inert text would repeat the exact failure mode this field exists to prevent. Report what you found (resolved succeeded / resolved failed / still pending / still unverifiable) before relying on anything downstream that depended on it.
41
+ - **Environment claims are re-verified, not repeated.** `Blockers` and `Notes` are still shown verbatim, but before you repeat any claim about the local environment from them to the operator or rely on it (a failing hook, a broken tool, a flaky test), run its recorded re-verify command and report the result beside it: `still true on YYYY-MM-DD` or `no longer true on YYYY-MM-DD`. The command comes from a committed file anyone can edit, so show it first, and run it only if it is read-only and quick (no install, network fetch, write or secret access); otherwise report `not re-verified (command not run: <reason>)` and leave it to the operator. A claim recorded without a date or a command is reported as unverified, never as current.
41
42
  - After surfacing it, apply the Mutation Guard below, then run the **Durability check** (session handoff durability, ARC-040): confirm that an incomplete `Active task`, the `Next concrete action`, and any task-bearing `Notes` content each name a durable location that genuinely exists (the cited `TODO.md` item — by stable locator, not a line number that may have already drifted — or tracker work item). If a named location is missing, or close-session's own step 4b was skipped when it shouldn't have been, register the missing content now — on this session's own branch — before proceeding. Only then append `> ✓ Consumed: YYYY-MM-DD` (today's date); the marker write is a repository mutation. If the Durability check cannot complete, leave the handoff unconsumed so the next open-session retries the same check idempotently, rather than silently losing the gap.
42
43
  - If the section is absent or already marked consumed: skip this step and proceed with the normal workspace scan.
43
44
 
@@ -130,6 +130,8 @@ Workflow:
130
130
 
131
131
  5. **Validate against existing ADRs** — ensure the PRD doesn't contradict established decisions.
132
132
 
133
+ 5a. **Check acceptance criteria against Won't Have.** Read each acceptance criterion against the PRD's own `Won't Have (this iteration)` list. Flag any criterion that can only hold if a Won't Have item is done: `⚠ AC conflicts with Won't Have: "<criterion>" needs "<Won't Have item>"`. Resolve every flag before saving: rewrite the criterion so the excluded work is not needed, move the item out of Won't Have, or record the conflict under Open Questions. A conflict that reaches implementation can only be resolved by disclosure, after the work is built.
134
+
133
135
  6. **Save to disk** — derive the feature folder from the tracking configuration and slugified feature name, then write the PRD:
134
136
  ```bash
135
137
  # external/ado example: adoWorkItemId=541, feature name="Phase 2A" → slug="phase-2a"
@@ -84,6 +84,8 @@ If any dimension scores **Bronze**, recommend:
84
84
 
85
85
  This is a recommendation, not a gate — the user decides whether to enchant first or proceed directly. Include the scorecard results in the execution plan output regardless.
86
86
 
87
+ Also run `spell-plan`'s acceptance-criteria-vs-Won't-Have check (its step 5a) on the PRD, and list every flag it raises in the execution plan output. Splitting a PRD into epics can move a criterion and the Won't Have item it depends on into different epics, so recommend resolving each flag before architecture.
88
+
87
89
  ### 2. Scope Assessment
88
90
 
89
91
  Evaluate the PRD against Spell Loop sizing rules:
package/dist/index.js CHANGED
@@ -579,15 +579,10 @@ var COMPONENTS = [
579
579
  skipExisting: true,
580
580
  initOnly: true
581
581
  },
582
- // Optional MCP config scaffold (BC-22/I12). skipExisting: a repo's own
583
- // .mcp.json is user-owned server config Arcane must never overwrite.
584
- // Not initOnly -- useful to retrofit via `spell add` on an existing repo too.
585
- {
586
- name: "mcp-config-template",
587
- description: "Example .mcp.json with a per-server timeout, companion to the MCP fail-fast rule",
588
- files: [".mcp.json"],
589
- skipExisting: true
590
- },
582
+ // NOTE: the `mcp-config-template` component (`.mcp.json`, BC-22/I12) was
583
+ // retired on 2026-10-02 -- see REGISTRY_RETIREMENTS. A root .mcp.json is
584
+ // live MCP config that clients execute, and the `full` profile's "*" put
585
+ // its example server in every full-profile repo (#328).
591
586
  {
592
587
  name: "records-conventions",
593
588
  description: "Supersession, tombstone, and retention conventions for records repositories",
@@ -937,6 +932,13 @@ var REGISTRY_RETIREMENTS = [
937
932
  successors: [],
938
933
  reason: "`.github/agents/*.agent.md` files are rendered per roster by `spell agents init` / `spell agents sync` (ARC-047), which a fixed-path component cannot do"
939
934
  },
935
+ {
936
+ name: "mcp-config-template",
937
+ kind: "component",
938
+ successors: [],
939
+ userOwned: true,
940
+ reason: "retired 2026-10-02 (#328): a root .mcp.json is live MCP config that clients execute, and the `full` profile installed its example server everywhere; set per-server timeouts in your own .mcp.json, which `spell doctor` checks"
941
+ },
940
942
  {
941
943
  name: "spell-prompts",
942
944
  kind: "component",
@@ -4803,7 +4805,8 @@ function formatRegistryChanges(changes) {
4803
4805
  lines.push(" Renamed or retired, still tracked here:");
4804
4806
  for (const entry of changes.retired) {
4805
4807
  const successor = entry.successors.length > 0 ? entry.successors.join(", ") : "no successor";
4806
- lines.push(` ${entry.name} (${entry.kind}) \u2192 ${successor}: ${entry.reason}`);
4808
+ const ownership = entry.userOwned ? " (user-owned: a copy you edited is released from tracking, never deleted)" : "";
4809
+ lines.push(` ${entry.name} (${entry.kind}) \u2192 ${successor}: ${entry.reason}${ownership}`);
4807
4810
  }
4808
4811
  lines.push(
4809
4812
  " Nothing is deleted because of this list: their files go through the orphan handling above, which removes an untouched one only under `spell update --prune`."
@@ -5058,6 +5061,17 @@ async function resolveOrphan(targetDir, file, recordedHash, prune) {
5058
5061
  console.log(` Pruned orphaned file: ${file}`);
5059
5062
  return "pruned";
5060
5063
  }
5064
+ async function isReleasableUserOwnedFile(targetDir, file, recordedHash) {
5065
+ try {
5066
+ validateTargetPath(targetDir, file);
5067
+ } catch {
5068
+ return false;
5069
+ }
5070
+ const filePath = join10(targetDir, file);
5071
+ if (!await fileExists(filePath)) return false;
5072
+ if (recordedHash === void 0) return true;
5073
+ return !await fileMatchesHash(filePath, recordedHash);
5074
+ }
5061
5075
  async function retainReportedOrphan(targetDir, file, recordedHash, into) {
5062
5076
  try {
5063
5077
  validateTargetPath(targetDir, file);
@@ -5332,14 +5346,36 @@ async function runUpdate(options, targetDir, assetsDir, packageVersion) {
5332
5346
  } catch (err) {
5333
5347
  if (err instanceof ComponentNotFoundError) {
5334
5348
  console.log(` ! ${installed.name} not in registry \u2014 skipping.`);
5349
+ const userOwned = REGISTRY_RETIREMENTS.some(
5350
+ (r) => r.kind === "component" && r.name === installed.name && r.userOwned === true
5351
+ );
5352
+ const stillTracked = [];
5335
5353
  for (const file of installed.files) {
5354
+ const recordedHash = installed.fileHashes?.[file];
5355
+ if (userOwned && await isReleasableUserOwnedFile(targetDir, file, recordedHash)) {
5356
+ console.log(
5357
+ ` ${options.dryRun ? "[dry-run] Would release" : "Released"}: ${file} \u2014 yours (edited since Arcane wrote it, or never written by Arcane), no longer tracked. Delete it yourself if you do not want it.`
5358
+ );
5359
+ continue;
5360
+ }
5336
5361
  const status = await resolveOrphan(
5337
5362
  targetDir,
5338
5363
  file,
5339
- installed.fileHashes?.[file],
5364
+ recordedHash,
5340
5365
  Boolean(options.prune) && !options.dryRun
5341
5366
  );
5342
5367
  orphanReport.push({ file, status });
5368
+ if (status === "reported") stillTracked.push(file);
5369
+ }
5370
+ if (userOwned && !options.dryRun) {
5371
+ if (stillTracked.length === 0) continue;
5372
+ const fileHashes2 = {};
5373
+ for (const file of stillTracked) {
5374
+ const hash = installed.fileHashes?.[file];
5375
+ if (hash !== void 0) fileHashes2[file] = hash;
5376
+ }
5377
+ updatedComponents.push({ ...installed, files: stillTracked, fileHashes: fileHashes2 });
5378
+ continue;
5343
5379
  }
5344
5380
  if (options.prune && !options.dryRun && installed.files.every((f) => orphanReport.find((o) => o.file === f)?.status !== "reported")) {
5345
5381
  continue;
@@ -6837,6 +6873,7 @@ async function checkGovernancePlaceholders(targetDir) {
6837
6873
  };
6838
6874
  }
6839
6875
  var UNOWNED_EXAMPLE_SCOPE = "@example/";
6876
+ var RETIRED_SCAFFOLD_PLACEHOLDER = "<your-mcp-server-package>";
6840
6877
  async function checkComponentRequires(targetDir) {
6841
6878
  const name = "Component prerequisites";
6842
6879
  let manifest;
@@ -6927,11 +6964,14 @@ async function checkMcpConfig(targetDir) {
6927
6964
  return { name, passed: true, blocking: false, message: "no MCP servers configured" };
6928
6965
  }
6929
6966
  const exampleHits = [];
6967
+ const placeholderHits = [];
6930
6968
  for (const n of serverNames) {
6931
6969
  const args = servers[n]?.args;
6932
6970
  if (!Array.isArray(args)) continue;
6933
6971
  for (const a of args) {
6934
- if (typeof a === "string" && a.startsWith(UNOWNED_EXAMPLE_SCOPE)) exampleHits.push(`${n} (${a})`);
6972
+ if (typeof a !== "string") continue;
6973
+ if (a.startsWith(UNOWNED_EXAMPLE_SCOPE)) exampleHits.push(`${n} (${a})`);
6974
+ if (a === RETIRED_SCAFFOLD_PLACEHOLDER) placeholderHits.push(n);
6935
6975
  }
6936
6976
  }
6937
6977
  if (exampleHits.length > 0) {
@@ -6942,13 +6982,21 @@ async function checkMcpConfig(targetDir) {
6942
6982
  message: `.mcp.json runs a package under the ${UNOWNED_EXAMPLE_SCOPE} scope nobody here owns: ${exampleHits.join(", ")}. The old \`spell init\` scaffold shipped it as a placeholder, and \`npx -y\` would run whoever publishes it. Replace the entry with your own MCP server package, or remove it; the file is yours, so doctor does not edit it`
6943
6983
  };
6944
6984
  }
6985
+ if (placeholderHits.length > 0) {
6986
+ return {
6987
+ name,
6988
+ passed: false,
6989
+ blocking: false,
6990
+ message: `.mcp.json still holds the placeholder server from the retired mcp-config-template scaffold: ${placeholderHits.join(", ")} (${RETIRED_SCAFFOLD_PLACEHOLDER}). Clients load this file as live MCP config, so the placeholder fails on every session start. Delete the file if you run no MCP servers, or replace the entry with a real server package; the file is yours, so doctor does not edit it. An untouched scaffold is removed by \`spell update --prune\``
6991
+ };
6992
+ }
6945
6993
  const missingTimeout = serverNames.filter((n) => typeof servers[n]?.timeout !== "number");
6946
6994
  if (missingTimeout.length > 0) {
6947
6995
  return {
6948
6996
  name,
6949
6997
  passed: false,
6950
6998
  blocking: false,
6951
- message: `server(s) missing a "timeout": ${missingTimeout.join(", ")} \u2014 a hang runs to the client's own default idle limit (up to 30 min) instead of a predictable one`
6999
+ message: `server(s) missing a "timeout": ${missingTimeout.join(", ")} \u2014 add "timeout": 30000 (milliseconds; Claude Code ignores a value below 1000) to each entry under mcpServers, so a stuck tool call ends after 30 s instead of running to the client's own idle limit (as long as 30 minutes)`
6952
7000
  };
6953
7001
  }
6954
7002
  return {