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 +1 -1
- package/dist/assets/.arcane/governance/git-conventions.md +7 -1
- package/dist/index.js +61 -13
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/dist/assets/.mcp.json +0 -10
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 (`.
|
|
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)
|
|
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
|
-
//
|
|
583
|
-
//
|
|
584
|
-
//
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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 {
|