arcane-cli 1.2.0 → 1.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -6,11 +6,11 @@ const require = __createRequire(import.meta.url);
6
6
  import { Command } from "commander";
7
7
  import { createRequire as createRequire2 } from "module";
8
8
  import { fileURLToPath } from "url";
9
- import { dirname as dirname10, join as join18 } from "path";
9
+ import { dirname as dirname11, join as join18 } from "path";
10
10
 
11
11
  // src/commands/init.ts
12
12
  import { select as select2, confirm, input as input2 } from "@inquirer/prompts";
13
- import { dirname as dirname3, join as join6 } from "path";
13
+ import { dirname as dirname4, join as join6 } from "path";
14
14
  import chalk2 from "chalk";
15
15
 
16
16
  // src/modules/copier.ts
@@ -1035,18 +1035,25 @@ function spellIdsInStore(components) {
1035
1035
  }
1036
1036
  return [...ids].sort();
1037
1037
  }
1038
+ var USER_AGENT_FANOUT_DIR = ".copilot/agents";
1039
+ var SPELL_FANOUT_CLIENTS = ["codex", "claude"];
1038
1040
  var USER_FANOUT_PATHS = {
1039
1041
  codex: (id) => `.agents/skills/${id}/SKILL.md`,
1040
1042
  claude: (id) => `.claude/commands/${id}.md`
1041
1043
  };
1044
+ function userAgentFanoutPath(slug) {
1045
+ return `${USER_AGENT_FANOUT_DIR}/${slug}.agent.md`;
1046
+ }
1042
1047
  var FANOUT_CLIENT_LABELS = {
1043
1048
  codex: "Codex/Copilot skills",
1044
- claude: "Claude Code commands"
1049
+ claude: "Claude Code commands",
1050
+ "copilot-agents": "Copilot agent modes"
1045
1051
  };
1046
1052
  function clientOfFanoutPath(relativePath) {
1047
1053
  const normalized = relativePath.replace(/\\/g, "/");
1048
1054
  if (normalized.startsWith(".agents/skills/")) return "codex";
1049
1055
  if (normalized.startsWith(".claude/commands/")) return "claude";
1056
+ if (normalized.startsWith(`${USER_AGENT_FANOUT_DIR}/`)) return "copilot-agents";
1050
1057
  return void 0;
1051
1058
  }
1052
1059
  async function readSpellSource(storeRoot, id, fallbackDir) {
@@ -1108,14 +1115,16 @@ async function removeDirectoryIfEmpty(dir) {
1108
1115
  }
1109
1116
  async function syncUserTierFanout(options) {
1110
1117
  const rendered = await renderUserFanout(options.storeRoot, options.spellIds, options.fallbackDir);
1111
- const desired = rendered.files;
1118
+ const desired = [...rendered.files, ...options.extraFiles ?? []];
1119
+ const owned = options.ownedClients;
1120
+ const ownsClient = (client) => owned === void 0 || client !== void 0 && owned.includes(client);
1112
1121
  const previous = options.previous ?? {};
1113
1122
  const dryRun = Boolean(options.dryRun);
1114
1123
  const record = {};
1115
1124
  const outcomes = [];
1116
1125
  const desiredPaths = new Set(desired.map((f) => f.relativePath));
1117
1126
  for (const id of rendered.missing) {
1118
- for (const client of ["codex", "claude"]) {
1127
+ for (const client of SPELL_FANOUT_CLIENTS) {
1119
1128
  const relativePath = USER_FANOUT_PATHS[client](id);
1120
1129
  desiredPaths.add(relativePath);
1121
1130
  if (previous[relativePath] !== void 0) record[relativePath] = previous[relativePath];
@@ -1158,6 +1167,10 @@ async function syncUserTierFanout(options) {
1158
1167
  }
1159
1168
  for (const [relativePath, recorded] of Object.entries(previous)) {
1160
1169
  if (desiredPaths.has(relativePath)) continue;
1170
+ if (!ownsClient(clientOfFanoutPath(relativePath))) {
1171
+ record[relativePath] = recorded;
1172
+ continue;
1173
+ }
1161
1174
  validateTargetPath(options.homeDir, relativePath);
1162
1175
  const target = join(options.homeDir, relativePath);
1163
1176
  const state = await classifyTarget(target);
@@ -1198,7 +1211,7 @@ function describeFanoutOutcomes(outcomes, dryRun = false) {
1198
1211
  const lines = [];
1199
1212
  const prefix = dryRun ? "[dry-run] Would " : "";
1200
1213
  const byClient = (status) => {
1201
- const n = { codex: 0, claude: 0 };
1214
+ const n = { codex: 0, claude: 0, "copilot-agents": 0 };
1202
1215
  for (const o of outcomes) {
1203
1216
  if (o.status !== status) continue;
1204
1217
  const client = clientOfFanoutPath(o.relativePath);
@@ -1209,8 +1222,9 @@ function describeFanoutOutcomes(outcomes, dryRun = false) {
1209
1222
  const paths = (status) => outcomes.filter((o) => o.status === status).map((o) => ` ~/${o.relativePath}`).join("\n");
1210
1223
  if (counts.written > 0) {
1211
1224
  const n = byClient("written");
1225
+ const breakdown = Object.keys(n).filter((client) => n[client] > 0).map((client) => `${n[client]} ${FANOUT_CLIENT_LABELS[client]}`).join(", ");
1212
1226
  lines.push(
1213
- `${prefix}${dryRun ? "write" : "Wrote"} ${counts.written} client file(s): ${n.codex} ${FANOUT_CLIENT_LABELS.codex}, ${n.claude} ${FANOUT_CLIENT_LABELS.claude}.`
1227
+ `${prefix}${dryRun ? "write" : "Wrote"} ${counts.written} client file(s): ${breakdown}.`
1214
1228
  );
1215
1229
  }
1216
1230
  if (counts.pruned > 0) {
@@ -1248,7 +1262,7 @@ ${paths("kept-edited")}`
1248
1262
  async function inspectUserTierFanout(homeDir, record) {
1249
1263
  const health = {
1250
1264
  total: 0,
1251
- byClient: { codex: 0, claude: 0 },
1265
+ byClient: { codex: 0, claude: 0, "copilot-agents": 0 },
1252
1266
  missing: [],
1253
1267
  customized: []
1254
1268
  };
@@ -1425,7 +1439,7 @@ function printWarning(message) {
1425
1439
  // src/modules/agents.ts
1426
1440
  import { select, checkbox, Separator } from "@inquirer/prompts";
1427
1441
  import { mkdir as mkdir4, writeFile as writeFile6, copyFile as copyFile2 } from "fs/promises";
1428
- import { join as join4 } from "path";
1442
+ import { dirname as dirname2, join as join4 } from "path";
1429
1443
  import { stringify } from "yaml";
1430
1444
 
1431
1445
  // src/config/agent-roles.ts
@@ -1834,9 +1848,15 @@ var AgentDefinitionNotFoundError = class extends Error {
1834
1848
  }
1835
1849
  };
1836
1850
  var AgentRosterNotFoundError = class extends Error {
1837
- constructor() {
1851
+ /**
1852
+ * Names the tier the caller actually asked for. A `--user` run that reports
1853
+ * the repository's path sends the operator to the wrong file and the wrong
1854
+ * command, which is worse than no message.
1855
+ */
1856
+ constructor(scope = "repo") {
1857
+ const user = scope === "user";
1838
1858
  super(
1839
- 'No agent roster found at .arcane/agents.yaml. Run "spell agents init" to create one.'
1859
+ `No agent roster found at ${user ? "~/.arcane/agents.yaml" : ".arcane/agents.yaml"}. Run "spell agents init${user ? " --user" : ""}" to create one.`
1840
1860
  );
1841
1861
  this.name = "AgentRosterNotFoundError";
1842
1862
  }
@@ -1859,30 +1879,33 @@ async function loadAgentDefinition(agentsDir, id) {
1859
1879
  }
1860
1880
  return validateAgentDefinition(parseYaml2(content, filePath), filePath);
1861
1881
  }
1862
- async function loadRoster(targetDir) {
1863
- const rosterPath = join2(targetDir, ".arcane", "agents.yaml");
1882
+ function agentsBaseDir(targetDir, scope = "repo") {
1883
+ return scope === "user" ? targetDir : join2(targetDir, ".arcane");
1884
+ }
1885
+ async function loadRoster(targetDir, scope = "repo") {
1886
+ const rosterPath = join2(agentsBaseDir(targetDir, scope), "agents.yaml");
1864
1887
  let content;
1865
1888
  try {
1866
1889
  content = await readFile4(rosterPath, "utf8");
1867
1890
  } catch {
1868
- throw new AgentRosterNotFoundError();
1891
+ throw new AgentRosterNotFoundError(scope);
1869
1892
  }
1870
1893
  return validateAgentRoster(parseYaml2(content, rosterPath), rosterPath);
1871
1894
  }
1872
- async function rosterExists(targetDir) {
1895
+ async function rosterExists(targetDir, scope = "repo") {
1873
1896
  try {
1874
- await access2(join2(targetDir, ".arcane", "agents.yaml"));
1897
+ await access2(join2(agentsBaseDir(targetDir, scope), "agents.yaml"));
1875
1898
  return true;
1876
1899
  } catch {
1877
1900
  return false;
1878
1901
  }
1879
1902
  }
1880
- function projectAgentsDir(targetDir) {
1881
- return join2(targetDir, ".arcane", "agents");
1903
+ function projectAgentsDir(targetDir, scope = "repo") {
1904
+ return join2(agentsBaseDir(targetDir, scope), "agents");
1882
1905
  }
1883
1906
 
1884
1907
  // src/modules/agent-generator.ts
1885
- import { writeFile as writeFile5, mkdir as mkdir3 } from "fs/promises";
1908
+ import { writeFile as writeFile5, mkdir as mkdir3, readdir as readdir4 } from "fs/promises";
1886
1909
  import { join as join3 } from "path";
1887
1910
  import { homedir as homedir2 } from "os";
1888
1911
 
@@ -2091,11 +2114,23 @@ function renderAgentRosterSection(entries) {
2091
2114
  ${rows}
2092
2115
  `;
2093
2116
  }
2117
+ async function existingAgentFiles(agentsDir) {
2118
+ try {
2119
+ const entries = await readdir4(agentsDir);
2120
+ return entries.filter((name) => name.endsWith(".agent.md")).sort().map((name) => `.github/agents/${name}`);
2121
+ } catch {
2122
+ return [];
2123
+ }
2124
+ }
2094
2125
  async function syncAgents(targetDir, assetsDir, roster, options = {}) {
2095
2126
  const synced = [];
2096
2127
  const skipped = [];
2097
2128
  let hasUnresolvedRoles = false;
2098
- const projectDefs = projectAgentsDir(targetDir);
2129
+ const userTier = options.scope === "user";
2130
+ const repoOptedOut = !userTier && options.spellScope === "user";
2131
+ const userAgentFiles = [];
2132
+ const unmanagedAgentFiles = [];
2133
+ const projectDefs = projectAgentsDir(targetDir, userTier ? "user" : "repo");
2099
2134
  const bundledDefs = join3(assetsDir, "agents");
2100
2135
  const resolved = [];
2101
2136
  for (const entry of roster.roster) {
@@ -2117,7 +2152,7 @@ async function syncAgents(targetDir, assetsDir, roster, options = {}) {
2117
2152
  displayName: entry.name ?? entry.definition
2118
2153
  });
2119
2154
  }
2120
- if (roster.openclaw.enabled && options.openclaw !== false) {
2155
+ if (!userTier && roster.openclaw.enabled && options.openclaw !== false) {
2121
2156
  const openclawRoot = roster.openclaw.workspace_root.replace(
2122
2157
  /^~/,
2123
2158
  homedir2()
@@ -2182,34 +2217,46 @@ async function syncAgents(targetDir, assetsDir, roster, options = {}) {
2182
2217
  }
2183
2218
  if (options.copilot !== false) {
2184
2219
  const agentsDir = join3(targetDir, ".github", "agents");
2185
- if (!options.dryRun) {
2186
- await mkdir3(agentsDir, { recursive: true });
2220
+ if (!options.dryRun && !userTier) {
2221
+ await mkdir3(repoOptedOut ? join3(targetDir, ".github") : agentsDir, { recursive: true });
2187
2222
  }
2188
2223
  for (const { def, displayName } of resolved) {
2189
2224
  if (!def.clients.copilot?.agent_file) continue;
2190
2225
  const slug = displayName.toLowerCase().replace(/\s+/g, "-");
2191
2226
  const fileName = `${slug}.agent.md`;
2227
+ const content = renderCopilotAgent(def, displayName);
2228
+ if (userTier) {
2229
+ const relativePath = userAgentFanoutPath(slug);
2230
+ userAgentFiles.push({ relativePath, client: "copilot-agents", content });
2231
+ synced.push(`~/${relativePath}`);
2232
+ continue;
2233
+ }
2234
+ if (repoOptedOut) continue;
2192
2235
  if (!options.dryRun) {
2193
- await writeFile5(
2194
- join3(agentsDir, fileName),
2195
- renderCopilotAgent(def, displayName),
2196
- "utf8"
2197
- );
2236
+ await writeFile5(join3(agentsDir, fileName), content, "utf8");
2198
2237
  } else {
2199
2238
  console.log(` [dry-run] Would write: .github/agents/${fileName}`);
2200
2239
  }
2201
2240
  synced.push(`.github/agents/${fileName}`);
2202
2241
  }
2203
- const copilotSection = `${renderSpellRoutingSection()}
2242
+ if (repoOptedOut) {
2243
+ unmanagedAgentFiles.push(...await existingAgentFiles(agentsDir));
2244
+ }
2245
+ if (!userTier) {
2246
+ const copilotSection = `${renderSpellRoutingSection()}
2204
2247
  ${renderAgentRosterSection(resolved)}`;
2205
- const copilotMerged = await mergeIntoFile(
2206
- targetDir,
2207
- ".github/copilot-instructions.md",
2208
- copilotSection,
2209
- { force: true, dryRun: options.dryRun }
2210
- );
2211
- if (copilotMerged)
2212
- synced.push(".github/copilot-instructions.md (agents section)");
2248
+ const copilotMerged = await mergeIntoFile(
2249
+ targetDir,
2250
+ ".github/copilot-instructions.md",
2251
+ copilotSection,
2252
+ { force: true, dryRun: options.dryRun }
2253
+ );
2254
+ if (copilotMerged)
2255
+ synced.push(".github/copilot-instructions.md (agents section)");
2256
+ }
2257
+ }
2258
+ if (userTier) {
2259
+ return { synced, skipped, hasUnresolvedRoles, userAgentFiles, unmanagedAgentFiles };
2213
2260
  }
2214
2261
  if (options.claude !== false) {
2215
2262
  const claudeSection = `${renderSpellRoutingSection()}
@@ -2233,12 +2280,83 @@ ${renderAgentRosterSection(resolved)}`;
2233
2280
  );
2234
2281
  if (codexMerged) synced.push("AGENTS.md (agents section)");
2235
2282
  }
2236
- return { synced, skipped, hasUnresolvedRoles };
2283
+ return { synced, skipped, hasUnresolvedRoles, userAgentFiles, unmanagedAgentFiles };
2237
2284
  }
2238
2285
 
2239
2286
  // src/modules/agents.ts
2287
+ function agentsTargetDir(scope, cwd) {
2288
+ return scope === "user" ? userTierRoot() : cwd;
2289
+ }
2290
+ async function requireUserStore(storeRoot) {
2291
+ try {
2292
+ return await readManifest(storeRoot);
2293
+ } catch {
2294
+ console.error(
2295
+ `
2296
+ \u2717 No user tier at ${storeRoot}.
2297
+ Run \`spell init --user\` once to create it, then re-run this command.
2298
+ `
2299
+ );
2300
+ process.exit(1);
2301
+ throw new Error("unreachable");
2302
+ }
2303
+ }
2304
+ async function repoSpellScope(targetDir) {
2305
+ try {
2306
+ return effectiveSpellScope(await readManifest(targetDir));
2307
+ } catch {
2308
+ return "repo";
2309
+ }
2310
+ }
2311
+ async function performAgentSync(targetDir, assetsDir, roster, options) {
2312
+ const scope = options.scope ?? "repo";
2313
+ const spellScope = scope === "user" ? void 0 : await repoSpellScope(targetDir);
2314
+ const result = await syncAgents(targetDir, assetsDir, roster, {
2315
+ ...options,
2316
+ scope,
2317
+ ...spellScope === void 0 ? {} : { spellScope }
2318
+ });
2319
+ if (scope === "user") {
2320
+ const manifest = await requireUserStore(targetDir);
2321
+ const fanout = await syncUserTierFanout({
2322
+ homeDir: dirname2(targetDir),
2323
+ storeRoot: targetDir,
2324
+ spellIds: [],
2325
+ extraFiles: result.userAgentFiles,
2326
+ ownedClients: ["copilot-agents"],
2327
+ previous: manifest.fanout,
2328
+ dryRun: options.dryRun,
2329
+ fallbackDir: assetsDir
2330
+ });
2331
+ for (const line of describeFanoutOutcomes(fanout.outcomes, options.dryRun)) {
2332
+ console.log(` ${line}`);
2333
+ }
2334
+ if (!options.dryRun) {
2335
+ await writeManifest(targetDir, { ...manifest, fanout: fanout.record });
2336
+ }
2337
+ }
2338
+ if (result.unmanagedAgentFiles.length > 0) {
2339
+ console.log(
2340
+ `
2341
+ ! This repository takes its spells from the user tier (spell_scope: "user"), so it no
2342
+ longer receives agent files of its own. ${result.unmanagedAgentFiles.length} are still on disk
2343
+ from before the opt-out. Nothing here deletes them -- no component ever tracked
2344
+ them, so there is no recorded hash to prove one untouched. Remove them yourself:
2345
+ `
2346
+ );
2347
+ for (const file of result.unmanagedAgentFiles) console.log(` ${file}`);
2348
+ console.log(`
2349
+ git rm ${result.unmanagedAgentFiles.join(" ")}
2350
+ `);
2351
+ }
2352
+ return result;
2353
+ }
2240
2354
  async function runAgentsInit(options, targetDir, assetsDir) {
2241
- if (!options.force && await rosterExists(targetDir)) {
2355
+ const scope = options.scope ?? "repo";
2356
+ if (scope === "user") await requireUserStore(targetDir);
2357
+ const baseDir = agentsBaseDir(targetDir, scope);
2358
+ const label = scope === "user" ? "~/.arcane" : ".arcane";
2359
+ if (!options.force && await rosterExists(targetDir, scope)) {
2242
2360
  console.log(
2243
2361
  'Agent roster already exists at .arcane/agents.yaml. Run "spell agents sync" to regenerate outputs, or use --force to reinitialize.'
2244
2362
  );
@@ -2332,16 +2450,12 @@ All 12 roles across ${allCategories.join(", ")}` : "\nPick individual roles from
2332
2450
  },
2333
2451
  roster: rosterEntries
2334
2452
  };
2335
- const arcaneDir = join4(targetDir, ".arcane", "agents");
2453
+ const arcaneDir = join4(baseDir, "agents");
2336
2454
  if (!options.dryRun) {
2337
2455
  await mkdir4(arcaneDir, { recursive: true });
2338
- await writeFile6(
2339
- join4(targetDir, ".arcane", "agents.yaml"),
2340
- stringify(roster),
2341
- "utf8"
2342
- );
2456
+ await writeFile6(join4(baseDir, "agents.yaml"), stringify(roster), "utf8");
2343
2457
  } else {
2344
- console.log(" [dry-run] Would write: .arcane/agents.yaml");
2458
+ console.log(` [dry-run] Would write: ${label}/agents.yaml`);
2345
2459
  }
2346
2460
  for (const roleId of selectedRoleIds) {
2347
2461
  const src = join4(assetsDir, "agents", `${roleId}.yaml`);
@@ -2353,7 +2467,7 @@ All 12 roles across ${allCategories.join(", ")}` : "\nPick individual roles from
2353
2467
  console.warn(` ! Could not copy definition for "${roleId}" \u2014 skipping`);
2354
2468
  }
2355
2469
  } else {
2356
- console.log(` [dry-run] Would copy: .arcane/agents/${roleId}.yaml`);
2470
+ console.log(` [dry-run] Would copy: ${label}/agents/${roleId}.yaml`);
2357
2471
  }
2358
2472
  }
2359
2473
  if (options.dryRun) {
@@ -2364,20 +2478,24 @@ All 12 roles across ${allCategories.join(", ")}` : "\nPick individual roles from
2364
2478
  return;
2365
2479
  }
2366
2480
  printStep("Syncing agent definitions to all clients...");
2367
- const { synced, skipped, hasUnresolvedRoles } = await syncAgents(
2481
+ const { synced, skipped, hasUnresolvedRoles } = await performAgentSync(
2368
2482
  targetDir,
2369
2483
  assetsDir,
2370
2484
  roster,
2371
- { dryRun: false }
2485
+ { dryRun: false, scope }
2372
2486
  );
2373
2487
  printSuccess("Agent roster initialized");
2374
2488
  console.log(` \u{1F4CB} Profile: ${profileId} \xB7 Naming: ${namingStrategy} \xB7 Agents: ${rosterEntries.length}`);
2375
- console.log(` \u{1F504} Synced: ${synced.length} outputs (Copilot, Claude, Codex, OpenClaw)`);
2489
+ console.log(
2490
+ ` \u{1F504} Synced: ${synced.length} outputs (${scope === "user" ? "Copilot agent modes in ~/.copilot/agents" : "Copilot, Claude, Codex, OpenClaw"})`
2491
+ );
2376
2492
  if (skipped.length > 0) {
2377
2493
  console.log(` \u26A0\uFE0F Skipped: ${skipped.length} (${skipped.join(", ")})`);
2378
2494
  }
2379
2495
  console.log();
2380
- console.log(` \u{1F4A1} Customize: edit .arcane/agents.yaml \u2192 spell agents sync`);
2496
+ console.log(
2497
+ ` \u{1F4A1} Customize: edit ${label}/agents.yaml \u2192 spell agents sync${scope === "user" ? " --user" : ""}`
2498
+ );
2381
2499
  console.log();
2382
2500
  if (hasUnresolvedRoles) {
2383
2501
  console.error(
@@ -2387,9 +2505,10 @@ All 12 roles across ${allCategories.join(", ")}` : "\nPick individual roles from
2387
2505
  }
2388
2506
  }
2389
2507
  async function runAgentsSync(options, targetDir, assetsDir) {
2508
+ const scope = options.scope ?? "repo";
2390
2509
  let roster;
2391
2510
  try {
2392
- roster = await loadRoster(targetDir);
2511
+ roster = await loadRoster(targetDir, scope);
2393
2512
  } catch (err) {
2394
2513
  if (err instanceof AgentRosterNotFoundError || err instanceof AgentConfigValidationError) {
2395
2514
  console.error(err.message);
@@ -2403,11 +2522,11 @@ async function runAgentsSync(options, targetDir, assetsDir) {
2403
2522
  } else {
2404
2523
  console.log("\n Syncing agents...\n");
2405
2524
  }
2406
- const { synced, skipped, hasUnresolvedRoles } = await syncAgents(
2525
+ const { synced, skipped, hasUnresolvedRoles } = await performAgentSync(
2407
2526
  targetDir,
2408
2527
  assetsDir,
2409
2528
  roster,
2410
- options
2529
+ { ...options, scope }
2411
2530
  );
2412
2531
  if (options.dryRun) {
2413
2532
  console.log(`
@@ -2434,10 +2553,10 @@ async function runAgentsSync(options, targetDir, assetsDir) {
2434
2553
  process.exitCode = 1;
2435
2554
  }
2436
2555
  }
2437
- async function runAgentsList(targetDir) {
2556
+ async function runAgentsList(targetDir, scope = "repo") {
2438
2557
  let roster;
2439
2558
  try {
2440
- roster = await loadRoster(targetDir);
2559
+ roster = await loadRoster(targetDir, scope);
2441
2560
  } catch (err) {
2442
2561
  if (err instanceof AgentRosterNotFoundError || err instanceof AgentConfigValidationError) {
2443
2562
  console.error(err.message);
@@ -2479,9 +2598,9 @@ async function runAgentsList(targetDir) {
2479
2598
  }
2480
2599
 
2481
2600
  // src/modules/push-safety.ts
2482
- import { mkdir as mkdir5, readFile as readFile6, writeFile as writeFile7, chmod, access as access3, rm as rm4, readdir as readdir4 } from "fs/promises";
2601
+ import { mkdir as mkdir5, readFile as readFile6, writeFile as writeFile7, chmod, access as access3, rm as rm4, readdir as readdir5 } from "fs/promises";
2483
2602
  import { constants } from "fs";
2484
- import { join as join5, resolve as resolve2, dirname as dirname2, basename } from "path";
2603
+ import { join as join5, resolve as resolve2, dirname as dirname3, basename } from "path";
2485
2604
 
2486
2605
  // src/modules/git.ts
2487
2606
  import { execFile } from "child_process";
@@ -2778,7 +2897,7 @@ async function repositoryRoot(cwd) {
2778
2897
  ]);
2779
2898
  const commonDir = stdout.trim();
2780
2899
  if (commonDir !== "") {
2781
- const parent = dirname2(commonDir);
2900
+ const parent = dirname3(commonDir);
2782
2901
  if (basename(commonDir) === ".git" && parent !== "") return parent;
2783
2902
  }
2784
2903
  } catch {
@@ -2868,7 +2987,7 @@ async function defaultHooks(cwd) {
2868
2987
  return [];
2869
2988
  }
2870
2989
  try {
2871
- return (await readdir4(hooksDir)).filter((name) => !name.endsWith(".sample")).sort();
2990
+ return (await readdir5(hooksDir)).filter((name) => !name.endsWith(".sample")).sort();
2872
2991
  } catch {
2873
2992
  return [];
2874
2993
  }
@@ -2902,7 +3021,7 @@ async function removeHook(cwd, hookName) {
2902
3021
  const hooksDir = join5(root, ARCANE_HOOKS_DIR);
2903
3022
  let remaining = [];
2904
3023
  try {
2905
- remaining = await readdir4(hooksDir);
3024
+ remaining = await readdir5(hooksDir);
2906
3025
  } catch {
2907
3026
  remaining = [];
2908
3027
  }
@@ -3667,7 +3786,7 @@ async function runInitUser(options, storeRoot, assetsDir, packageVersion) {
3667
3786
  console.log(chalk2.hex("#a855f7")(`
3668
3787
  \u2726 Arcane v${packageVersion}
3669
3788
  `));
3670
- const homeDir = dirname3(storeRoot);
3789
+ const homeDir = dirname4(storeRoot);
3671
3790
  const components = USER_TIER_COMPONENTS.map(
3672
3791
  (name) => componentForScope(getComponent(name), "user")
3673
3792
  );
@@ -3715,7 +3834,7 @@ async function runInitUser(options, storeRoot, assetsDir, packageVersion) {
3715
3834
  fileHashes
3716
3835
  });
3717
3836
  }
3718
- const fanout = await syncUserTierFanout({ homeDir, storeRoot, spellIds, fallbackDir: assetsDir });
3837
+ const fanout = await syncUserTierFanout({ homeDir, storeRoot, spellIds, fallbackDir: assetsDir, ownedClients: SPELL_FANOUT_CLIENTS });
3719
3838
  const manifest = {
3720
3839
  version: packageVersion,
3721
3840
  // The tier is un-profiled; "full" records the profile its component set
@@ -3829,12 +3948,12 @@ async function runAdd(name, options, targetDir, assetsDir, packageVersion) {
3829
3948
  }
3830
3949
 
3831
3950
  // src/commands/update.ts
3832
- import { dirname as dirname4, join as join9 } from "path";
3951
+ import { dirname as dirname5, join as join9 } from "path";
3833
3952
  import { readFile as readFile7, writeFile as writeFile9 } from "fs/promises";
3834
3953
 
3835
3954
  // src/modules/hub.ts
3836
3955
  import { confirm as confirm2, select as select3, input as input3 } from "@inquirer/prompts";
3837
- import { readdir as readdir5, writeFile as writeFile8 } from "fs/promises";
3956
+ import { readdir as readdir6, writeFile as writeFile8 } from "fs/promises";
3838
3957
  import { join as join8 } from "path";
3839
3958
  var MANIFEST_RETROFITS = [
3840
3959
  {
@@ -3979,7 +4098,7 @@ async function offerRegistryScaffold(targetDir, businessRoot) {
3979
4098
  const businessRootPath = join8(targetDir, businessRoot);
3980
4099
  let ventureDirs;
3981
4100
  try {
3982
- const entries = await readdir5(businessRootPath, { withFileTypes: true });
4101
+ const entries = await readdir6(businessRootPath, { withFileTypes: true });
3983
4102
  ventureDirs = entries.filter((e) => e.isDirectory() && e.name !== "_template").map((e) => e.name);
3984
4103
  } catch {
3985
4104
  return;
@@ -4275,7 +4394,7 @@ async function runUpdate(options, targetDir, assetsDir, packageVersion) {
4275
4394
  const scope = resolvedScope.scope;
4276
4395
  const spellScope = effectiveSpellScope(manifest);
4277
4396
  const readopted = findReadoptedSpellComponents(manifest.components, scope, spellScope);
4278
- const homeDir = dirname4(targetDir);
4397
+ const homeDir = dirname5(targetDir);
4279
4398
  if (scope === "repo") {
4280
4399
  console.warn(
4281
4400
  `WARNING: Arcane v${packageVersion} update safety notice: commit your work before updating.`
@@ -4314,6 +4433,7 @@ async function runUpdate(options, targetDir, assetsDir, packageVersion) {
4314
4433
  console.log("Already up to date.");
4315
4434
  if (scope === "user") {
4316
4435
  const fanout = await syncUserTierFanout({
4436
+ ownedClients: SPELL_FANOUT_CLIENTS,
4317
4437
  homeDir,
4318
4438
  storeRoot: targetDir,
4319
4439
  spellIds: spellIdsInStore(manifest.components),
@@ -4526,6 +4646,7 @@ ${verb} ${orphansToReport.length} orphaned file(s) (tracked before this update,
4526
4646
  let fanoutRecord;
4527
4647
  if (scope === "user") {
4528
4648
  const fanout = await syncUserTierFanout({
4649
+ ownedClients: SPELL_FANOUT_CLIENTS,
4529
4650
  homeDir,
4530
4651
  storeRoot: targetDir,
4531
4652
  spellIds: spellIdsInStore(updatedComponents),
@@ -4589,11 +4710,11 @@ ${verb} ${orphansToReport.length} orphaned file(s) (tracked before this update,
4589
4710
  }
4590
4711
 
4591
4712
  // src/commands/status.ts
4592
- import { dirname as dirname6 } from "path";
4713
+ import { dirname as dirname7 } from "path";
4593
4714
 
4594
4715
  // src/modules/version-check.ts
4595
4716
  import { readFile as readFile8, writeFile as writeFile10, mkdir as mkdir6 } from "fs/promises";
4596
- import { dirname as dirname5, join as join10 } from "path";
4717
+ import { dirname as dirname6, join as join10 } from "path";
4597
4718
  import { homedir as homedir3 } from "os";
4598
4719
  import { createRequire } from "module";
4599
4720
  var require2 = createRequire(import.meta.url);
@@ -4622,7 +4743,7 @@ async function readCache(cachePath) {
4622
4743
  }
4623
4744
  }
4624
4745
  async function writeCache(cachePath, latest) {
4625
- await mkdir6(dirname5(cachePath), { recursive: true });
4746
+ await mkdir6(dirname6(cachePath), { recursive: true });
4626
4747
  const entry = { latest, checkedAt: (/* @__PURE__ */ new Date()).toISOString() };
4627
4748
  await writeFile10(cachePath, JSON.stringify(entry, null, 2), "utf-8");
4628
4749
  }
@@ -4763,7 +4884,7 @@ async function runStatus(targetDir, packageVersion, options = {}) {
4763
4884
  console.log("");
4764
4885
  if (scope === "user") {
4765
4886
  console.log(` Scope: user \u2014 ${targetDir}`);
4766
- const health = await inspectUserTierFanout(dirname6(targetDir), manifest.fanout);
4887
+ const health = await inspectUserTierFanout(dirname7(targetDir), manifest.fanout);
4767
4888
  console.log(
4768
4889
  ` Client files: ${health.total} (${health.byClient.codex} ${FANOUT_CLIENT_LABELS.codex}, ${health.byClient.claude} ${FANOUT_CLIENT_LABELS.claude})`
4769
4890
  );
@@ -4826,7 +4947,7 @@ async function runStatus(targetDir, packageVersion, options = {}) {
4826
4947
 
4827
4948
  // src/commands/uninstall.ts
4828
4949
  import { rm as rm5 } from "fs/promises";
4829
- import { dirname as dirname7, join as join11 } from "path";
4950
+ import { dirname as dirname8, join as join11 } from "path";
4830
4951
  import { confirm as confirm3 } from "@inquirer/prompts";
4831
4952
  async function runUninstall(options, targetDir) {
4832
4953
  let manifest;
@@ -4935,7 +5056,7 @@ Run \`spell unblock-push\` first (interactive), then uninstall.`
4935
5056
  \u2713 Uninstalled \u2014 ${removed} files removed.`);
4936
5057
  }
4937
5058
  async function uninstallUserTier(options, storeRoot, manifest) {
4938
- const homeDir = dirname7(storeRoot);
5059
+ const homeDir = dirname8(storeRoot);
4939
5060
  const storeFiles = manifest.components.flatMap((c) => c.files);
4940
5061
  const recordedCount = Object.keys(manifest.fanout ?? {}).length;
4941
5062
  if (options.dryRun) {
@@ -4981,6 +5102,8 @@ async function uninstallUserTier(options, storeRoot, manifest) {
4981
5102
  }
4982
5103
  }
4983
5104
  await rm5(join11(storeRoot, ".arcane.json"), { force: true });
5105
+ await rm5(join11(storeRoot, "agents"), { recursive: true, force: true });
5106
+ await rm5(join11(storeRoot, "agents.yaml"), { force: true });
4984
5107
  await removeDirectoryIfEmpty(join11(storeRoot, USER_TIER_SPELLS_DIR));
4985
5108
  await removeDirectoryIfEmpty(storeRoot);
4986
5109
  for (const line of describeFanoutOutcomes(fanout.outcomes)) console.log(` ${line}`);
@@ -4993,8 +5116,8 @@ async function uninstallUserTier(options, storeRoot, manifest) {
4993
5116
  }
4994
5117
 
4995
5118
  // src/commands/doctor.ts
4996
- import { access as access4, readdir as readdir7, readFile as readFile10, mkdir as mkdir7, copyFile as fsCopyFile } from "fs/promises";
4997
- import { join as join13, dirname as dirname8 } from "path";
5119
+ import { access as access4, readdir as readdir8, readFile as readFile10, mkdir as mkdir7, copyFile as fsCopyFile } from "fs/promises";
5120
+ import { join as join13, dirname as dirname9 } from "path";
4998
5121
  import { execFile as execFile3 } from "child_process";
4999
5122
  import { promisify as promisify2 } from "util";
5000
5123
 
@@ -5064,7 +5187,7 @@ function evaluateIncidentGate(records) {
5064
5187
  }
5065
5188
 
5066
5189
  // src/modules/denylist-scan.ts
5067
- import { readFile as readFile9, readdir as readdir6 } from "fs/promises";
5190
+ import { readFile as readFile9, readdir as readdir7 } from "fs/promises";
5068
5191
  import { join as join12, relative } from "path";
5069
5192
  function escapeRegExp(value) {
5070
5193
  return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
@@ -5111,7 +5234,7 @@ async function collectScannableFiles(root, repoRoot = root, extensions = DEFAULT
5111
5234
  const collected = [];
5112
5235
  let entries;
5113
5236
  try {
5114
- entries = await readdir6(root, { withFileTypes: true });
5237
+ entries = await readdir7(root, { withFileTypes: true });
5115
5238
  } catch {
5116
5239
  return collected;
5117
5240
  }
@@ -5333,8 +5456,8 @@ async function checkVSCodeExtension(id, label) {
5333
5456
  ];
5334
5457
  for (const dir of extDirs) {
5335
5458
  try {
5336
- const { readdir: readdir10 } = await import("fs/promises");
5337
- const entries = await readdir10(dir);
5459
+ const { readdir: readdir11 } = await import("fs/promises");
5460
+ const entries = await readdir11(dir);
5338
5461
  const prefix = id.toLowerCase() + "-";
5339
5462
  const found = entries.some(
5340
5463
  (e) => e.toLowerCase() === id.toLowerCase() || e.toLowerCase().startsWith(prefix)
@@ -5496,7 +5619,7 @@ async function fixSessionContinuity(targetDir, assetsDir) {
5496
5619
  await access4(destPath);
5497
5620
  } catch {
5498
5621
  const srcPath = join13(assetsDir, file);
5499
- await mkdir7(dirname8(destPath), { recursive: true });
5622
+ await mkdir7(dirname9(destPath), { recursive: true });
5500
5623
  await fsCopyFile(srcPath, destPath);
5501
5624
  created.push(file);
5502
5625
  }
@@ -5789,7 +5912,7 @@ async function checkSpellScope(targetDir, homeDir = resolveHomeDir()) {
5789
5912
  let probed = false;
5790
5913
  const probe = "spell-status.md";
5791
5914
  try {
5792
- const entries = await readdir7(join13(storeRoot, USER_TIER_SPELLS_DIR));
5915
+ const entries = await readdir8(join13(storeRoot, USER_TIER_SPELLS_DIR));
5793
5916
  const spells = entries.filter((e) => e.startsWith("spell-") && e.endsWith(".md"));
5794
5917
  spellCount = spells.length;
5795
5918
  probed = spells.includes(probe);
@@ -5811,11 +5934,19 @@ async function checkSpellScope(targetDir, homeDir = resolveHomeDir()) {
5811
5934
  message: `this repository is at v${manifest.version} but the user tier it uses is at v${store.version} \u2014 run \`spell update --user\` (and \`spell update\` here) to bring them together.`
5812
5935
  };
5813
5936
  }
5937
+ const repoExpectsAgents = await rosterExists(targetDir);
5938
+ if (repoExpectsAgents && !await rosterExists(storeRoot, "user")) {
5939
+ return {
5940
+ name,
5941
+ passed: false,
5942
+ message: `this repository has an agent roster and takes its client files from the user tier, but ${storeRoot} has no roster of its own \u2014 no agent mode can reach a client here. Run \`spell agents init --user\`, or set spell_scope back to "repo" and run \`spell agents sync\`.`
5943
+ };
5944
+ }
5814
5945
  return {
5815
5946
  name,
5816
5947
  passed: true,
5817
5948
  blocking: false,
5818
- message: `user \u2014 ${spellCount} spell(s) from ${storeRoot} (v${store.version}), ${probe} present`
5949
+ message: `user \u2014 ${spellCount} spell(s) from ${storeRoot} (v${store.version}), ${probe} present${repoExpectsAgents ? ", agent roster present in the tier" : ""}`
5819
5950
  };
5820
5951
  }
5821
5952
  async function runDoctor(targetDir, options = {}, assetsDir) {
@@ -5892,7 +6023,7 @@ async function runDoctor(targetDir, options = {}, assetsDir) {
5892
6023
  import { readFile as readFile12 } from "fs/promises";
5893
6024
 
5894
6025
  // src/modules/ward.ts
5895
- import { readdir as readdir8, readFile as readFile11 } from "fs/promises";
6026
+ import { readdir as readdir9, readFile as readFile11 } from "fs/promises";
5896
6027
  import { join as join14, relative as relative2 } from "path";
5897
6028
  async function scanFileAllRules(absolutePath, displayPath, rules) {
5898
6029
  const findings = [];
@@ -5951,7 +6082,7 @@ async function scanFilenames(root, rules, repoRoot = root) {
5951
6082
  const findings = [];
5952
6083
  let entries;
5953
6084
  try {
5954
- entries = await readdir8(root, { withFileTypes: true });
6085
+ entries = await readdir9(root, { withFileTypes: true });
5955
6086
  } catch {
5956
6087
  return findings;
5957
6088
  }
@@ -6173,8 +6304,8 @@ import { access as access5 } from "fs/promises";
6173
6304
  import { join as join17 } from "path";
6174
6305
 
6175
6306
  // src/modules/show-report/generate.ts
6176
- import { mkdir as mkdir8, readdir as readdir9, readFile as readFile14, writeFile as writeFile11 } from "fs/promises";
6177
- import { basename as basename3, dirname as dirname9, isAbsolute, join as join16, relative as relative3, resolve as resolve4 } from "path";
6307
+ import { mkdir as mkdir8, readdir as readdir10, readFile as readFile14, writeFile as writeFile11 } from "fs/promises";
6308
+ import { basename as basename3, dirname as dirname10, isAbsolute, join as join16, relative as relative3, resolve as resolve4 } from "path";
6178
6309
 
6179
6310
  // src/modules/show-report/model.ts
6180
6311
  import { readFile as readFile13 } from "fs/promises";
@@ -6731,7 +6862,7 @@ async function programFromPlan(rootDir, slug, planRelPath) {
6731
6862
  program: {
6732
6863
  slug,
6733
6864
  planRelPath,
6734
- queueRelPath: toPosix(join16(dirname9(planRelPath), "OPERATOR-QUEUE.md")),
6865
+ queueRelPath: toPosix(join16(dirname10(planRelPath), "OPERATOR-QUEUE.md")),
6735
6866
  ledgerProgramName: programNameFromTitle(frontmatter.title ?? slug)
6736
6867
  }
6737
6868
  };
@@ -6740,7 +6871,7 @@ async function discoverPrograms(rootDir) {
6740
6871
  const plansDir = join16(rootDir, "docs", "plans");
6741
6872
  let entries;
6742
6873
  try {
6743
- entries = (await readdir9(plansDir, { withFileTypes: true })).filter((e) => e.isDirectory());
6874
+ entries = (await readdir10(plansDir, { withFileTypes: true })).filter((e) => e.isDirectory());
6744
6875
  } catch (error) {
6745
6876
  if (error.code === "ENOENT") return { programs: [], skipped: [] };
6746
6877
  throw error;
@@ -6757,7 +6888,7 @@ async function discoverPrograms(rootDir) {
6757
6888
  async function programFromPlanPath(rootDir, planPath) {
6758
6889
  const planAbs = isAbsolute(planPath) ? planPath : resolve4(rootDir, planPath);
6759
6890
  const planRelPath = toPosix(relative3(rootDir, planAbs));
6760
- return programFromPlan(rootDir, basename3(dirname9(planAbs)), planRelPath);
6891
+ return programFromPlan(rootDir, basename3(dirname10(planAbs)), planRelPath);
6761
6892
  }
6762
6893
  function normalizeLineEndings2(content) {
6763
6894
  return content.replace(/\r\n?/g, "\n");
@@ -6797,7 +6928,7 @@ async function runReportGeneration(options) {
6797
6928
  const generated = await generateReport(options.rootDir, program2, templateHtml);
6798
6929
  const missing = JSON.parse(generated.json).sections.flatMap((section) => section.rows).filter((row) => row.descriptionState === "unwritten").map((row) => row.id);
6799
6930
  if (missing.length > 0) unwritten.push({ slug: program2.slug, ids: missing });
6800
- const outAbs = options.outDir ? isAbsolute(options.outDir) ? options.outDir : resolve4(options.rootDir, options.outDir) : resolve4(options.rootDir, dirname9(program2.planRelPath));
6931
+ const outAbs = options.outDir ? isAbsolute(options.outDir) ? options.outDir : resolve4(options.rootDir, options.outDir) : resolve4(options.rootDir, dirname10(program2.planRelPath));
6801
6932
  const outRel = toPosix(relative3(options.rootDir, outAbs));
6802
6933
  const outputs = [
6803
6934
  [`${outRel}/show-report.json`, generated.json],
@@ -6883,9 +7014,10 @@ async function runReport(targetDir, options = {}, assetsDir) {
6883
7014
  import { realpathSync } from "fs";
6884
7015
  var require3 = createRequire2(import.meta.url);
6885
7016
  var pkg = require3("../package.json");
6886
- var ASSETS_DIR = process.env["ARCANE_ASSETS_DIR"] ?? join18(dirname10(fileURLToPath(import.meta.url)), "assets");
7017
+ var ASSETS_DIR = process.env["ARCANE_ASSETS_DIR"] ?? join18(dirname11(fileURLToPath(import.meta.url)), "assets");
6887
7018
  var program = new Command();
6888
7019
  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)";
7020
+ 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)";
6889
7021
  function targetDirFor(opts) {
6890
7022
  return opts.user ? userTierRoot() : process.cwd();
6891
7023
  }
@@ -6987,39 +7119,44 @@ agentsCommand.command("init").description(
6987
7119
  ).option(
6988
7120
  "--naming <strategy>",
6989
7121
  "Naming strategy: arcanos | generic | random | custom"
6990
- ).option("--force", "Reinitialize even if roster already exists").option("--dry-run", "Preview what would be created without writing files").action(
7122
+ ).option("--force", "Reinitialize even if roster already exists").option("--dry-run", "Preview what would be created without writing files").option("--user", USER_TIER_AGENTS_DESCRIPTION).action(
6991
7123
  async (opts) => {
7124
+ const scope = opts.user ? "user" : "repo";
6992
7125
  await runAgentsInit(
6993
7126
  {
6994
7127
  profile: opts.profile,
6995
7128
  naming: opts.naming,
6996
7129
  force: opts.force,
6997
- dryRun: opts.dryRun
7130
+ dryRun: opts.dryRun,
7131
+ scope
6998
7132
  },
6999
- process.cwd(),
7133
+ agentsTargetDir(scope, process.cwd()),
7000
7134
  ASSETS_DIR
7001
7135
  );
7002
7136
  }
7003
7137
  );
7004
7138
  agentsCommand.command("sync").description(
7005
7139
  "Regenerate all client instruction files from canonical agent definitions"
7006
- ).option("--dry-run", "Preview what would be generated without writing files").option("--no-openclaw", "Skip OpenClaw workspace output").option("--no-copilot", "Skip Copilot output").option("--no-claude", "Skip Claude output").option("--no-codex", "Skip Codex output").action(
7140
+ ).option("--dry-run", "Preview what would be generated without writing files").option("--no-openclaw", "Skip OpenClaw workspace output").option("--no-copilot", "Skip Copilot output").option("--no-claude", "Skip Claude output").option("--no-codex", "Skip Codex output").option("--user", USER_TIER_AGENTS_DESCRIPTION).action(
7007
7141
  async (opts) => {
7142
+ const scope = opts.user ? "user" : "repo";
7008
7143
  await runAgentsSync(
7009
7144
  {
7010
7145
  dryRun: opts.dryRun,
7011
7146
  openclaw: opts.openclaw,
7012
7147
  copilot: opts.copilot,
7013
7148
  claude: opts.claude,
7014
- codex: opts.codex
7149
+ codex: opts.codex,
7150
+ scope
7015
7151
  },
7016
- process.cwd(),
7152
+ agentsTargetDir(scope, process.cwd()),
7017
7153
  ASSETS_DIR
7018
7154
  );
7019
7155
  }
7020
7156
  );
7021
- agentsCommand.command("list").description("Show the current agent roster with assigned names and roles").action(async () => {
7022
- await runAgentsList(process.cwd());
7157
+ agentsCommand.command("list").description("Show the current agent roster with assigned names and roles").option("--user", USER_TIER_AGENTS_DESCRIPTION).action(async (opts) => {
7158
+ const scope = opts.user ? "user" : "repo";
7159
+ await runAgentsList(agentsTargetDir(scope, process.cwd()), scope);
7023
7160
  });
7024
7161
  if (realpathSync(process.argv[1]) === fileURLToPath(import.meta.url)) {
7025
7162
  await program.parseAsync(process.argv);