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 +1 -1
- package/dist/assets/.arcane/governance/git-conventions.md +7 -1
- package/dist/assets/.arcane/spells/spell-sync-pull-request.md +30 -4
- package/dist/assets/.github/prompts/spell-sync-pull-request.prompt.md +2 -2
- 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
|
|
|
@@ -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
|
|
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-
|
|
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
|
|
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
|
|
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-
|
|
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
|
-
//
|
|
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 {
|