arcane-cli 1.10.1 → 1.11.0

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
 
@@ -486,8 +486,14 @@ reported in output so a human sees it happened. The known-issues rows below (`MC
486
486
  `MCP create_pull_request`) are concrete instances of this general rule, not special cases of their own.
487
487
  **Consumer hardening:** set a per-server `"timeout"` (milliseconds) in `.mcp.json` so a hang aborts on a
488
488
  predictable schedule instead of running to the client's own default idle limit (as long as 30 minutes).
489
+ In Claude Code the value goes inside each server's entry under `mcpServers` — `"timeout": 30000` is 30
490
+ seconds — and must be 1000 or more; a smaller value is ignored (Claude Code MCP docs, checked 2026-10-02;
491
+ other clients have their own keys, so check theirs before copying this one). `spell doctor` warns on an
492
+ entry without one and never edits the file: `.mcp.json` is yours. Arcane ships no example `.mcp.json` —
493
+ the `mcp-config-template` scaffold was retired on 2026-10-02 because a root `.mcp.json` is live
494
+ configuration that clients execute, not documentation, and its placeholder server failed on every session start.
489
495
  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.**
496
+ 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
497
 
492
498
  **Known issues:**
493
499
 
@@ -1,10 +1,10 @@
1
1
  ---
2
2
  name: Spell — Sync Pull Request
3
- description: Safely sync an open PR's branch with its target when it has fallen behind — recoverable pre-rebase ref, mechanical-vs-ambiguous conflict handling, force-with-lease push, provider-verified landing. Use when a PR needs rebasing onto a moved target, or was routed here from spell-create-pull-request/spell-ship's own conflict-stop points.
3
+ description: Safely sync an open PR's branch with its target when it has fallen behind — recoverable pre-rebase ref, mechanical / regenerable / ambiguous conflict handling, force-with-lease push, provider-verified landing. Use when a PR needs rebasing onto a moved target, or was routed here from spell-create-pull-request/spell-ship's own conflict-stop points.
4
4
  claude_description: Use PROACTIVELY whenever an open PR has fallen behind its target branch and needs a safe rebase/sync, or when spell-create-pull-request/spell-ship stop on a conflict they route here.
5
5
  argument-hint: '[PR number or branch name]'
6
6
  agent: agent
7
- last_updated: 2026-08-31
7
+ last_updated: 2026-10-01
8
8
  ---
9
9
 
10
10
  ## Executive Summary
@@ -13,7 +13,9 @@ last_updated: 2026-08-31
13
13
  - It is the recovery path `spell-create-pull-request` and `spell-ship` route to when they detect a
14
14
  conflict they deliberately don't try to resolve themselves.
15
15
  - It draws a hard line between conflicts safe to resolve mechanically (identical changes on both sides)
16
- and conflicts that need a human — it never guesses on the ambiguous side of that line.
16
+ and conflicts that need a human — it never guesses on the ambiguous side of that line. A third class,
17
+ a script-regenerable artifact, is neither merged nor handed off: it is regenerated against the rebased
18
+ tree and verified by its own check.
17
19
  - Every history-rewriting step is preceded by a recoverable ref, and every push uses
18
20
  `--force-with-lease`, never bare `--force`.
19
21
 
@@ -72,7 +74,7 @@ Resolve the repository's actual merge-strategy ladder from `git-conventions.md`
72
74
  - **Never** produce a squash or semi-linear result as part of this sync — those are never sanctioned,
73
75
  regardless of which of the two above applies.
74
76
 
75
- ## Step 3 — Classify any conflict: mechanical vs. ambiguous
77
+ ## Step 3 — Classify any conflict: mechanical, regenerable, or ambiguous
76
78
 
77
79
  If Step 2 completes with no conflicts, skip straight to Step 4 — **this is the common case and it must
78
80
  not be treated as a special case of conflict handling.**
@@ -84,6 +86,25 @@ then classify **each conflicted hunk independently** — a single file can conta
84
86
  same import added the same way), or one side's change is a strict superset of the other's with no
85
87
  semantic divergence. Resolve by keeping the superset/identical content, verify no line from either
86
88
  side's *unique* content was dropped, stage the file, and continue the rebase/merge.
89
+ - **Regenerable** — the file is a committed artifact that a script owns: a known command regenerates it
90
+ from other files in the tree and overwrites it unconditionally, ignoring any conflict markers already
91
+ in it. Neither side's content is the right answer, because both were computed from trees that no
92
+ longer exist; the correct content is a fresh regeneration against the post-rebase tree. **Do not
93
+ hand-merge the diff and do not pick a side.** Leave the conflicted file as it is, run its regeneration
94
+ command, verify with the file's own `--check` command, then stage the file and continue the
95
+ rebase/merge. Known artifacts and their commands:
96
+ - `docs/plans/*/show-report.{json,html}` — `npm run fix:report` in Arcane's own repository (`spell
97
+ report` in a consumer), verified by `npm run check:report`. On a program's own `completed:` day the
98
+ regeneration must go in a **trailer-free, report-only commit** that touches nothing else, because
99
+ the close-commit logic counts any other commit made that day (recorded in
100
+ `docs/plans/upstream-intake-2026-09/PLAN.md`, "What this program taught").
101
+ - Self-hosted parity copies and client shims under `.arcane/`, `.github/`, `.claude/` and `.agents/`
102
+ in Arcane's own repository — `npm run fix:self-host-parity`, verified by
103
+ `npm run check:self-host-parity`. A compiled asset with its own `fix:*`/`check:*` pair in
104
+ `package.json` follows the same rule.
105
+ - `package-lock.json` — `npm install` against the rebased `package.json`, verified by `npm ci`.
106
+ A file you cannot name a regeneration command for is not regenerable: classify its hunks as mechanical
107
+ or ambiguous like any other, and never promote a hand-edited file to this class to avoid a STOP.
87
108
  - **Ambiguous** — the same lines changed with different intent, or you cannot establish with confidence
88
109
  that one side's content is a strict superset of the other's. **STOP.** Do not guess, do not pick a
89
110
  side, do not attempt a "reasonable-looking" merge of the two. Abort the in-progress rebase/merge
@@ -177,6 +198,8 @@ report the discrepancy rather than declaring the sync done.
177
198
  ```
178
199
 
179
200
  - If Step 3 resolved any mechanical conflicts, list exactly which files and what was kept from each side.
201
+ - If Step 3 regenerated any artifact, name the file, the regeneration command that was run and the
202
+ `--check` command that verified the result.
180
203
  - If Step 3 aborted on an ambiguous conflict, this report never runs — Step 3's own STOP report is final.
181
204
  - Never delete the Step 1 recoverable ref as part of this report — that is the human's call.
182
205
 
@@ -185,6 +208,9 @@ report the discrepancy rather than declaring the sync done.
185
208
  - Never push with bare `--force` — only `--force-with-lease`, and never retry a rejected lease blindly.
186
209
  - Never guess on an ambiguous conflict — abort and hand off, every time, with no exception for
187
210
  "obviously trivial-looking" content on either side that this spell cannot independently verify.
211
+ - Never hand-merge a script-regenerable artifact — regenerate it against the rebased tree and verify it
212
+ with its own check; and never classify a file as regenerable without naming the command that
213
+ regenerates it.
188
214
  - Never skip the recoverable ref (Step 1), even for a sync expected to be clean — the ref costs nothing
189
215
  and the alternative is an unrecoverable mistake on the case that turns out not to be clean.
190
216
  - Never report a sync as complete on a push success alone — provider verification (Step 5) is mandatory.
@@ -1,10 +1,10 @@
1
1
  ---
2
2
  name: Spell — Sync Pull Request
3
- description: Safely sync an open PR's branch with its target when it has fallen behind — recoverable pre-rebase ref, mechanical-vs-ambiguous conflict handling, force-with-lease push, provider-verified landing. Use when a PR needs rebasing onto a moved target, or was routed here from spell-create-pull-request/spell-ship's own conflict-stop points.
3
+ description: Safely sync an open PR's branch with its target when it has fallen behind — recoverable pre-rebase ref, mechanical / regenerable / ambiguous conflict handling, force-with-lease push, provider-verified landing. Use when a PR needs rebasing onto a moved target, or was routed here from spell-create-pull-request/spell-ship's own conflict-stop points.
4
4
  claude_description: Use PROACTIVELY whenever an open PR has fallen behind its target branch and needs a safe rebase/sync, or when spell-create-pull-request/spell-ship stop on a conflict they route here.
5
5
  argument-hint: '[PR number or branch name]'
6
6
  agent: agent
7
- last_updated: 2026-08-31
7
+ last_updated: 2026-10-01
8
8
  ---
9
9
 
10
10
  This prompt is the Arcane `spell-sync-pull-request` spell. Read [`.arcane/spells/spell-sync-pull-request.md`](../../.arcane/spells/spell-sync-pull-request.md) and follow it as the complete workflow.
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 {