arcane-cli 1.12.0 → 1.12.2

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/dist/index.js CHANGED
@@ -6,25 +6,296 @@ const require = __createRequire(import.meta.url);
6
6
  import { Command } from "commander";
7
7
  import { createRequire as createRequire3 } from "module";
8
8
  import { fileURLToPath } from "url";
9
- import { dirname as dirname12, join as join22 } from "path";
9
+ import { dirname as dirname13, join as join23 } from "path";
10
10
 
11
11
  // src/commands/init.ts
12
12
  import { select as select3, confirm as confirm2, input as input3 } from "@inquirer/prompts";
13
- import { dirname as dirname4, join as join7 } from "path";
13
+ import { dirname as dirname5, join as join8 } from "path";
14
14
  import chalk2 from "chalk";
15
15
 
16
16
  // src/modules/copier.ts
17
17
  import {
18
18
  mkdir,
19
19
  access,
20
+ lstat,
20
21
  readdir,
21
22
  readFile,
22
23
  rm,
23
24
  rmdir,
25
+ unlink,
24
26
  writeFile
25
27
  } from "fs/promises";
26
28
  import { createHash } from "crypto";
27
29
  import path from "path";
30
+
31
+ // src/modules/git.ts
32
+ import { execFile } from "child_process";
33
+ var GitTimeoutError = class extends Error {
34
+ constructor(message, options) {
35
+ super(message, options);
36
+ this.name = "GitTimeoutError";
37
+ }
38
+ };
39
+ var NETWORK_SUBCOMMANDS = /* @__PURE__ */ new Set(["fetch", "pull", "push", "clone", "ls-remote", "send-email"]);
40
+ var WRITE_SUBCOMMANDS = /* @__PURE__ */ new Set([
41
+ "init",
42
+ "commit",
43
+ "add",
44
+ "checkout",
45
+ "switch",
46
+ "merge",
47
+ "rebase",
48
+ "reset",
49
+ "tag",
50
+ "mv",
51
+ "rm",
52
+ "cherry-pick",
53
+ "revert",
54
+ "stash",
55
+ "apply",
56
+ "am",
57
+ "restore",
58
+ "submodule",
59
+ "sparse-checkout",
60
+ "maintenance",
61
+ "notes"
62
+ ]);
63
+ var AMBIGUOUS_AS_WRITE = /* @__PURE__ */ new Set(["branch", "config", "remote", "worktree"]);
64
+ var READ_SUBCOMMANDS = /* @__PURE__ */ new Set([
65
+ "status",
66
+ "rev-parse",
67
+ "log",
68
+ "diff",
69
+ "show",
70
+ "describe",
71
+ "cat-file",
72
+ "ls-files",
73
+ "ls-tree",
74
+ "symbolic-ref",
75
+ "show-ref",
76
+ "for-each-ref",
77
+ "merge-base",
78
+ "blame",
79
+ "shortlog",
80
+ "grep",
81
+ "reflog",
82
+ "diff-tree",
83
+ "rev-list",
84
+ "name-rev",
85
+ "count-objects",
86
+ "verify-commit",
87
+ "verify-tag",
88
+ "help",
89
+ "version"
90
+ ]);
91
+ var GLOBAL_FLAGS_WITH_SEPARATE_VALUE = /* @__PURE__ */ new Set(["-c", "-C"]);
92
+ var DEFAULT_TIMEOUTS_MS = {
93
+ read: 15e3,
94
+ write: 3e4,
95
+ network: 12e4
96
+ };
97
+ function classifyGitCommand(args) {
98
+ let subcommand;
99
+ for (let i = 0; i < args.length; i++) {
100
+ const arg = args[i];
101
+ if (GLOBAL_FLAGS_WITH_SEPARATE_VALUE.has(arg)) {
102
+ i++;
103
+ continue;
104
+ }
105
+ if (arg.startsWith("-")) continue;
106
+ subcommand = arg;
107
+ break;
108
+ }
109
+ if (!subcommand) return "read";
110
+ if (NETWORK_SUBCOMMANDS.has(subcommand)) return "network";
111
+ if (WRITE_SUBCOMMANDS.has(subcommand) || AMBIGUOUS_AS_WRITE.has(subcommand)) return "write";
112
+ if (READ_SUBCOMMANDS.has(subcommand)) return "read";
113
+ return "write";
114
+ }
115
+ function buildGitEnv() {
116
+ return {
117
+ ...process.env,
118
+ GIT_TERMINAL_PROMPT: "0",
119
+ GCM_INTERACTIVE: "Never",
120
+ GIT_OPTIONAL_LOCKS: "0"
121
+ };
122
+ }
123
+ function runGit(cwd, args, options = {}) {
124
+ const commandClass = options.commandClass ?? classifyGitCommand(args);
125
+ const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUTS_MS[commandClass] ?? DEFAULT_TIMEOUTS_MS.write;
126
+ return new Promise((resolve6, reject) => {
127
+ const child = execFile(
128
+ "git",
129
+ args,
130
+ {
131
+ cwd,
132
+ env: buildGitEnv(),
133
+ timeout: timeoutMs,
134
+ // SIGTERM (Node's default) rather than SIGKILL: gives git a chance
135
+ // to run its own cleanup on POSIX, relevant to EF-20's own concern
136
+ // about a killed operation leaving refs/locks in an uncertain state.
137
+ // Moot on Windows -- Node docs: both map to an abrupt TerminateProcess
138
+ // there -- but this module isn't Windows-gated.
139
+ killSignal: "SIGTERM",
140
+ windowsHide: true,
141
+ encoding: "utf8"
142
+ },
143
+ (error, stdout, stderr) => {
144
+ if (error) {
145
+ if (error.killed) {
146
+ reject(
147
+ new GitTimeoutError(
148
+ // NOTE: embeds the raw args, including any secrets a future
149
+ // network-class caller might pass (e.g. a credentialed
150
+ // clone URL). No current caller does; redact before one does.
151
+ `git ${args.join(" ")} timed out after ${timeoutMs}ms (class: ${commandClass})`,
152
+ { cause: error }
153
+ )
154
+ );
155
+ return;
156
+ }
157
+ reject(error);
158
+ return;
159
+ }
160
+ resolve6({ stdout, stderr });
161
+ }
162
+ );
163
+ child.stdin?.end();
164
+ });
165
+ }
166
+ async function inspectGitRepository(cwd) {
167
+ try {
168
+ const { stdout: stdout2 } = await runGit(cwd, ["rev-parse", "--is-inside-work-tree"]);
169
+ if (stdout2.trim() !== "true") {
170
+ return { status: "not-repository" };
171
+ }
172
+ } catch {
173
+ return { status: "not-repository" };
174
+ }
175
+ try {
176
+ await runGit(cwd, ["rev-parse", "--verify", "HEAD"]);
177
+ } catch {
178
+ return { status: "no-commits" };
179
+ }
180
+ const { stdout } = await runGit(cwd, ["status", "--porcelain", "-uall"]);
181
+ const lines = stdout.trim();
182
+ return {
183
+ status: "ready",
184
+ uncommittedChanges: lines.length === 0 ? 0 : lines.split("\n").length
185
+ };
186
+ }
187
+ async function correctUnbornMasterDefault(cwd) {
188
+ let current;
189
+ try {
190
+ const { stdout } = await runGit(cwd, ["symbolic-ref", "--short", "HEAD"]);
191
+ current = stdout.trim();
192
+ } catch {
193
+ return { corrected: false, to: "main" };
194
+ }
195
+ if (current !== "master") {
196
+ return { corrected: false, to: "main" };
197
+ }
198
+ try {
199
+ await runGit(cwd, ["rev-parse", "--verify", "HEAD"]);
200
+ return { corrected: false, to: "main" };
201
+ } catch {
202
+ }
203
+ let mainRef;
204
+ try {
205
+ mainRef = await runGit(cwd, ["for-each-ref", "--format=%(objectname)", "refs/heads/main"]);
206
+ } catch {
207
+ return { corrected: false, to: "main", blockedReason: "target-unreadable" };
208
+ }
209
+ if (mainRef.stdout.trim() !== "") {
210
+ return { corrected: false, to: "main" };
211
+ }
212
+ if (mainRef.stderr.trim() !== "") {
213
+ return { corrected: false, to: "main", blockedReason: "target-unreadable" };
214
+ }
215
+ try {
216
+ await runGit(cwd, ["symbolic-ref", "-q", "refs/heads/main"]);
217
+ return { corrected: false, to: "main", blockedReason: "target-unreadable" };
218
+ } catch (error) {
219
+ if (error.code !== 1) {
220
+ return { corrected: false, to: "main", blockedReason: "target-unreadable" };
221
+ }
222
+ }
223
+ await runGit(cwd, ["symbolic-ref", "HEAD", "refs/heads/main"]);
224
+ return { corrected: true, from: current, to: "main" };
225
+ }
226
+ async function ensureLocalPullRebase(cwd) {
227
+ let rawLocalValue;
228
+ try {
229
+ const { stdout } = await runGit(cwd, ["config", "--local", "--get", "pull.rebase"]);
230
+ rawLocalValue = stdout.trim();
231
+ } catch {
232
+ rawLocalValue = void 0;
233
+ }
234
+ if (rawLocalValue === void 0) {
235
+ await runGit(cwd, ["config", "--local", "pull.rebase", "true"]);
236
+ return { action: "set" };
237
+ }
238
+ try {
239
+ const { stdout } = await runGit(cwd, ["config", "--local", "--type=bool", "--get", "pull.rebase"]);
240
+ if (stdout.trim() === "false") {
241
+ return { action: "explicit-false-preserved" };
242
+ }
243
+ return { action: "already-set" };
244
+ } catch {
245
+ return { action: "already-set" };
246
+ }
247
+ }
248
+ async function listSymlinkTrackedPaths(cwd) {
249
+ const paths = /* @__PURE__ */ new Set();
250
+ let stdout;
251
+ try {
252
+ ({ stdout } = await runGit(cwd, ["ls-files", "-s", "-z"]));
253
+ } catch {
254
+ return paths;
255
+ }
256
+ for (const entry of stdout.split("\0")) {
257
+ const tab = entry.indexOf(" ");
258
+ if (tab < 0) continue;
259
+ const [mode] = entry.slice(0, tab).split(" ");
260
+ if (mode === "120000") paths.add(entry.slice(tab + 1));
261
+ }
262
+ return paths;
263
+ }
264
+ async function recordRegularFileInIndex(cwd, relativePath) {
265
+ const { stdout } = await runGit(cwd, ["hash-object", "-w", "--", relativePath]);
266
+ const { stdout: prefix } = await runGit(cwd, ["rev-parse", "--show-prefix"]);
267
+ const rootRelative = `${prefix.trim()}${relativePath.replace(/\\/g, "/")}`;
268
+ await runGit(cwd, ["update-index", "--add", "--cacheinfo", `100644,${stdout.trim()},${rootRelative}`]);
269
+ }
270
+ async function isUneditedTrackedFile(cwd, relativePath) {
271
+ let status;
272
+ try {
273
+ ({ stdout: status } = await runGit(cwd, ["status", "--porcelain", "-z", "--", relativePath]));
274
+ } catch {
275
+ return { verdict: "unknown", detail: "not a git repository, or git is unavailable" };
276
+ }
277
+ if (status.startsWith("??")) return { verdict: "untracked", detail: "git does not track it" };
278
+ if (status.length > 0) return { verdict: "edited", detail: "it has uncommitted changes" };
279
+ try {
280
+ const { stdout: shallow } = await runGit(cwd, ["rev-parse", "--is-shallow-repository"]);
281
+ if (shallow.trim() === "true") {
282
+ return { verdict: "unknown", detail: "this is a shallow clone, so the history is truncated" };
283
+ }
284
+ } catch {
285
+ }
286
+ let log;
287
+ try {
288
+ ({ stdout: log } = await runGit(cwd, ["log", "--format=%H", "--", relativePath]));
289
+ } catch {
290
+ return { verdict: "unknown", detail: "git log could not read its history" };
291
+ }
292
+ const commits = log.split("\n").filter(Boolean).length;
293
+ if (commits === 0) return { verdict: "untracked", detail: "no commit contains it" };
294
+ if (commits > 1) return { verdict: "edited", detail: `${commits} commits touched it` };
295
+ return { verdict: "unedited", detail: "tracked, clean, and added by its only commit" };
296
+ }
297
+
298
+ // src/modules/copier.ts
28
299
  function normalizeLineEndings(content) {
29
300
  if (!content.includes(13)) return content;
30
301
  const out = Buffer.allocUnsafe(content.length);
@@ -72,7 +343,52 @@ async function fileExists(filePath) {
72
343
  return false;
73
344
  }
74
345
  }
75
- async function copyFile(srcPath, targetDir, relativePath, opts = {}) {
346
+ async function replaceSymlinkedDestination(destPath, relativePath, opts) {
347
+ let isLink;
348
+ try {
349
+ isLink = (await lstat(destPath)).isSymbolicLink();
350
+ } catch {
351
+ return null;
352
+ }
353
+ if (isLink) {
354
+ await unlink(destPath);
355
+ return "symlink";
356
+ }
357
+ if (opts.linkTracked?.has(relativePath.replace(/\\/g, "/"))) {
358
+ await unlink(destPath);
359
+ return "index-symlink";
360
+ }
361
+ return null;
362
+ }
363
+ function looksLikeBarePath(body) {
364
+ const trimmed = body.trim();
365
+ return trimmed.length <= 512 && /^\.{1,2}\/\S+$/.test(trimmed) && !trimmed.includes("://");
366
+ }
367
+ async function managedPathLinkShape(targetDir, relativePath, linkTracked, bodyHeuristic) {
368
+ const destPath = path.resolve(targetDir, relativePath);
369
+ try {
370
+ if ((await lstat(destPath)).isSymbolicLink()) return "symlink";
371
+ } catch {
372
+ return null;
373
+ }
374
+ if (linkTracked?.has(relativePath.replace(/\\/g, "/"))) return "index-symlink";
375
+ if (!bodyHeuristic) return null;
376
+ try {
377
+ return looksLikeBarePath(await readFile(destPath, "utf-8")) ? "path-body" : null;
378
+ } catch {
379
+ return null;
380
+ }
381
+ }
382
+ async function writeManagedFile(targetDir, relativePath, content, opts = {}) {
383
+ validateTargetPath(targetDir, relativePath);
384
+ const destPath = path.resolve(targetDir, relativePath);
385
+ await ensureDir(path.dirname(destPath));
386
+ const shape = await replaceSymlinkedDestination(destPath, relativePath, opts);
387
+ await writeFile(destPath, content);
388
+ if (shape === "index-symlink") await recordRegularFileInIndex(targetDir, relativePath);
389
+ if (shape !== null) opts.conversions?.push({ file: relativePath, shape });
390
+ }
391
+ async function copyFile(srcPath, targetDir, relativePath, opts = {}) {
76
392
  validateTargetPath(targetDir, relativePath);
77
393
  const destPath = path.resolve(targetDir, relativePath);
78
394
  await ensureDir(path.dirname(destPath));
@@ -83,7 +399,7 @@ async function copyFile(srcPath, targetDir, relativePath, opts = {}) {
83
399
  }
84
400
  const content = await readFile(srcPath);
85
401
  const hash = hashContent(content);
86
- await writeFile(destPath, content);
402
+ await writeManagedFile(targetDir, relativePath, content, opts);
87
403
  return hash;
88
404
  }
89
405
  async function removeWithin(targetDir, relativePath) {
@@ -976,7 +1292,7 @@ function listProfiles() {
976
1292
  }
977
1293
 
978
1294
  // src/modules/user-tier.ts
979
- import { lstat, mkdir as mkdir2, readdir as readdir2, readFile as readFile3, rm as rm2, rmdir as rmdir2, writeFile as writeFile3 } from "fs/promises";
1295
+ import { lstat as lstat2, mkdir as mkdir2, readdir as readdir2, readFile as readFile3, rm as rm2, rmdir as rmdir2, writeFile as writeFile3 } from "fs/promises";
980
1296
  import { homedir } from "os";
981
1297
  import { dirname, join, resolve } from "path";
982
1298
 
@@ -1192,7 +1508,7 @@ async function renderUserFanout(storeRoot, spellIds, fallbackDir) {
1192
1508
  }
1193
1509
  async function classifyTarget(path4) {
1194
1510
  try {
1195
- const stats = await lstat(path4);
1511
+ const stats = await lstat2(path4);
1196
1512
  return stats.isFile() ? "file" : "other";
1197
1513
  } catch (err) {
1198
1514
  if (err.code === "ENOENT") return "absent";
@@ -1417,7 +1733,7 @@ var WORDMARK = [
1417
1733
  var INDENT = " ";
1418
1734
  var LOCKUP_WIDTH = INDENT.length + Math.max(...WORDMARK.map((l) => l.length));
1419
1735
  function sleep(ms) {
1420
- return new Promise((resolve5) => setTimeout(resolve5, ms));
1736
+ return new Promise((resolve6) => setTimeout(resolve6, ms));
1421
1737
  }
1422
1738
  function isAnimatable() {
1423
1739
  return !!process.stdout.isTTY && process.env["TERM"] !== "dumb" && process.env["CI"] !== "true" && process.env["ARCANE_DISABLE_ANIMATION"] !== "true" && (process.stdout.columns ?? 80) >= LOCKUP_WIDTH;
@@ -2194,7 +2510,10 @@ ${rules}
2194
2510
  **Denied:** ${def.tools.denied.join(", ")}
2195
2511
  `;
2196
2512
  }
2197
- function renderSpellRoutingSection() {
2513
+ function renderSpellRoutingSection(spellScope) {
2514
+ const userTierRule = spellScope === "user" ? `
2515
+ This repository takes its spells from the user tier (\`~/.arcane\`), not from files it carries. If a spell named here does not resolve for your client, stop and say that \`spell init --user\` is needed on this machine; do not improvise the workflow from general knowledge.
2516
+ ` : "";
2198
2517
  return `## Spell Routing
2199
2518
 
2200
2519
  | When you're about to... | Invoke |
@@ -2208,7 +2527,7 @@ function renderSpellRoutingSection() {
2208
2527
  | Review code or a PR | \`spell-review\` |
2209
2528
 
2210
2529
  If a spell exists for the workflow you are about to perform, invoke it \u2014 do not improvise the workflow from general knowledge, even when the user doesn't name the spell.
2211
- `;
2530
+ ${userTierRule}`;
2212
2531
  }
2213
2532
  async function loadBranchNamingFragment(assetsDir) {
2214
2533
  try {
@@ -2370,7 +2689,7 @@ async function syncAgents(targetDir, assetsDir, roster, options = {}) {
2370
2689
  unmanagedAgentFiles.push(...await existingAgentFiles(agentsDir));
2371
2690
  }
2372
2691
  if (!userTier) {
2373
- const copilotSection = `${renderSpellRoutingSection()}
2692
+ const copilotSection = `${renderSpellRoutingSection(options.spellScope)}
2374
2693
  ${branchNaming}${renderAgentRosterSection(resolved)}`;
2375
2694
  const copilotMerged = await mergeIntoFile(
2376
2695
  targetDir,
@@ -2386,7 +2705,7 @@ ${branchNaming}${renderAgentRosterSection(resolved)}`;
2386
2705
  return { synced, skipped, hasUnresolvedRoles, userAgentFiles, unmanagedAgentFiles };
2387
2706
  }
2388
2707
  if (options.claude !== false) {
2389
- const claudeSection = `${renderSpellRoutingSection()}
2708
+ const claudeSection = `${renderSpellRoutingSection(options.spellScope)}
2390
2709
  ${branchNaming}${renderAgentRosterSection(resolved)}`;
2391
2710
  const claudeMerged = await mergeIntoFile(
2392
2711
  targetDir,
@@ -2397,7 +2716,7 @@ ${branchNaming}${renderAgentRosterSection(resolved)}`;
2397
2716
  if (claudeMerged) synced.push("CLAUDE.md (agents section)");
2398
2717
  }
2399
2718
  if (options.codex !== false) {
2400
- const codexSection = `${renderSpellRoutingSection()}
2719
+ const codexSection = `${renderSpellRoutingSection(options.spellScope)}
2401
2720
  ${branchNaming}${renderAgentRosterSection(resolved)}`;
2402
2721
  const codexMerged = await mergeIntoFile(
2403
2722
  targetDir,
@@ -2604,374 +2923,156 @@ All 12 roles across ${allCategories.join(", ")}` : "\nPick individual roles from
2604
2923
  }
2605
2924
  for (const roleId of selectedRoleIds) {
2606
2925
  const src = join4(assetsDir, "agents", `${roleId}.yaml`);
2607
- const dest = join4(arcaneDir, `${roleId}.yaml`);
2608
- if (!options.dryRun) {
2609
- try {
2610
- await copyFile2(src, dest);
2611
- } catch {
2612
- console.warn(` ! Could not copy definition for "${roleId}" \u2014 skipping`);
2613
- }
2614
- } else {
2615
- console.log(` [dry-run] Would copy: ${label2}/agents/${roleId}.yaml`);
2616
- }
2617
- }
2618
- if (options.dryRun) {
2619
- console.log(
2620
- `
2621
- [dry-run] Would initialize roster with ${selectedRoleIds.length} agents`
2622
- );
2623
- return;
2624
- }
2625
- printStep("Syncing agent definitions to all clients...");
2626
- const { synced, skipped, hasUnresolvedRoles } = await performAgentSync(
2627
- targetDir,
2628
- assetsDir,
2629
- roster,
2630
- { dryRun: false, scope }
2631
- );
2632
- printSuccess("Agent roster initialized");
2633
- console.log(` \u{1F4CB} Profile: ${profileId} \xB7 Naming: ${namingStrategy} \xB7 Agents: ${rosterEntries.length}`);
2634
- console.log(
2635
- ` \u{1F504} Synced: ${synced.length} outputs (${scope === "user" ? "agent personas in ~/.claude/agents" : "Copilot, Claude, Codex, OpenClaw"})`
2636
- );
2637
- if (skipped.length > 0) {
2638
- console.log(` \u26A0\uFE0F Skipped: ${skipped.length} (${skipped.join(", ")})`);
2639
- }
2640
- console.log();
2641
- console.log(
2642
- ` \u{1F4A1} Customize: edit ${label2}/agents.yaml \u2192 spell agents sync${scope === "user" ? " --user" : ""}`
2643
- );
2644
- console.log();
2645
- if (hasUnresolvedRoles) {
2646
- console.error(
2647
- ' \u2717 One or more rostered roles could not be resolved (see "Skipped" above) \u2014 a malformed or missing agent template must not pass silently.'
2648
- );
2649
- process.exitCode = 1;
2650
- }
2651
- }
2652
- async function runAgentsSync(options, targetDir, assetsDir) {
2653
- const scope = options.scope ?? "repo";
2654
- let roster;
2655
- let definitionsDir;
2656
- let fromUserTier = false;
2657
- try {
2658
- ({ roster, definitionsDir, fromUserTier } = await resolveRoster(targetDir, scope));
2659
- } catch (err) {
2660
- if (err instanceof AgentRosterNotFoundError || err instanceof AgentConfigValidationError) {
2661
- console.error(err.message);
2662
- process.exit(1);
2663
- return;
2664
- }
2665
- throw err;
2666
- }
2667
- if (options.dryRun) {
2668
- console.log("\n [dry-run] Would sync agents from roster:\n");
2669
- } else {
2670
- console.log("\n Syncing agents...\n");
2671
- }
2672
- if (fromUserTier && scope === "repo") {
2673
- console.log(
2674
- " This repository has no roster of its own and takes its spells from the user tier,\n so its roster tables are built from ~/.arcane/agents.yaml.\n"
2675
- );
2676
- }
2677
- const { synced, skipped, hasUnresolvedRoles } = await performAgentSync(
2678
- targetDir,
2679
- assetsDir,
2680
- roster,
2681
- { ...options, scope, ...definitionsDir === void 0 ? {} : { definitionsDir } }
2682
- );
2683
- if (options.dryRun) {
2684
- console.log(`
2685
- [dry-run] ${synced.length} outputs would be generated`);
2686
- return;
2687
- }
2688
- console.log(`
2689
- \u2713 Synced ${synced.length} outputs`);
2690
- for (const item of synced) {
2691
- console.log(` + ${item}`);
2692
- }
2693
- if (skipped.length > 0) {
2694
- console.log(`
2695
- ! Skipped ${skipped.length}:`);
2696
- for (const item of skipped) {
2697
- console.log(` - ${item}`);
2698
- }
2699
- }
2700
- console.log();
2701
- if (hasUnresolvedRoles) {
2702
- console.error(
2703
- ' \u2717 One or more rostered roles could not be resolved (see "Skipped" above) \u2014 a malformed or missing agent template must not pass silently.'
2704
- );
2705
- process.exitCode = 1;
2706
- }
2707
- }
2708
- async function runAgentsList(targetDir, scope = "repo") {
2709
- let roster;
2710
- try {
2711
- roster = await loadRoster(targetDir, scope);
2712
- } catch (err) {
2713
- if (err instanceof AgentRosterNotFoundError || err instanceof AgentConfigValidationError) {
2714
- console.error(err.message);
2715
- process.exit(1);
2716
- return;
2717
- }
2718
- throw err;
2719
- }
2720
- const nameWidth = Math.max(
2721
- "Name".length,
2722
- ...roster.roster.map((e) => (e.name ?? e.definition).length)
2723
- );
2724
- const defWidth = Math.max(
2725
- "Role ID".length,
2726
- ...roster.roster.map((e) => e.definition.length)
2727
- );
2728
- const idWidth = Math.max(
2729
- "OpenClaw ID".length,
2730
- ...roster.roster.map((e) => e.id.length)
2731
- );
2732
- const pad = (s, n) => s.padEnd(n);
2733
- console.log(
2734
- `
2735
- Profile: ${roster.agent_profile} | Naming: ${roster.naming_strategy} | Agents: ${roster.roster.length}
2736
- `
2737
- );
2738
- console.log(
2739
- ` ${pad("Name", nameWidth)} ${pad("Role ID", defWidth)} ${pad("OpenClaw ID", idWidth)}`
2740
- );
2741
- console.log(
2742
- ` ${"-".repeat(nameWidth)} ${"-".repeat(defWidth)} ${"-".repeat(idWidth)}`
2743
- );
2744
- for (const entry of roster.roster) {
2745
- console.log(
2746
- ` ${pad(entry.name ?? "(unnamed)", nameWidth)} ${pad(entry.definition, defWidth)} ${pad(entry.id, idWidth)}`
2747
- );
2748
- }
2749
- console.log();
2750
- }
2751
-
2752
- // src/modules/hub.ts
2753
- import { confirm, select as select2, input as input2 } from "@inquirer/prompts";
2754
- import { readdir as readdir5, writeFile as writeFile7 } from "fs/promises";
2755
- import { join as join5 } from "path";
2756
-
2757
- // src/modules/git.ts
2758
- import { execFile } from "child_process";
2759
- var GitTimeoutError = class extends Error {
2760
- constructor(message, options) {
2761
- super(message, options);
2762
- this.name = "GitTimeoutError";
2763
- }
2764
- };
2765
- var NETWORK_SUBCOMMANDS = /* @__PURE__ */ new Set(["fetch", "pull", "push", "clone", "ls-remote", "send-email"]);
2766
- var WRITE_SUBCOMMANDS = /* @__PURE__ */ new Set([
2767
- "init",
2768
- "commit",
2769
- "add",
2770
- "checkout",
2771
- "switch",
2772
- "merge",
2773
- "rebase",
2774
- "reset",
2775
- "tag",
2776
- "mv",
2777
- "rm",
2778
- "cherry-pick",
2779
- "revert",
2780
- "stash",
2781
- "apply",
2782
- "am",
2783
- "restore",
2784
- "submodule",
2785
- "sparse-checkout",
2786
- "maintenance",
2787
- "notes"
2788
- ]);
2789
- var AMBIGUOUS_AS_WRITE = /* @__PURE__ */ new Set(["branch", "config", "remote", "worktree"]);
2790
- var READ_SUBCOMMANDS = /* @__PURE__ */ new Set([
2791
- "status",
2792
- "rev-parse",
2793
- "log",
2794
- "diff",
2795
- "show",
2796
- "describe",
2797
- "cat-file",
2798
- "ls-files",
2799
- "ls-tree",
2800
- "symbolic-ref",
2801
- "show-ref",
2802
- "for-each-ref",
2803
- "merge-base",
2804
- "blame",
2805
- "shortlog",
2806
- "grep",
2807
- "reflog",
2808
- "diff-tree",
2809
- "rev-list",
2810
- "name-rev",
2811
- "count-objects",
2812
- "verify-commit",
2813
- "verify-tag",
2814
- "help",
2815
- "version"
2816
- ]);
2817
- var GLOBAL_FLAGS_WITH_SEPARATE_VALUE = /* @__PURE__ */ new Set(["-c", "-C"]);
2818
- var DEFAULT_TIMEOUTS_MS = {
2819
- read: 15e3,
2820
- write: 3e4,
2821
- network: 12e4
2822
- };
2823
- function classifyGitCommand(args) {
2824
- let subcommand;
2825
- for (let i = 0; i < args.length; i++) {
2826
- const arg = args[i];
2827
- if (GLOBAL_FLAGS_WITH_SEPARATE_VALUE.has(arg)) {
2828
- i++;
2829
- continue;
2830
- }
2831
- if (arg.startsWith("-")) continue;
2832
- subcommand = arg;
2833
- break;
2834
- }
2835
- if (!subcommand) return "read";
2836
- if (NETWORK_SUBCOMMANDS.has(subcommand)) return "network";
2837
- if (WRITE_SUBCOMMANDS.has(subcommand) || AMBIGUOUS_AS_WRITE.has(subcommand)) return "write";
2838
- if (READ_SUBCOMMANDS.has(subcommand)) return "read";
2839
- return "write";
2840
- }
2841
- function buildGitEnv() {
2842
- return {
2843
- ...process.env,
2844
- GIT_TERMINAL_PROMPT: "0",
2845
- GCM_INTERACTIVE: "Never",
2846
- GIT_OPTIONAL_LOCKS: "0"
2847
- };
2848
- }
2849
- function runGit(cwd, args, options = {}) {
2850
- const commandClass = options.commandClass ?? classifyGitCommand(args);
2851
- const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUTS_MS[commandClass] ?? DEFAULT_TIMEOUTS_MS.write;
2852
- return new Promise((resolve5, reject) => {
2853
- const child = execFile(
2854
- "git",
2855
- args,
2856
- {
2857
- cwd,
2858
- env: buildGitEnv(),
2859
- timeout: timeoutMs,
2860
- // SIGTERM (Node's default) rather than SIGKILL: gives git a chance
2861
- // to run its own cleanup on POSIX, relevant to EF-20's own concern
2862
- // about a killed operation leaving refs/locks in an uncertain state.
2863
- // Moot on Windows -- Node docs: both map to an abrupt TerminateProcess
2864
- // there -- but this module isn't Windows-gated.
2865
- killSignal: "SIGTERM",
2866
- windowsHide: true,
2867
- encoding: "utf8"
2868
- },
2869
- (error, stdout, stderr) => {
2870
- if (error) {
2871
- if (error.killed) {
2872
- reject(
2873
- new GitTimeoutError(
2874
- // NOTE: embeds the raw args, including any secrets a future
2875
- // network-class caller might pass (e.g. a credentialed
2876
- // clone URL). No current caller does; redact before one does.
2877
- `git ${args.join(" ")} timed out after ${timeoutMs}ms (class: ${commandClass})`,
2878
- { cause: error }
2879
- )
2880
- );
2881
- return;
2882
- }
2883
- reject(error);
2884
- return;
2885
- }
2886
- resolve5({ stdout, stderr });
2926
+ const dest = join4(arcaneDir, `${roleId}.yaml`);
2927
+ if (!options.dryRun) {
2928
+ try {
2929
+ await copyFile2(src, dest);
2930
+ } catch {
2931
+ console.warn(` ! Could not copy definition for "${roleId}" \u2014 skipping`);
2887
2932
  }
2888
- );
2889
- child.stdin?.end();
2890
- });
2891
- }
2892
- async function inspectGitRepository(cwd) {
2893
- try {
2894
- const { stdout: stdout2 } = await runGit(cwd, ["rev-parse", "--is-inside-work-tree"]);
2895
- if (stdout2.trim() !== "true") {
2896
- return { status: "not-repository" };
2933
+ } else {
2934
+ console.log(` [dry-run] Would copy: ${label2}/agents/${roleId}.yaml`);
2897
2935
  }
2898
- } catch {
2899
- return { status: "not-repository" };
2900
2936
  }
2901
- try {
2902
- await runGit(cwd, ["rev-parse", "--verify", "HEAD"]);
2903
- } catch {
2904
- return { status: "no-commits" };
2937
+ if (options.dryRun) {
2938
+ console.log(
2939
+ `
2940
+ [dry-run] Would initialize roster with ${selectedRoleIds.length} agents`
2941
+ );
2942
+ return;
2905
2943
  }
2906
- const { stdout } = await runGit(cwd, ["status", "--porcelain", "-uall"]);
2907
- const lines = stdout.trim();
2908
- return {
2909
- status: "ready",
2910
- uncommittedChanges: lines.length === 0 ? 0 : lines.split("\n").length
2911
- };
2912
- }
2913
- async function correctUnbornMasterDefault(cwd) {
2914
- let current;
2915
- try {
2916
- const { stdout } = await runGit(cwd, ["symbolic-ref", "--short", "HEAD"]);
2917
- current = stdout.trim();
2918
- } catch {
2919
- return { corrected: false, to: "main" };
2944
+ printStep("Syncing agent definitions to all clients...");
2945
+ const { synced, skipped, hasUnresolvedRoles } = await performAgentSync(
2946
+ targetDir,
2947
+ assetsDir,
2948
+ roster,
2949
+ { dryRun: false, scope }
2950
+ );
2951
+ printSuccess("Agent roster initialized");
2952
+ console.log(` \u{1F4CB} Profile: ${profileId} \xB7 Naming: ${namingStrategy} \xB7 Agents: ${rosterEntries.length}`);
2953
+ console.log(
2954
+ ` \u{1F504} Synced: ${synced.length} outputs (${scope === "user" ? "agent personas in ~/.claude/agents" : "Copilot, Claude, Codex, OpenClaw"})`
2955
+ );
2956
+ if (skipped.length > 0) {
2957
+ console.log(` \u26A0\uFE0F Skipped: ${skipped.length} (${skipped.join(", ")})`);
2920
2958
  }
2921
- if (current !== "master") {
2922
- return { corrected: false, to: "main" };
2959
+ console.log();
2960
+ console.log(
2961
+ ` \u{1F4A1} Customize: edit ${label2}/agents.yaml \u2192 spell agents sync${scope === "user" ? " --user" : ""}`
2962
+ );
2963
+ console.log();
2964
+ if (hasUnresolvedRoles) {
2965
+ console.error(
2966
+ ' \u2717 One or more rostered roles could not be resolved (see "Skipped" above) \u2014 a malformed or missing agent template must not pass silently.'
2967
+ );
2968
+ process.exitCode = 1;
2923
2969
  }
2970
+ }
2971
+ async function runAgentsSync(options, targetDir, assetsDir) {
2972
+ const scope = options.scope ?? "repo";
2973
+ let roster;
2974
+ let definitionsDir;
2975
+ let fromUserTier = false;
2924
2976
  try {
2925
- await runGit(cwd, ["rev-parse", "--verify", "HEAD"]);
2926
- return { corrected: false, to: "main" };
2927
- } catch {
2977
+ ({ roster, definitionsDir, fromUserTier } = await resolveRoster(targetDir, scope));
2978
+ } catch (err) {
2979
+ if (err instanceof AgentRosterNotFoundError || err instanceof AgentConfigValidationError) {
2980
+ console.error(err.message);
2981
+ process.exit(1);
2982
+ return;
2983
+ }
2984
+ throw err;
2928
2985
  }
2929
- let mainRef;
2930
- try {
2931
- mainRef = await runGit(cwd, ["for-each-ref", "--format=%(objectname)", "refs/heads/main"]);
2932
- } catch {
2933
- return { corrected: false, to: "main", blockedReason: "target-unreadable" };
2986
+ if (options.dryRun) {
2987
+ console.log("\n [dry-run] Would sync agents from roster:\n");
2988
+ } else {
2989
+ console.log("\n Syncing agents...\n");
2934
2990
  }
2935
- if (mainRef.stdout.trim() !== "") {
2936
- return { corrected: false, to: "main" };
2991
+ if (fromUserTier && scope === "repo") {
2992
+ console.log(
2993
+ " This repository has no roster of its own and takes its spells from the user tier,\n so its roster tables are built from ~/.arcane/agents.yaml.\n"
2994
+ );
2937
2995
  }
2938
- if (mainRef.stderr.trim() !== "") {
2939
- return { corrected: false, to: "main", blockedReason: "target-unreadable" };
2996
+ const { synced, skipped, hasUnresolvedRoles } = await performAgentSync(
2997
+ targetDir,
2998
+ assetsDir,
2999
+ roster,
3000
+ { ...options, scope, ...definitionsDir === void 0 ? {} : { definitionsDir } }
3001
+ );
3002
+ if (options.dryRun) {
3003
+ console.log(`
3004
+ [dry-run] ${synced.length} outputs would be generated`);
3005
+ return;
2940
3006
  }
2941
- try {
2942
- await runGit(cwd, ["symbolic-ref", "-q", "refs/heads/main"]);
2943
- return { corrected: false, to: "main", blockedReason: "target-unreadable" };
2944
- } catch (error) {
2945
- if (error.code !== 1) {
2946
- return { corrected: false, to: "main", blockedReason: "target-unreadable" };
2947
- }
3007
+ console.log(`
3008
+ \u2713 Synced ${synced.length} outputs`);
3009
+ for (const item of synced) {
3010
+ console.log(` + ${item}`);
2948
3011
  }
2949
- await runGit(cwd, ["symbolic-ref", "HEAD", "refs/heads/main"]);
2950
- return { corrected: true, from: current, to: "main" };
2951
- }
2952
- async function ensureLocalPullRebase(cwd) {
2953
- let rawLocalValue;
2954
- try {
2955
- const { stdout } = await runGit(cwd, ["config", "--local", "--get", "pull.rebase"]);
2956
- rawLocalValue = stdout.trim();
2957
- } catch {
2958
- rawLocalValue = void 0;
3012
+ if (skipped.length > 0) {
3013
+ console.log(`
3014
+ ! Skipped ${skipped.length}:`);
3015
+ for (const item of skipped) {
3016
+ console.log(` - ${item}`);
3017
+ }
2959
3018
  }
2960
- if (rawLocalValue === void 0) {
2961
- await runGit(cwd, ["config", "--local", "pull.rebase", "true"]);
2962
- return { action: "set" };
3019
+ console.log();
3020
+ if (hasUnresolvedRoles) {
3021
+ console.error(
3022
+ ' \u2717 One or more rostered roles could not be resolved (see "Skipped" above) \u2014 a malformed or missing agent template must not pass silently.'
3023
+ );
3024
+ process.exitCode = 1;
2963
3025
  }
3026
+ }
3027
+ async function runAgentsList(targetDir, scope = "repo") {
3028
+ let roster;
2964
3029
  try {
2965
- const { stdout } = await runGit(cwd, ["config", "--local", "--type=bool", "--get", "pull.rebase"]);
2966
- if (stdout.trim() === "false") {
2967
- return { action: "explicit-false-preserved" };
3030
+ roster = await loadRoster(targetDir, scope);
3031
+ } catch (err) {
3032
+ if (err instanceof AgentRosterNotFoundError || err instanceof AgentConfigValidationError) {
3033
+ console.error(err.message);
3034
+ process.exit(1);
3035
+ return;
2968
3036
  }
2969
- return { action: "already-set" };
2970
- } catch {
2971
- return { action: "already-set" };
3037
+ throw err;
3038
+ }
3039
+ const nameWidth = Math.max(
3040
+ "Name".length,
3041
+ ...roster.roster.map((e) => (e.name ?? e.definition).length)
3042
+ );
3043
+ const defWidth = Math.max(
3044
+ "Role ID".length,
3045
+ ...roster.roster.map((e) => e.definition.length)
3046
+ );
3047
+ const idWidth = Math.max(
3048
+ "OpenClaw ID".length,
3049
+ ...roster.roster.map((e) => e.id.length)
3050
+ );
3051
+ const pad = (s, n) => s.padEnd(n);
3052
+ console.log(
3053
+ `
3054
+ Profile: ${roster.agent_profile} | Naming: ${roster.naming_strategy} | Agents: ${roster.roster.length}
3055
+ `
3056
+ );
3057
+ console.log(
3058
+ ` ${pad("Name", nameWidth)} ${pad("Role ID", defWidth)} ${pad("OpenClaw ID", idWidth)}`
3059
+ );
3060
+ console.log(
3061
+ ` ${"-".repeat(nameWidth)} ${"-".repeat(defWidth)} ${"-".repeat(idWidth)}`
3062
+ );
3063
+ for (const entry of roster.roster) {
3064
+ console.log(
3065
+ ` ${pad(entry.name ?? "(unnamed)", nameWidth)} ${pad(entry.definition, defWidth)} ${pad(entry.id, idWidth)}`
3066
+ );
2972
3067
  }
3068
+ console.log();
2973
3069
  }
2974
3070
 
3071
+ // src/modules/hub.ts
3072
+ import { confirm, select as select2, input as input2 } from "@inquirer/prompts";
3073
+ import { readdir as readdir5, writeFile as writeFile7 } from "fs/promises";
3074
+ import { join as join5 } from "path";
3075
+
2975
3076
  // src/modules/ado-context.ts
2976
3077
  function remoteUrlHost(url) {
2977
3078
  const trimmed = url.trim();
@@ -3336,7 +3437,27 @@ async function offerRegistryScaffold(targetDir, businessRoot) {
3336
3437
  // src/modules/push-safety.ts
3337
3438
  import { mkdir as mkdir5, readFile as readFile7, writeFile as writeFile8, chmod, access as access3, rm as rm4, readdir as readdir6 } from "fs/promises";
3338
3439
  import { constants } from "fs";
3339
- import { join as join6, resolve as resolve2, dirname as dirname3, basename } from "path";
3440
+ import { join as join7, resolve as resolve3, dirname as dirname4, basename as basename2 } from "path";
3441
+
3442
+ // src/modules/real-path.ts
3443
+ import { realpathSync } from "fs";
3444
+ import { basename, dirname as dirname3, join as join6, resolve as resolve2 } from "path";
3445
+ function resolveRealPath(path4) {
3446
+ const missing = [];
3447
+ let existing = path4;
3448
+ for (; ; ) {
3449
+ try {
3450
+ return join6(realpathSync.native(existing), ...missing);
3451
+ } catch {
3452
+ const parent = dirname3(existing);
3453
+ if (parent === existing) return resolve2(path4);
3454
+ missing.unshift(basename(existing));
3455
+ existing = parent;
3456
+ }
3457
+ }
3458
+ }
3459
+
3460
+ // src/modules/push-safety.ts
3340
3461
  var DISABLED_PUSH_URL = "arcane-push-blocked-run-spell-unblock-push://this-repository";
3341
3462
  var NO_PUSH_URL_MARKER = (remote) => `remote.${remote}.arcaneHadNoPushUrl`;
3342
3463
  var ARCANE_HOOKS_DIR = ".arcane/hooks";
@@ -3365,10 +3486,12 @@ function isArcaneHooksPath(root, value) {
3365
3486
  const windows = process.platform === "win32";
3366
3487
  const normalize = (p) => {
3367
3488
  const separated = windows ? p.replace(/\\/g, "/") : p;
3368
- const absolute = resolve2(root, separated).replace(/\\/g, "/").replace(/\/+$/, "");
3489
+ const resolved = resolve3(root, separated);
3490
+ const absolute = (windows ? resolved.replace(/\\/g, "/") : resolved).replace(/\/+$/, "");
3369
3491
  return windows ? absolute.toLowerCase() : absolute;
3370
3492
  };
3371
- return normalize(value) === normalize(ARCANE_HOOKS_DIR);
3493
+ if (normalize(value) === normalize(ARCANE_HOOKS_DIR)) return true;
3494
+ return normalize(resolveRealPath(resolve3(root, value))) === normalize(join7(resolveRealPath(root), ARCANE_HOOKS_DIR));
3372
3495
  }
3373
3496
  var PRE_PUSH_HOOK_BODY = `#!/bin/sh
3374
3497
  # Installed by Arcane because this repository's .arcane.json sets
@@ -3413,8 +3536,8 @@ async function repositoryRoot(cwd) {
3413
3536
  ]);
3414
3537
  const commonDir = stdout.trim();
3415
3538
  if (commonDir !== "") {
3416
- const parent = dirname3(commonDir);
3417
- if (basename(commonDir) === ".git" && parent !== "") return parent;
3539
+ const parent = dirname4(commonDir);
3540
+ if (basename2(commonDir) === ".git" && parent !== "") return parent;
3418
3541
  }
3419
3542
  } catch {
3420
3543
  }
@@ -3427,10 +3550,11 @@ async function repositoryRoot(cwd) {
3427
3550
  return cwd;
3428
3551
  }
3429
3552
  async function hookFilePath(cwd, hookName = "pre-push") {
3430
- return join6(await repositoryRoot(cwd), ARCANE_HOOKS_DIR, hookName);
3553
+ return join7(await repositoryRoot(cwd), ARCANE_HOOKS_DIR, hookName);
3431
3554
  }
3432
3555
  async function hooksPathValue(cwd) {
3433
- return join6(await repositoryRoot(cwd), ARCANE_HOOKS_DIR).replace(/\\/g, "/");
3556
+ const value = join7(await repositoryRoot(cwd), ARCANE_HOOKS_DIR);
3557
+ return process.platform === "win32" ? value.replace(/\\/g, "/") : value;
3434
3558
  }
3435
3559
  async function installHook(cwd, hookName, body) {
3436
3560
  const root = await repositoryRoot(cwd);
@@ -3449,11 +3573,11 @@ async function installHook(cwd, hookName, body) {
3449
3573
  return { status: "refused-default-hooks", hooks: displaced };
3450
3574
  }
3451
3575
  }
3452
- const hookPath = join6(root, ARCANE_HOOKS_DIR, hookName);
3576
+ const hookPath = join7(root, ARCANE_HOOKS_DIR, hookName);
3453
3577
  if (existing !== void 0 && await hookFileMatches(cwd, hookName, body)) {
3454
3578
  return { status: "already-ours" };
3455
3579
  }
3456
- await mkdir5(join6(root, ARCANE_HOOKS_DIR), { recursive: true });
3580
+ await mkdir5(join7(root, ARCANE_HOOKS_DIR), { recursive: true });
3457
3581
  await writeFile8(hookPath, body, "utf-8");
3458
3582
  await chmod(hookPath, 493);
3459
3583
  await runGit(cwd, ["config", "--local", "core.hooksPath", await hooksPathValue(cwd)]);
@@ -3498,7 +3622,7 @@ async function defaultHooks(cwd) {
3498
3622
  let hooksDir;
3499
3623
  try {
3500
3624
  const { stdout } = await runGit(cwd, ["rev-parse", "--git-common-dir"]);
3501
- hooksDir = join6(resolve2(cwd, stdout.trim()), "hooks");
3625
+ hooksDir = join7(resolve3(cwd, stdout.trim()), "hooks");
3502
3626
  } catch {
3503
3627
  return [];
3504
3628
  }
@@ -3534,7 +3658,7 @@ async function removeHook(cwd, hookName) {
3534
3658
  await rm4(await hookFilePath(cwd, hookName), { force: true });
3535
3659
  } catch {
3536
3660
  }
3537
- const hooksDir = join6(root, ARCANE_HOOKS_DIR);
3661
+ const hooksDir = join7(root, ARCANE_HOOKS_DIR);
3538
3662
  let remaining = [];
3539
3663
  try {
3540
3664
  remaining = await readdir6(hooksDir);
@@ -3937,7 +4061,7 @@ async function runInit(options, targetDir, assetsDir, packageVersion) {
3937
4061
  }
3938
4062
  let flags = {};
3939
4063
  try {
3940
- flags = resolveManifestFlags(options.manifestFlags, {}, join7(targetDir, ".arcane.json"));
4064
+ flags = resolveManifestFlags(options.manifestFlags, {}, join8(targetDir, ".arcane.json"));
3941
4065
  } catch (err) {
3942
4066
  if (!(err instanceof ManifestFlagError)) throw err;
3943
4067
  console.error(`
@@ -4072,7 +4196,7 @@ async function runInit(options, targetDir, assetsDir, packageVersion) {
4072
4196
  const installedFiles = [];
4073
4197
  const fileHashes = {};
4074
4198
  for (const file of component.files) {
4075
- const srcPath = join7(assetsDir, component.sourceOverrides?.[file] ?? file);
4199
+ const srcPath = join8(assetsDir, component.sourceOverrides?.[file] ?? file);
4076
4200
  if (options.dryRun) {
4077
4201
  printDryRun(`Would copy: ${file}`);
4078
4202
  } else {
@@ -4089,7 +4213,7 @@ async function runInit(options, targetDir, assetsDir, packageVersion) {
4089
4213
  fileCount++;
4090
4214
  }
4091
4215
  for (const dir of component.directories ?? []) {
4092
- const srcDirPath = join7(assetsDir, dir);
4216
+ const srcDirPath = join8(assetsDir, dir);
4093
4217
  if (options.dryRun) {
4094
4218
  printDryRun(`Would copy directory: ${dir}/`);
4095
4219
  } else {
@@ -4118,7 +4242,7 @@ async function runInit(options, targetDir, assetsDir, packageVersion) {
4118
4242
  if (options.dryRun) {
4119
4243
  if (hasAgents) {
4120
4244
  const agentFiles = await import("fs").then(
4121
- (fsModule) => fsModule.promises.readdir(join7(assetsDir, "agents"))
4245
+ (fsModule) => fsModule.promises.readdir(join8(assetsDir, "agents"))
4122
4246
  );
4123
4247
  const yamlFiles = agentFiles.filter((f) => f.endsWith(".yaml")).sort();
4124
4248
  printDryRun("Would run agent setup (interactive):");
@@ -4357,7 +4481,7 @@ async function runInitUser(options, storeRoot, assetsDir, packageVersion) {
4357
4481
  console.log(chalk2.hex("#a855f7")(`
4358
4482
  \u2726 Arcane v${packageVersion}
4359
4483
  `));
4360
- const homeDir = dirname4(storeRoot);
4484
+ const homeDir = dirname5(storeRoot);
4361
4485
  const components = USER_TIER_COMPONENTS.map(
4362
4486
  (name) => componentForScope(getComponent(name), "user")
4363
4487
  );
@@ -4375,7 +4499,7 @@ async function runInitUser(options, storeRoot, assetsDir, packageVersion) {
4375
4499
  for (const id of spellIds) {
4376
4500
  for (const client2 of ["codex", "claude"]) {
4377
4501
  const relativePath = USER_FANOUT_PATHS[client2](id);
4378
- if (await fileExists(join7(homeDir, relativePath))) {
4502
+ if (await fileExists(join8(homeDir, relativePath))) {
4379
4503
  printDryRun(`Would leave alone (exists, not written by Arcane): ~/${relativePath}`);
4380
4504
  } else {
4381
4505
  printDryRun(`Would write: ~/${relativePath}`);
@@ -4393,7 +4517,7 @@ async function runInitUser(options, storeRoot, assetsDir, packageVersion) {
4393
4517
  const installedFiles = [];
4394
4518
  const fileHashes = {};
4395
4519
  for (const file of component.files) {
4396
- const srcPath = join7(assetsDir, component.sourceOverrides?.[file] ?? file);
4520
+ const srcPath = join8(assetsDir, component.sourceOverrides?.[file] ?? file);
4397
4521
  fileHashes[file] = await copyFile(srcPath, storeRoot, file, { force: options.force });
4398
4522
  installedFiles.push(file);
4399
4523
  fileCount++;
@@ -4440,23 +4564,23 @@ async function runInitUser(options, storeRoot, assetsDir, packageVersion) {
4440
4564
 
4441
4565
  // src/modules/component-install.ts
4442
4566
  import { readFile as readFile8, stat, writeFile as writeFile9 } from "fs/promises";
4443
- import { join as join8 } from "path";
4567
+ import { join as join9 } from "path";
4444
4568
  async function planComponentInstall(component, targetDir, assetsDir, opts = {}) {
4445
4569
  const files = [];
4446
4570
  const stateOf = async (file, src) => {
4447
- const dest = join8(targetDir, file);
4571
+ const dest = join9(targetDir, file);
4448
4572
  if (!await fileExists(dest)) return { file, src, state: "missing" };
4449
4573
  if ((await stat(dest)).isDirectory()) return { file, src, state: "directory" };
4450
4574
  const same = await hashFile(dest) === await hashFile(src);
4451
4575
  return { file, src, state: same ? "identical" : "differs" };
4452
4576
  };
4453
4577
  for (const file of component.files) {
4454
- files.push(await stateOf(file, join8(assetsDir, component.sourceOverrides?.[file] ?? file)));
4578
+ files.push(await stateOf(file, join9(assetsDir, component.sourceOverrides?.[file] ?? file)));
4455
4579
  }
4456
4580
  for (const dir of component.directories ?? []) {
4457
- const srcDir = join8(assetsDir, dir);
4581
+ const srcDir = join9(assetsDir, dir);
4458
4582
  for (const file of await listDirectoryFiles(srcDir, dir)) {
4459
- files.push(await stateOf(file, join8(assetsDir, file)));
4583
+ files.push(await stateOf(file, join9(assetsDir, file)));
4460
4584
  }
4461
4585
  }
4462
4586
  const of = (...states) => files.filter((f) => states.includes(f.state));
@@ -4468,14 +4592,14 @@ async function planComponentInstall(component, targetDir, assetsDir, opts = {})
4468
4592
  }
4469
4593
  return { component, files, toCopy: of("missing", "identical"), kept: [], conflicts: of("differs", "directory") };
4470
4594
  }
4471
- async function executeInstallPlan(plan, targetDir) {
4595
+ async function executeInstallPlan(plan, targetDir, opts = {}) {
4472
4596
  const files = [];
4473
4597
  const fileHashes = {};
4474
4598
  const written = [];
4475
4599
  try {
4476
4600
  for (const { file, src, state } of plan.toCopy) {
4477
- const previous = state === "missing" ? null : await readFile8(join8(targetDir, file));
4478
- fileHashes[file] = await copyFile(src, targetDir, file, { force: true });
4601
+ const previous = state === "missing" ? null : await readFile8(join9(targetDir, file));
4602
+ fileHashes[file] = await copyFile(src, targetDir, file, { force: true, ...opts });
4479
4603
  written.push({ file, previous });
4480
4604
  files.push(file);
4481
4605
  }
@@ -4492,7 +4616,7 @@ async function rollback(written, targetDir) {
4492
4616
  await removeWithin(targetDir, file);
4493
4617
  await removeEmptyAncestors(targetDir, file);
4494
4618
  } else {
4495
- await writeFile9(join8(targetDir, file), previous);
4619
+ await writeFile9(join9(targetDir, file), previous);
4496
4620
  }
4497
4621
  } catch {
4498
4622
  }
@@ -4628,8 +4752,8 @@ async function runAdd(names, options, targetDir, assetsDir, packageVersion) {
4628
4752
  }
4629
4753
 
4630
4754
  // src/commands/update.ts
4631
- import { dirname as dirname5, join as join10 } from "path";
4632
- import { readFile as readFile9, writeFile as writeFile10 } from "fs/promises";
4755
+ import { dirname as dirname6, join as join11 } from "path";
4756
+ import { readFile as readFile9 } from "fs/promises";
4633
4757
 
4634
4758
  // src/modules/merge3.ts
4635
4759
  var CONFLICT_START = "<<<<<<< yours (edited)";
@@ -4760,7 +4884,7 @@ function merge3(base, ours, theirs) {
4760
4884
  }
4761
4885
 
4762
4886
  // src/modules/registry-changes.ts
4763
- import { join as join9 } from "path";
4887
+ import { join as join10 } from "path";
4764
4888
  function findMissingRequires(installed, profile) {
4765
4889
  const names = new Set(installed.flatMap((c) => LEGACY_COMPONENT_MIGRATIONS[c.name] ?? [c.name]));
4766
4890
  const promised = listProfiles().find((p) => p.id === profile)?.components;
@@ -4806,7 +4930,7 @@ async function findNewlyAvailable(targetDir, profile, installed, spellScope) {
4806
4930
  const alreadyPresent = [];
4807
4931
  if (component.skipExisting) {
4808
4932
  for (const file of component.files) {
4809
- if (await fileExists(join9(targetDir, file))) alreadyPresent.push(file);
4933
+ if (await fileExists(join10(targetDir, file))) alreadyPresent.push(file);
4810
4934
  }
4811
4935
  if (component.files.length > 0 && alreadyPresent.length === component.files.length) continue;
4812
4936
  }
@@ -4922,11 +5046,11 @@ function describeInstallPlan(plan) {
4922
5046
  }
4923
5047
  return lines;
4924
5048
  }
4925
- async function executeUpdateInstalls(plan, targetDir, packageVersion) {
5049
+ async function executeUpdateInstalls(plan, targetDir, packageVersion, copyOpts = {}) {
4926
5050
  const installed = [];
4927
5051
  const lines = [];
4928
5052
  for (const item of plan.items) {
4929
- const { files, fileHashes } = await executeInstallPlan(item.plan, targetDir);
5053
+ const { files, fileHashes } = await executeInstallPlan(item.plan, targetDir, copyOpts);
4930
5054
  installed.push({
4931
5055
  name: item.plan.component.name,
4932
5056
  files,
@@ -4963,7 +5087,7 @@ async function missingTierWarning(scope, spellScope, homeDir) {
4963
5087
  } catch {
4964
5088
  return [
4965
5089
  "",
4966
- ` ! This repository takes its spells from the user tier, and ${storeRoot} is not installed.`,
5090
+ ` ! This repository takes its spells from the user tier, and ${storeRoot} is not installed (or its manifest cannot be read).`,
4967
5091
  " No client can reach a spell here: the routing block in CLAUDE.md and AGENTS.md still",
4968
5092
  " names spells that resolve to nothing, so an agent improvises the workflow instead of",
4969
5093
  " failing. Fix it with one of:",
@@ -5065,12 +5189,21 @@ async function resolveOrphan(targetDir, file, recordedHash, prune) {
5065
5189
  console.log(` ! Refused (path escapes the target directory): ${file}`);
5066
5190
  return "reported";
5067
5191
  }
5068
- const filePath = join10(targetDir, file);
5192
+ const filePath = join11(targetDir, file);
5069
5193
  if (!await fileExists(filePath)) return "not-found";
5070
5194
  if (!prune) return "reported";
5071
5195
  if (recordedHash === void 0) {
5072
- console.log(` ! Orphaned, not pruned (no recorded hash to verify it's untouched): ${file}`);
5073
- return "reported";
5196
+ const evidence = await isUneditedTrackedFile(targetDir, file);
5197
+ if (evidence.verdict !== "unedited") {
5198
+ console.log(
5199
+ ` ! Orphaned, not pruned: ${file} \u2014 no recorded hash, and git cannot confirm it is untouched (${evidence.detail}). Delete it yourself (git rm -- <path> if git tracks it), then run spell update --prune again to drop it from .arcane.json.`
5200
+ );
5201
+ return "reported";
5202
+ }
5203
+ await removeWithin(targetDir, file);
5204
+ await removeEmptyAncestors(targetDir, file);
5205
+ console.log(` Pruned orphaned file (no recorded hash; git shows it ${evidence.detail}): ${file}`);
5206
+ return "pruned";
5074
5207
  }
5075
5208
  if (!await fileMatchesHash(filePath, recordedHash)) {
5076
5209
  console.log(` ! Orphaned but edited since install \u2014 not pruning: ${file}`);
@@ -5087,7 +5220,7 @@ async function isReleasableUserOwnedFile(targetDir, file, recordedHash) {
5087
5220
  } catch {
5088
5221
  return false;
5089
5222
  }
5090
- const filePath = join10(targetDir, file);
5223
+ const filePath = join11(targetDir, file);
5091
5224
  if (!await fileExists(filePath)) return false;
5092
5225
  if (recordedHash === void 0) return true;
5093
5226
  return !await fileMatchesHash(filePath, recordedHash);
@@ -5098,7 +5231,7 @@ async function retainReportedOrphan(targetDir, file, recordedHash, into) {
5098
5231
  } catch {
5099
5232
  return;
5100
5233
  }
5101
- if (!await fileExists(join10(targetDir, file))) return;
5234
+ if (!await fileExists(join11(targetDir, file))) return;
5102
5235
  if (!into.files.includes(file)) into.files.push(file);
5103
5236
  if (recordedHash !== void 0) {
5104
5237
  into.fileHashes ??= {};
@@ -5127,7 +5260,7 @@ async function findMissingTrackedFiles(targetDir, components, scope = "repo", sp
5127
5260
  const candidates = readopted.has(installed.name) ? component.files : installed.files;
5128
5261
  for (const file of candidates) {
5129
5262
  if (!component.files.includes(file)) continue;
5130
- if (!await fileExists(join10(targetDir, file))) missing.push(file);
5263
+ if (!await fileExists(join11(targetDir, file))) missing.push(file);
5131
5264
  }
5132
5265
  }
5133
5266
  return missing;
@@ -5169,7 +5302,7 @@ async function runUpdate(options, targetDir, assetsDir, packageVersion) {
5169
5302
  const movedFilesPresent = /* @__PURE__ */ new Set();
5170
5303
  for (const move of COMPONENT_FILE_MOVES) {
5171
5304
  for (const file of move.files) {
5172
- if (await fileExists(join10(targetDir, file))) movedFilesPresent.add(file);
5305
+ if (await fileExists(join11(targetDir, file))) movedFilesPresent.add(file);
5173
5306
  }
5174
5307
  }
5175
5308
  manifest = {
@@ -5200,7 +5333,7 @@ async function runUpdate(options, targetDir, assetsDir, packageVersion) {
5200
5333
  return;
5201
5334
  }
5202
5335
  try {
5203
- flagPatch = resolveManifestFlags(options.manifestFlags, manifest, join10(targetDir, ".arcane.json"));
5336
+ flagPatch = resolveManifestFlags(options.manifestFlags, manifest, join11(targetDir, ".arcane.json"));
5204
5337
  } catch (err) {
5205
5338
  if (!(err instanceof ManifestFlagError)) throw err;
5206
5339
  console.error(`
@@ -5213,8 +5346,16 @@ async function runUpdate(options, targetDir, assetsDir, packageVersion) {
5213
5346
  const answered = { ...manifest, ...flagPatch };
5214
5347
  const spellScope = effectiveSpellScope(manifest);
5215
5348
  const readopted = findReadoptedSpellComponents(manifest.components, scope, spellScope);
5216
- const homeDir = dirname5(targetDir);
5217
- for (const line of await missingTierWarning(scope, spellScope)) console.log(line);
5349
+ const homeDir = dirname6(targetDir);
5350
+ const tierGate = await missingTierWarning(scope, spellScope);
5351
+ if (tierGate.length > 0) {
5352
+ for (const line of tierGate) console.error(line);
5353
+ console.error(
5354
+ " \u2717 Update refused: nothing was written. Install the tier or change spell_scope, then run `spell update` again."
5355
+ );
5356
+ process.exit(1);
5357
+ return;
5358
+ }
5218
5359
  if (scope === "repo") {
5219
5360
  console.warn(
5220
5361
  `WARNING: Arcane v${packageVersion} update safety notice: commit your work before updating.`
@@ -5245,6 +5386,8 @@ async function runUpdate(options, targetDir, assetsDir, packageVersion) {
5245
5386
  return;
5246
5387
  }
5247
5388
  }
5389
+ const linkTracked = scope === "repo" ? await listSymlinkTrackedPaths(targetDir) : /* @__PURE__ */ new Set();
5390
+ const conversions = [];
5248
5391
  const registryChanges = {
5249
5392
  retired: findTrackedRetirements(manifest.components),
5250
5393
  ...scope === "repo" ? {
@@ -5284,7 +5427,10 @@ async function runUpdate(options, targetDir, assetsDir, packageVersion) {
5284
5427
  for (const line of describeInstallPlan(installPlan)) console.log(line);
5285
5428
  return [];
5286
5429
  }
5287
- const { installed, lines } = await executeUpdateInstalls(installPlan, targetDir, packageVersion);
5430
+ const { installed, lines } = await executeUpdateInstalls(installPlan, targetDir, packageVersion, {
5431
+ linkTracked,
5432
+ conversions
5433
+ });
5288
5434
  for (const line of lines) console.log(line);
5289
5435
  return installed;
5290
5436
  };
@@ -5400,7 +5546,16 @@ async function runUpdate(options, targetDir, assetsDir, packageVersion) {
5400
5546
  if (options.prune && !options.dryRun && installed.files.every((f) => orphanReport.find((o) => o.file === f)?.status !== "reported")) {
5401
5547
  continue;
5402
5548
  }
5403
- updatedComponents.push(installed);
5549
+ const keptHashes = {};
5550
+ for (const file of stillTracked) {
5551
+ const hash = installed.fileHashes?.[file];
5552
+ if (hash !== void 0) keptHashes[file] = hash;
5553
+ }
5554
+ updatedComponents.push({
5555
+ ...installed,
5556
+ files: stillTracked,
5557
+ ...installed.fileHashes ? { fileHashes: keptHashes } : {}
5558
+ });
5404
5559
  continue;
5405
5560
  }
5406
5561
  throw err;
@@ -5408,8 +5563,8 @@ async function runUpdate(options, targetDir, assetsDir, packageVersion) {
5408
5563
  const updatedFiles = [];
5409
5564
  const fileHashes = {};
5410
5565
  for (const file of component.files) {
5411
- const srcPath = join10(assetsDir, component.sourceOverrides?.[file] ?? file);
5412
- const targetExists = await fileExists(join10(targetDir, file));
5566
+ const srcPath = join11(assetsDir, component.sourceOverrides?.[file] ?? file);
5567
+ const targetExists = await fileExists(join11(targetDir, file));
5413
5568
  const preserveExisting = Boolean(component.skipExisting) && targetExists;
5414
5569
  const recordedHash = installed.fileHashes?.[file];
5415
5570
  if (component.initOnly && !targetExists) {
@@ -5429,7 +5584,8 @@ async function runUpdate(options, targetDir, assetsDir, packageVersion) {
5429
5584
  continue;
5430
5585
  }
5431
5586
  }
5432
- const editedByOperator = recordedHash !== void 0 && targetExists && !preserveExisting && !await fileMatchesHash(join10(targetDir, file), recordedHash);
5587
+ const linkShape = targetExists ? await managedPathLinkShape(targetDir, file, linkTracked, isClientShimPath(file)) : null;
5588
+ const editedByOperator = recordedHash !== void 0 && targetExists && !preserveExisting && linkShape === null && !await fileMatchesHash(join11(targetDir, file), recordedHash);
5433
5589
  if (preserveExisting) {
5434
5590
  console.log(` ${options.dryRun ? "[dry-run] Would preserve" : "Preserved"}: ${file}`);
5435
5591
  } else if (editedByOperator && isClientShimPath(file)) {
@@ -5439,7 +5595,9 @@ async function runUpdate(options, targetDir, assetsDir, packageVersion) {
5439
5595
  ` ${options.dryRun ? "[dry-run] Would keep" : "Kept"} customized (not merged): ${file}`
5440
5596
  );
5441
5597
  } else if (options.dryRun) {
5442
- console.log(` [dry-run] Would ${sameVersionRestore ? "restore missing" : "update"}: ${file}`);
5598
+ console.log(
5599
+ ` [dry-run] Would ${linkShape !== null ? "convert to a regular file (it is a symlink, or a link written out as text)" : sameVersionRestore ? "restore missing" : "update"}: ${file}`
5600
+ );
5443
5601
  fileCount++;
5444
5602
  } else {
5445
5603
  let handled = false;
@@ -5456,11 +5614,11 @@ async function runUpdate(options, targetDir, assetsDir, packageVersion) {
5456
5614
  handled = true;
5457
5615
  } else {
5458
5616
  const [currentContent, newContent] = await Promise.all([
5459
- readFile9(join10(targetDir, file), "utf-8"),
5617
+ readFile9(join11(targetDir, file), "utf-8"),
5460
5618
  readFile9(srcPath, "utf-8")
5461
5619
  ]);
5462
5620
  const result = merge3(oldVendorContent, currentContent, newContent);
5463
- await writeFile10(join10(targetDir, file), result.content, "utf-8");
5621
+ await writeManagedFile(targetDir, file, result.content, { linkTracked, conversions });
5464
5622
  fileHashes[file] = await hashFile(srcPath);
5465
5623
  if (result.hasConflict) {
5466
5624
  conflictedFiles.push(file);
@@ -5472,7 +5630,8 @@ async function runUpdate(options, targetDir, assetsDir, packageVersion) {
5472
5630
  }
5473
5631
  }
5474
5632
  if (!handled) {
5475
- fileHashes[file] = await copyFile(srcPath, targetDir, file, { force: true });
5633
+ fileHashes[file] = await copyFile(srcPath, targetDir, file, { force: true, linkTracked, conversions });
5634
+ if (linkShape === "path-body") conversions.push({ file, shape: "path-body" });
5476
5635
  if (sameVersionRestore) console.log(` Restored missing: ${file}`);
5477
5636
  }
5478
5637
  fileCount++;
@@ -5482,11 +5641,11 @@ async function runUpdate(options, targetDir, assetsDir, packageVersion) {
5482
5641
  }
5483
5642
  }
5484
5643
  for (const dir of component.directories ?? []) {
5485
- const srcDirPath = join10(assetsDir, dir);
5644
+ const srcDirPath = join11(assetsDir, dir);
5486
5645
  if (options.dryRun) {
5487
5646
  console.log(` [dry-run] Would update directory: ${dir}/`);
5488
5647
  } else {
5489
- const copied = await copyDirectory(srcDirPath, targetDir, dir, { force: true });
5648
+ const copied = await copyDirectory(srcDirPath, targetDir, dir, { force: true, linkTracked, conversions });
5490
5649
  for (const { path: copiedPath, hash } of copied) {
5491
5650
  updatedFiles.push(copiedPath);
5492
5651
  fileHashes[copiedPath] = hash;
@@ -5550,6 +5709,21 @@ ${verb} ${orphansToReport.length} orphaned file(s) (tracked before this update,
5550
5709
  console.log(" Run `spell update --prune` to remove the ones that are safe to delete.");
5551
5710
  }
5552
5711
  }
5712
+ if (conversions.length > 0) {
5713
+ console.log(`
5714
+ Converted ${conversions.length} symlinked managed file(s) to regular files (#357):`);
5715
+ const describe = {
5716
+ "index-symlink": "the index tracked it as a symlink (mode 120000); rewritten and staged as a regular file",
5717
+ symlink: "was a symlink in the working tree; now a regular file (stage the typechange with git add)",
5718
+ "path-body": "its body was only a link target, a symlink written out as text; rewritten as a real file"
5719
+ };
5720
+ for (const c of conversions) console.log(` ${c.file} \u2014 ${describe[c.shape]}`);
5721
+ if (conversions.some((c) => c.shape === "index-symlink")) {
5722
+ console.log(
5723
+ " Commit the staged typechange, or every other checkout keeps reading a link whose target is the file's own text."
5724
+ );
5725
+ }
5726
+ }
5553
5727
  printRegistryChanges();
5554
5728
  if (customizedShims.length > 0) {
5555
5729
  const verb = options.dryRun ? "[dry-run] Would keep" : "Kept";
@@ -5642,11 +5816,11 @@ ${verb} ${orphansToReport.length} orphaned file(s) (tracked before this update,
5642
5816
  }
5643
5817
 
5644
5818
  // src/commands/status.ts
5645
- import { dirname as dirname7 } from "path";
5819
+ import { dirname as dirname8 } from "path";
5646
5820
 
5647
5821
  // src/modules/version-check.ts
5648
- import { readFile as readFile10, writeFile as writeFile11, mkdir as mkdir6 } from "fs/promises";
5649
- import { dirname as dirname6, join as join11 } from "path";
5822
+ import { readFile as readFile10, writeFile as writeFile10, mkdir as mkdir6 } from "fs/promises";
5823
+ import { dirname as dirname7, join as join12 } from "path";
5650
5824
  import { homedir as homedir3 } from "os";
5651
5825
  import { createRequire } from "module";
5652
5826
  var require2 = createRequire(import.meta.url);
@@ -5660,7 +5834,7 @@ function getFeedUrl() {
5660
5834
  }
5661
5835
  return url;
5662
5836
  }
5663
- var DEFAULT_CACHE_PATH = join11(homedir3(), ".arcane", "version-cache.json");
5837
+ var DEFAULT_CACHE_PATH = join12(homedir3(), ".arcane", "version-cache.json");
5664
5838
  var CACHE_TTL_MS = 24 * 60 * 60 * 1e3;
5665
5839
  async function readCache(cachePath) {
5666
5840
  try {
@@ -5675,9 +5849,9 @@ async function readCache(cachePath) {
5675
5849
  }
5676
5850
  }
5677
5851
  async function writeCache(cachePath, latest) {
5678
- await mkdir6(dirname6(cachePath), { recursive: true });
5852
+ await mkdir6(dirname7(cachePath), { recursive: true });
5679
5853
  const entry = { latest, checkedAt: (/* @__PURE__ */ new Date()).toISOString() };
5680
- await writeFile11(cachePath, JSON.stringify(entry, null, 2), "utf-8");
5854
+ await writeFile10(cachePath, JSON.stringify(entry, null, 2), "utf-8");
5681
5855
  }
5682
5856
  async function getLatestVersion(feedUrl) {
5683
5857
  const normalizedBase = feedUrl.endsWith("/") ? feedUrl : feedUrl + "/";
@@ -5816,7 +5990,7 @@ async function runStatus(targetDir, packageVersion, options = {}) {
5816
5990
  console.log("");
5817
5991
  if (scope === "user") {
5818
5992
  console.log(` Scope: user \u2014 ${targetDir}`);
5819
- const health = await inspectUserTierFanout(dirname7(targetDir), manifest.fanout);
5993
+ const health = await inspectUserTierFanout(dirname8(targetDir), manifest.fanout);
5820
5994
  console.log(
5821
5995
  ` Client files: ${health.total} (${health.byClient.codex} ${FANOUT_CLIENT_LABELS.codex}, ${health.byClient.claude} ${FANOUT_CLIENT_LABELS.claude})`
5822
5996
  );
@@ -5879,7 +6053,7 @@ async function runStatus(targetDir, packageVersion, options = {}) {
5879
6053
 
5880
6054
  // src/commands/uninstall.ts
5881
6055
  import { rm as rm5 } from "fs/promises";
5882
- import { dirname as dirname8, join as join12 } from "path";
6056
+ import { dirname as dirname9, join as join13 } from "path";
5883
6057
  import { confirm as confirm3 } from "@inquirer/prompts";
5884
6058
  async function runUninstall(options, targetDir) {
5885
6059
  let manifest;
@@ -5962,16 +6136,16 @@ Run \`spell unblock-push\` first (interactive), then uninstall.`
5962
6136
  }
5963
6137
  }
5964
6138
  }
5965
- await rm5(join12(targetDir, ".arcane.json"), { force: true });
6139
+ await rm5(join13(targetDir, ".arcane.json"), { force: true });
5966
6140
  const agentDirs = [
5967
6141
  ".github/agents",
5968
6142
  ".arcane/agents",
5969
6143
  ".arcane/generated"
5970
6144
  ];
5971
6145
  for (const dir of agentDirs) {
5972
- await rm5(join12(targetDir, dir), { recursive: true, force: true });
6146
+ await rm5(join13(targetDir, dir), { recursive: true, force: true });
5973
6147
  }
5974
- await rm5(join12(targetDir, ".arcane/agents.yaml"), { force: true });
6148
+ await rm5(join13(targetDir, ".arcane/agents.yaml"), { force: true });
5975
6149
  const markerFiles = [
5976
6150
  "CLAUDE.md",
5977
6151
  "AGENTS.md",
@@ -5988,7 +6162,7 @@ Run \`spell unblock-push\` first (interactive), then uninstall.`
5988
6162
  \u2713 Uninstalled \u2014 ${removed} files removed.`);
5989
6163
  }
5990
6164
  async function uninstallUserTier(options, storeRoot, manifest) {
5991
- const homeDir = dirname8(storeRoot);
6165
+ const homeDir = dirname9(storeRoot);
5992
6166
  const storeFiles = manifest.components.flatMap((c) => c.files);
5993
6167
  const recordedCount = Object.keys(manifest.fanout ?? {}).length;
5994
6168
  if (options.dryRun) {
@@ -6033,10 +6207,10 @@ async function uninstallUserTier(options, storeRoot, manifest) {
6033
6207
  console.log(` ! Skipped ${file}: ${err instanceof Error ? err.message : String(err)}`);
6034
6208
  }
6035
6209
  }
6036
- await rm5(join12(storeRoot, ".arcane.json"), { force: true });
6037
- await rm5(join12(storeRoot, "agents"), { recursive: true, force: true });
6038
- await rm5(join12(storeRoot, "agents.yaml"), { force: true });
6039
- await removeDirectoryIfEmpty(join12(storeRoot, USER_TIER_SPELLS_DIR));
6210
+ await rm5(join13(storeRoot, ".arcane.json"), { force: true });
6211
+ await rm5(join13(storeRoot, "agents"), { recursive: true, force: true });
6212
+ await rm5(join13(storeRoot, "agents.yaml"), { force: true });
6213
+ await removeDirectoryIfEmpty(join13(storeRoot, USER_TIER_SPELLS_DIR));
6040
6214
  await removeDirectoryIfEmpty(storeRoot);
6041
6215
  for (const line of describeFanoutOutcomes(fanout.outcomes)) console.log(` ${line}`);
6042
6216
  const pruned = fanout.outcomes.filter((o) => o.status === "pruned").length;
@@ -6049,7 +6223,7 @@ async function uninstallUserTier(options, storeRoot, manifest) {
6049
6223
 
6050
6224
  // src/commands/doctor.ts
6051
6225
  import { access as access4, readdir as readdir9, readFile as readFile13, mkdir as mkdir7, copyFile as fsCopyFile } from "fs/promises";
6052
- import { join as join15, dirname as dirname9 } from "path";
6226
+ import { join as join16, dirname as dirname10 } from "path";
6053
6227
 
6054
6228
  // src/config/incidents.ts
6055
6229
  var INCIDENT_QUEUE = [
@@ -6118,7 +6292,7 @@ function evaluateIncidentGate(records) {
6118
6292
 
6119
6293
  // src/modules/denylist-scan.ts
6120
6294
  import { readFile as readFile11, readdir as readdir7 } from "fs/promises";
6121
- import { join as join13, relative } from "path";
6295
+ import { join as join14, relative } from "path";
6122
6296
  function escapeRegExp(value) {
6123
6297
  return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
6124
6298
  }
@@ -6169,7 +6343,7 @@ async function collectScannableFiles(root, repoRoot = root, extensions = DEFAULT
6169
6343
  return collected;
6170
6344
  }
6171
6345
  for (const entry of entries) {
6172
- const absolutePath = join13(root, entry.name);
6346
+ const absolutePath = join14(root, entry.name);
6173
6347
  if (entry.isDirectory()) {
6174
6348
  if (SKIP_DIRECTORIES.has(entry.name)) continue;
6175
6349
  collected.push(...await collectScannableFiles(absolutePath, repoRoot, extensions));
@@ -6196,7 +6370,7 @@ import { existsSync, readFileSync } from "fs";
6196
6370
  import { win32 } from "path";
6197
6371
  var EXTERNAL_CLI_TIMEOUT_MS = 1e4;
6198
6372
  function execFileWithTimeout(file, args, timeoutMs, options = {}) {
6199
- return new Promise((resolve5, reject) => {
6373
+ return new Promise((resolve6, reject) => {
6200
6374
  const child = execFile2(
6201
6375
  file,
6202
6376
  args,
@@ -6212,7 +6386,7 @@ function execFileWithTimeout(file, args, timeoutMs, options = {}) {
6212
6386
  reject(error);
6213
6387
  return;
6214
6388
  }
6215
- resolve5({ stdout, stderr });
6389
+ resolve6({ stdout, stderr });
6216
6390
  }
6217
6391
  );
6218
6392
  child.stdin?.end();
@@ -6430,7 +6604,7 @@ function parseAdoRemote(url) {
6430
6604
 
6431
6605
  // src/modules/placeholders.ts
6432
6606
  import { readdir as readdir8, readFile as readFile12 } from "fs/promises";
6433
- import { join as join14 } from "path";
6607
+ import { join as join15 } from "path";
6434
6608
  var PLACEHOLDER_STANDARD_FILE = "spell-authoring-standards.md";
6435
6609
  var RUNTIME_PLACEHOLDERS_START = "<!-- runtime-placeholders:start -->";
6436
6610
  var RUNTIME_PLACEHOLDERS_END = "<!-- runtime-placeholders:end -->";
@@ -6525,7 +6699,7 @@ function parseRuntimePlaceholders(standardContent) {
6525
6699
  async function scanGovernancePlaceholders(governanceDir) {
6526
6700
  let standard;
6527
6701
  try {
6528
- standard = await readFile12(join14(governanceDir, PLACEHOLDER_STANDARD_FILE), "utf8");
6702
+ standard = await readFile12(join15(governanceDir, PLACEHOLDER_STANDARD_FILE), "utf8");
6529
6703
  } catch (err) {
6530
6704
  if (err.code === "ENOENT") {
6531
6705
  return { status: "skipped", reason: `${PLACEHOLDER_STANDARD_FILE} is not installed` };
@@ -6544,7 +6718,7 @@ async function scanGovernancePlaceholders(governanceDir) {
6544
6718
  const templateDocs = [];
6545
6719
  const findings = [];
6546
6720
  for (const file of files) {
6547
- const content = await readFile12(join14(governanceDir, file), "utf8");
6721
+ const content = await readFile12(join15(governanceDir, file), "utf8");
6548
6722
  const status = parseFrontmatterStatus(content);
6549
6723
  if (status === "template") templateDocs.push(file);
6550
6724
  if (status !== "active") continue;
@@ -6575,8 +6749,8 @@ async function checkNodeVersion() {
6575
6749
  async function checkVSCodeExtension(id, label2, homeDir = resolveHomeDir()) {
6576
6750
  const name = `VS Code extension: ${label2}`;
6577
6751
  const flavours = [
6578
- { dir: join15(homeDir, ".vscode", "extensions"), bin: "code" },
6579
- { dir: join15(homeDir, ".vscode-insiders", "extensions"), bin: "code-insiders" }
6752
+ { dir: join16(homeDir, ".vscode", "extensions"), bin: "code" },
6753
+ { dir: join16(homeDir, ".vscode-insiders", "extensions"), bin: "code-insiders" }
6580
6754
  ];
6581
6755
  let installBin = null;
6582
6756
  for (const flavour of flavours) {
@@ -6604,11 +6778,11 @@ async function checkVSCodeExtension(id, label2, homeDir = resolveHomeDir()) {
6604
6778
  }
6605
6779
  async function checkArcaneManifest(targetDir) {
6606
6780
  const name = "Arcane manifest (.arcane.json)";
6607
- const manifestPath2 = join15(targetDir, ".arcane.json");
6781
+ const manifestPath2 = join16(targetDir, ".arcane.json");
6608
6782
  try {
6609
6783
  await access4(manifestPath2);
6610
6784
  } catch {
6611
- const selfHostedManifestPath = join15(targetDir, "src", "assets", ".arcane.json");
6785
+ const selfHostedManifestPath = join16(targetDir, "src", "assets", ".arcane.json");
6612
6786
  try {
6613
6787
  const raw = await readFile13(selfHostedManifestPath, "utf8");
6614
6788
  const manifest = JSON.parse(raw);
@@ -6714,7 +6888,7 @@ async function checkSessionContinuity(targetDir) {
6714
6888
  const results = [];
6715
6889
  for (const file of SESSION_CONTINUITY_FILES) {
6716
6890
  const name = `Session file: ${file}`;
6717
- const filePath = join15(targetDir, file);
6891
+ const filePath = join16(targetDir, file);
6718
6892
  try {
6719
6893
  await access4(filePath);
6720
6894
  results.push({ name, passed: true, message: "present" });
@@ -6732,12 +6906,12 @@ async function checkSessionContinuity(targetDir) {
6732
6906
  async function fixSessionContinuity(targetDir, assetsDir) {
6733
6907
  const created = [];
6734
6908
  for (const file of SESSION_CONTINUITY_FILES) {
6735
- const destPath = join15(targetDir, file);
6909
+ const destPath = join16(targetDir, file);
6736
6910
  try {
6737
6911
  await access4(destPath);
6738
6912
  } catch {
6739
- const srcPath = join15(assetsDir, file);
6740
- await mkdir7(dirname9(destPath), { recursive: true });
6913
+ const srcPath = join16(assetsDir, file);
6914
+ await mkdir7(dirname10(destPath), { recursive: true });
6741
6915
  await fsCopyFile(srcPath, destPath);
6742
6916
  created.push(file);
6743
6917
  }
@@ -6864,7 +7038,7 @@ async function checkGovernancePlaceholders(targetDir) {
6864
7038
  const name = "Governance placeholders (ARC-051)";
6865
7039
  let scan;
6866
7040
  try {
6867
- scan = await scanGovernancePlaceholders(join15(targetDir, ".arcane", "governance"));
7041
+ scan = await scanGovernancePlaceholders(join16(targetDir, ".arcane", "governance"));
6868
7042
  } catch (err) {
6869
7043
  return {
6870
7044
  name,
@@ -6951,7 +7125,7 @@ async function checkNewlyAvailableComponents(targetDir) {
6951
7125
  }
6952
7126
  async function checkMcpConfig(targetDir) {
6953
7127
  const name = "MCP config (per-server timeout, I12)";
6954
- const filePath = join15(targetDir, ".mcp.json");
7128
+ const filePath = join16(targetDir, ".mcp.json");
6955
7129
  let raw;
6956
7130
  try {
6957
7131
  raw = await readFile13(filePath, "utf8");
@@ -7034,7 +7208,7 @@ function isDelegationsFile(value) {
7034
7208
  }
7035
7209
  async function checkDelegations(targetDir) {
7036
7210
  const name = "Standing delegations (T13)";
7037
- const filePath = join15(targetDir, ".arcane", "delegations.json");
7211
+ const filePath = join16(targetDir, ".arcane", "delegations.json");
7038
7212
  let raw;
7039
7213
  try {
7040
7214
  raw = await readFile13(filePath, "utf8");
@@ -7116,6 +7290,49 @@ async function checkPlatformBranchPolicy(targetDir) {
7116
7290
  message: "remote is neither GitHub nor Azure DevOps \u2014 platform policy verification not available for this provider"
7117
7291
  };
7118
7292
  }
7293
+ async function checkManagedFileModes(targetDir) {
7294
+ const name = "Managed files are regular files (#357)";
7295
+ let manifest;
7296
+ try {
7297
+ manifest = await readManifest(targetDir);
7298
+ } catch {
7299
+ return { name, passed: true, skipped: true, message: "no .arcane.json \u2014 nothing to check" };
7300
+ }
7301
+ try {
7302
+ await runGit(targetDir, ["rev-parse", "--git-dir"]);
7303
+ } catch {
7304
+ return { name, passed: true, skipped: true, message: "not a git repository \u2014 the index cannot be read" };
7305
+ }
7306
+ const managed = [...new Set(manifest.components.flatMap((c) => c.files))];
7307
+ const linkTracked = await listSymlinkTrackedPaths(targetDir);
7308
+ const asLinks = managed.filter((file) => linkTracked.has(file));
7309
+ const pathBodies = [];
7310
+ for (const file of managed) {
7311
+ if (!isClientShimPath(file) || asLinks.includes(file)) continue;
7312
+ let body;
7313
+ try {
7314
+ body = await readFile13(join16(targetDir, file), "utf-8");
7315
+ } catch {
7316
+ continue;
7317
+ }
7318
+ if (looksLikeBarePath(body)) pathBodies.push(file);
7319
+ }
7320
+ if (asLinks.length === 0 && pathBodies.length === 0) {
7321
+ return { name, passed: true, message: `${managed.length} managed path(s), all regular files` };
7322
+ }
7323
+ const parts = [];
7324
+ if (asLinks.length > 0) {
7325
+ parts.push(`${asLinks.length} tracked as a symlink (mode 120000): ${asLinks.join(", ")}`);
7326
+ }
7327
+ if (pathBodies.length > 0) {
7328
+ parts.push(`${pathBodies.length} client shim(s) whose body is only a path: ${pathBodies.join(", ")}`);
7329
+ }
7330
+ return {
7331
+ name,
7332
+ passed: false,
7333
+ message: parts.join("; ") + " \u2014 clients silently skip or empty these spells. Run `spell update` (it rewrites them as regular files and stages the typechange), then commit."
7334
+ };
7335
+ }
7119
7336
  async function checkSpellScope(targetDir, homeDir = resolveHomeDir()) {
7120
7337
  const name = "Spell scope (ARC-045)";
7121
7338
  let manifest;
@@ -7147,7 +7364,7 @@ async function checkSpellScope(targetDir, homeDir = resolveHomeDir()) {
7147
7364
  let probed = false;
7148
7365
  const probe = "spell-status.md";
7149
7366
  try {
7150
- const entries = await readdir9(join15(storeRoot, USER_TIER_SPELLS_DIR));
7367
+ const entries = await readdir9(join16(storeRoot, USER_TIER_SPELLS_DIR));
7151
7368
  const spells = entries.filter((e) => e.startsWith("spell-") && e.endsWith(".md"));
7152
7369
  spellCount = spells.length;
7153
7370
  probed = spells.includes(probe);
@@ -7211,6 +7428,7 @@ async function runDoctor(targetDir, options = {}, assetsDir) {
7211
7428
  checkMcpConfig(targetDir),
7212
7429
  checkUserTier(resolveHomeDir(), options.packageVersion),
7213
7430
  checkSpellScope(targetDir),
7431
+ checkManagedFileModes(targetDir),
7214
7432
  checkGovernancePlaceholders(targetDir),
7215
7433
  checkComponentRequires(targetDir),
7216
7434
  checkNewlyAvailableComponents(targetDir)
@@ -7261,7 +7479,7 @@ import { readFile as readFile15 } from "fs/promises";
7261
7479
 
7262
7480
  // src/modules/ward.ts
7263
7481
  import { readdir as readdir10, readFile as readFile14 } from "fs/promises";
7264
- import { join as join16, relative as relative2 } from "path";
7482
+ import { join as join17, relative as relative2 } from "path";
7265
7483
  async function scanFileAllRules(absolutePath, displayPath, rules) {
7266
7484
  const findings = [];
7267
7485
  let content;
@@ -7324,7 +7542,7 @@ async function scanFilenames(root, rules, repoRoot = root) {
7324
7542
  return findings;
7325
7543
  }
7326
7544
  for (const entry of entries) {
7327
- const absolutePath = join16(root, entry.name);
7545
+ const absolutePath = join17(root, entry.name);
7328
7546
  if (entry.isDirectory()) {
7329
7547
  if (SKIP_DIRECTORIES.has(entry.name)) continue;
7330
7548
  findings.push(...await scanFilenames(absolutePath, rules, repoRoot));
@@ -7439,7 +7657,7 @@ async function runWardCli(targetDir, options = {}, exclusions = []) {
7439
7657
  }
7440
7658
 
7441
7659
  // src/commands/unblock-push.ts
7442
- import { basename as basename2, resolve as resolve3 } from "path";
7660
+ import { basename as basename3, resolve as resolve4 } from "path";
7443
7661
  import { input as input4 } from "@inquirer/prompts";
7444
7662
  async function runUnblockPush(targetDir) {
7445
7663
  let manifest;
@@ -7471,7 +7689,7 @@ async function runUnblockPush(targetDir) {
7471
7689
  process.exit(1);
7472
7690
  return;
7473
7691
  }
7474
- const repoName = basename2(resolve3(targetDir));
7692
+ const repoName = basename3(resolve4(targetDir));
7475
7693
  if (repoName === "") {
7476
7694
  console.error(
7477
7695
  "Could not determine a repository name to confirm against (is this repository at a filesystem root?). Refusing to unblock."
@@ -7760,10 +7978,10 @@ import * as client from "openid-client";
7760
7978
  // src/modules/result-pages.ts
7761
7979
  import { createHash as createHash3 } from "crypto";
7762
7980
  import { readFile as readFile16 } from "fs/promises";
7763
- import { join as join17 } from "path";
7981
+ import { join as join18 } from "path";
7764
7982
  var RESULT_KINDS = ["success", "denied", "failure"];
7765
7983
  function resultPagePath(kind) {
7766
- return join17("sign-in", `sign-in-callback-${kind}.html`);
7984
+ return join18("sign-in", `sign-in-callback-${kind}.html`);
7767
7985
  }
7768
7986
  function styleHashes(html) {
7769
7987
  return [...html.matchAll(/<style\b[^>]*>([\s\S]*?)<\/style\s*>/gi)].map((match) => {
@@ -7811,7 +8029,7 @@ var PLAIN_RESULT_PAGES = {
7811
8029
  async function loadResultPages(assetsDir) {
7812
8030
  try {
7813
8031
  const [success, denied, failure] = await Promise.all(
7814
- RESULT_KINDS.map(async (kind) => resultPage(await readFile16(join17(assetsDir, resultPagePath(kind)), "utf8")))
8032
+ RESULT_KINDS.map(async (kind) => resultPage(await readFile16(join18(assetsDir, resultPagePath(kind)), "utf8")))
7815
8033
  );
7816
8034
  return { success, denied, failure };
7817
8035
  } catch {
@@ -7889,9 +8107,9 @@ async function browserLogin(config, options) {
7889
8107
  const state = client.randomState();
7890
8108
  const nonce = client.randomNonce();
7891
8109
  const server = createServer();
7892
- await new Promise((resolve5, reject) => {
8110
+ await new Promise((resolve6, reject) => {
7893
8111
  server.once("error", reject);
7894
- server.listen(0, "127.0.0.1", () => resolve5());
8112
+ server.listen(0, "127.0.0.1", () => resolve6());
7895
8113
  });
7896
8114
  const address = server.address();
7897
8115
  if (address === null || typeof address === "string") {
@@ -7917,7 +8135,7 @@ async function browserLogin(config, options) {
7917
8135
  server.closeAllConnections?.();
7918
8136
  server.close();
7919
8137
  };
7920
- const tokens = new Promise((resolve5, reject) => {
8138
+ const tokens = new Promise((resolve6, reject) => {
7921
8139
  server.on("request", (req, res) => {
7922
8140
  const url = new URL(req.url ?? "/", redirectUri);
7923
8141
  if (RESULT_PATHS.has(url.pathname)) {
@@ -7928,7 +8146,7 @@ async function browserLogin(config, options) {
7928
8146
  const served = browserResult;
7929
8147
  const page = pages[served.kind];
7930
8148
  const settle = () => {
7931
- if (served.kind === "success") resolve5(served.result);
8149
+ if (served.kind === "success") resolve6(served.result);
7932
8150
  else reject(served.error);
7933
8151
  };
7934
8152
  res.once("finish", settle).once("close", settle);
@@ -7957,7 +8175,7 @@ async function browserLogin(config, options) {
7957
8175
  }).end();
7958
8176
  if (result.kind === "success") {
7959
8177
  if (timer) clearTimeout(timer);
7960
- timer = setTimeout(() => resolve5(result.result), Math.min(timeoutMs, 5e3));
8178
+ timer = setTimeout(() => resolve6(result.result), Math.min(timeoutMs, 5e3));
7961
8179
  }
7962
8180
  };
7963
8181
  const errorCode = url.searchParams.get("error");
@@ -8009,12 +8227,12 @@ async function refreshSession(config, refreshToken, scopes) {
8009
8227
  }
8010
8228
  function openInDefaultBrowser(url) {
8011
8229
  const [command, args] = process.platform === "win32" ? ["rundll32", ["url.dll,FileProtocolHandler", url]] : process.platform === "darwin" ? ["open", [url]] : ["xdg-open", [url]];
8012
- return new Promise((resolve5) => {
8230
+ return new Promise((resolve6) => {
8013
8231
  try {
8014
- const child = execFile3(command, args, { windowsHide: true }, (error) => resolve5(!error));
8015
- child.on("error", () => resolve5(false));
8232
+ const child = execFile3(command, args, { windowsHide: true }, (error) => resolve6(!error));
8233
+ child.on("error", () => resolve6(false));
8016
8234
  } catch {
8017
- resolve5(false);
8235
+ resolve6(false);
8018
8236
  }
8019
8237
  });
8020
8238
  }
@@ -8023,7 +8241,7 @@ function openInDefaultBrowser(url) {
8023
8241
  import { execFileSync } from "child_process";
8024
8242
  import { mkdirSync, readFileSync as readFileSync2, renameSync, rmSync, writeFileSync, existsSync as existsSync2 } from "fs";
8025
8243
  import { userInfo } from "os";
8026
- import { dirname as dirname10, join as join18 } from "path";
8244
+ import { dirname as dirname11, join as join19 } from "path";
8027
8245
 
8028
8246
  // src/modules/keychain-backend.ts
8029
8247
  import { createRequire as createRequire2 } from "module";
@@ -8112,7 +8330,7 @@ var FileSecretBackend = class {
8112
8330
  rmSync(this.filePath, { force: true });
8113
8331
  return;
8114
8332
  }
8115
- mkdirSync(dirname10(this.filePath), { recursive: true });
8333
+ mkdirSync(dirname11(this.filePath), { recursive: true });
8116
8334
  const tmp = `${this.filePath}.${process.pid}.tmp`;
8117
8335
  writeFileSync(tmp, JSON.stringify(entries), { encoding: "utf8", mode: 384 });
8118
8336
  try {
@@ -8157,7 +8375,7 @@ function restrictToCurrentUser(filePath) {
8157
8375
  });
8158
8376
  }
8159
8377
  function insecureSessionFilePath(homeDir) {
8160
- return join18(userTierRoot(homeDir), INSECURE_SESSION_FILE_NAME);
8378
+ return join19(userTierRoot(homeDir), INSECURE_SESSION_FILE_NAME);
8161
8379
  }
8162
8380
  function createSessionStorage(options = {}) {
8163
8381
  const keychain = new ChunkedSecretStore(options.keychainBackend ?? createKeychainBackend());
@@ -8425,15 +8643,15 @@ async function runWhoami(options, deps = defaultWhoamiDeps()) {
8425
8643
 
8426
8644
  // src/commands/report.ts
8427
8645
  import { access as access5 } from "fs/promises";
8428
- import { join as join21 } from "path";
8646
+ import { join as join22 } from "path";
8429
8647
 
8430
8648
  // src/modules/show-report/generate.ts
8431
- import { mkdir as mkdir8, readdir as readdir11, readFile as readFile18, writeFile as writeFile12 } from "fs/promises";
8432
- import { basename as basename3, dirname as dirname11, isAbsolute, join as join20, relative as relative3, resolve as resolve4 } from "path";
8649
+ import { mkdir as mkdir8, readdir as readdir11, readFile as readFile18, writeFile as writeFile11 } from "fs/promises";
8650
+ import { basename as basename4, dirname as dirname12, isAbsolute, join as join21, relative as relative3, resolve as resolve5 } from "path";
8433
8651
 
8434
8652
  // src/modules/show-report/model.ts
8435
8653
  import { readFile as readFile17 } from "fs/promises";
8436
- import { join as join19 } from "path";
8654
+ import { join as join20 } from "path";
8437
8655
 
8438
8656
  // src/modules/show-report/plan-parser.ts
8439
8657
  function parseFrontmatter(content) {
@@ -8802,11 +9020,11 @@ function buildCorrections(ledgerRows, programName) {
8802
9020
  };
8803
9021
  }
8804
9022
  async function buildShowReportModel(options) {
8805
- const planAbsPath = join19(options.rootDir, options.planRelPath);
8806
- const queueAbsPath = join19(options.rootDir, options.queueRelPath);
9023
+ const planAbsPath = join20(options.rootDir, options.planRelPath);
9024
+ const queueAbsPath = join20(options.rootDir, options.queueRelPath);
8807
9025
  const ledgerRelPath = "docs/verification-ledger.md";
8808
- const ledgerAbsPath = join19(options.rootDir, ledgerRelPath);
8809
- const decisionsAbsPath = join19(options.rootDir, "DECISIONS.md");
9026
+ const ledgerAbsPath = join20(options.rootDir, ledgerRelPath);
9027
+ const decisionsAbsPath = join20(options.rootDir, "DECISIONS.md");
8810
9028
  const [planContent, queueContent, ledgerContent, decisionsContent] = await Promise.all([
8811
9029
  readFile17(planAbsPath, "utf8"),
8812
9030
  readOptional(queueAbsPath),
@@ -9004,7 +9222,7 @@ function toPosix(path4) {
9004
9222
  async function programFromPlan(rootDir, slug, planRelPath) {
9005
9223
  let planContent;
9006
9224
  try {
9007
- planContent = await readFile18(join20(rootDir, planRelPath), "utf8");
9225
+ planContent = await readFile18(join21(rootDir, planRelPath), "utf8");
9008
9226
  } catch {
9009
9227
  return { skipped: { slug, reason: `no PLAN.md at ${planRelPath}` } };
9010
9228
  }
@@ -9016,13 +9234,13 @@ async function programFromPlan(rootDir, slug, planRelPath) {
9016
9234
  program: {
9017
9235
  slug,
9018
9236
  planRelPath,
9019
- queueRelPath: toPosix(join20(dirname11(planRelPath), "OPERATOR-QUEUE.md")),
9237
+ queueRelPath: toPosix(join21(dirname12(planRelPath), "OPERATOR-QUEUE.md")),
9020
9238
  ledgerProgramName: programNameFromTitle(frontmatter.title ?? slug)
9021
9239
  }
9022
9240
  };
9023
9241
  }
9024
9242
  async function discoverPrograms(rootDir) {
9025
- const plansDir = join20(rootDir, "docs", "plans");
9243
+ const plansDir = join21(rootDir, "docs", "plans");
9026
9244
  let entries;
9027
9245
  try {
9028
9246
  entries = (await readdir11(plansDir, { withFileTypes: true })).filter((e) => e.isDirectory());
@@ -9040,9 +9258,9 @@ async function discoverPrograms(rootDir) {
9040
9258
  return { programs, skipped };
9041
9259
  }
9042
9260
  async function programFromPlanPath(rootDir, planPath) {
9043
- const planAbs = isAbsolute(planPath) ? planPath : resolve4(rootDir, planPath);
9261
+ const planAbs = isAbsolute(planPath) ? planPath : resolve5(rootDir, planPath);
9044
9262
  const planRelPath = toPosix(relative3(rootDir, planAbs));
9045
- return programFromPlan(rootDir, basename3(dirname11(planAbs)), planRelPath);
9263
+ return programFromPlan(rootDir, basename4(dirname12(planAbs)), planRelPath);
9046
9264
  }
9047
9265
  function normalizeLineEndings2(content) {
9048
9266
  return content.replace(/\r\n?/g, "\n");
@@ -9056,7 +9274,7 @@ async function readOptionalFile(path4) {
9056
9274
  }
9057
9275
  }
9058
9276
  async function generateReport(rootDir, program2, templateHtml, theme = "auto") {
9059
- const planContent = await readFile18(join20(rootDir, program2.planRelPath), "utf8");
9277
+ const planContent = await readFile18(join21(rootDir, program2.planRelPath), "utf8");
9060
9278
  const frontmatter = parseFrontmatter(planContent);
9061
9279
  const model = await buildShowReportModel({
9062
9280
  rootDir,
@@ -9087,19 +9305,19 @@ async function runReportGeneration(options) {
9087
9305
  if (!generated.themeApplied) themeSkipped.push(program2.slug);
9088
9306
  const missing = JSON.parse(generated.json).sections.flatMap((section) => section.rows).filter((row) => row.descriptionState === "unwritten").map((row) => row.id);
9089
9307
  if (missing.length > 0) unwritten.push({ slug: program2.slug, ids: missing });
9090
- const outAbs = options.outDir ? isAbsolute(options.outDir) ? options.outDir : resolve4(options.rootDir, options.outDir) : resolve4(options.rootDir, dirname11(program2.planRelPath));
9308
+ const outAbs = options.outDir ? isAbsolute(options.outDir) ? options.outDir : resolve5(options.rootDir, options.outDir) : resolve5(options.rootDir, dirname12(program2.planRelPath));
9091
9309
  const outRel = toPosix(relative3(options.rootDir, outAbs));
9092
9310
  const outputs = [
9093
9311
  [`${outRel}/show-report.json`, generated.json],
9094
9312
  [`${outRel}/show-report.html`, generated.html]
9095
9313
  ];
9096
9314
  for (const [relPath, expected] of outputs) {
9097
- const actual = await readOptionalFile(join20(options.rootDir, relPath));
9315
+ const actual = await readOptionalFile(join21(options.rootDir, relPath));
9098
9316
  if (actual === null || normalizeLineEndings2(actual) !== normalizeLineEndings2(expected)) {
9099
9317
  drifted.push(`${relPath} does not match a regeneration from its sources.`);
9100
9318
  if (options.mode === "write") {
9101
9319
  await mkdir8(outAbs, { recursive: true });
9102
- await writeFile12(join20(options.rootDir, relPath), expected, "utf8");
9320
+ await writeFile11(join21(options.rootDir, relPath), expected, "utf8");
9103
9321
  repaired.push(relPath);
9104
9322
  }
9105
9323
  }
@@ -9110,7 +9328,7 @@ async function runReportGeneration(options) {
9110
9328
 
9111
9329
  // src/commands/report.ts
9112
9330
  async function runReport(targetDir, options = {}, assetsDir) {
9113
- const templatePath = join21(assetsDir, "report", "show-report.template.html");
9331
+ const templatePath = join22(assetsDir, "report", "show-report.template.html");
9114
9332
  try {
9115
9333
  await access5(templatePath);
9116
9334
  } catch {
@@ -9184,10 +9402,10 @@ async function runReport(targetDir, options = {}, assetsDir) {
9184
9402
  }
9185
9403
 
9186
9404
  // src/index.ts
9187
- import { realpathSync } from "fs";
9405
+ import { realpathSync as realpathSync2 } from "fs";
9188
9406
  var require3 = createRequire3(import.meta.url);
9189
9407
  var pkg = require3("../package.json");
9190
- var ASSETS_DIR = process.env["ARCANE_ASSETS_DIR"] ?? join22(dirname12(fileURLToPath(import.meta.url)), "assets");
9408
+ var ASSETS_DIR = process.env["ARCANE_ASSETS_DIR"] ?? join23(dirname13(fileURLToPath(import.meta.url)), "assets");
9191
9409
  var program = new Command();
9192
9410
  var USER_FLAG_DESCRIPTION = "Operate on the per-user tier at ~/.arcane \u2014 spells shared by every repository on this machine, fanned out to ~/.agents/skills (Codex, Copilot) and ~/.claude/commands (Claude Code)";
9193
9411
  var USER_TIER_AGENTS_DESCRIPTION = "Operate on the per-user tier at ~/.arcane \u2014 one agent roster shared by every repository on this machine, rendered to ~/.copilot/agents (VS Code agent modes)";
@@ -9397,7 +9615,7 @@ agentsCommand.command("list").description("Show the current agent roster with as
9397
9615
  const scope = opts.user ? "user" : "repo";
9398
9616
  await runAgentsList(agentsTargetDir(scope, process.cwd()), scope);
9399
9617
  });
9400
- if (realpathSync(process.argv[1]) === fileURLToPath(import.meta.url)) {
9618
+ if (realpathSync2(process.argv[1]) === fileURLToPath(import.meta.url)) {
9401
9619
  await program.parseAsync(process.argv);
9402
9620
  }
9403
9621
  export {