arcane-cli 1.10.2 → 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
 
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 {