claude-code-arcane 2.8.0 → 2.9.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/CHANGELOG.md CHANGED
@@ -1,3 +1,37 @@
1
+ ## [2.9.2](https://github.com/SebastianLuser/claude-code-arcane/compare/v2.9.1...v2.9.2) (2026-09-07)
2
+
3
+
4
+ ### Bug Fixes
5
+
6
+ * **update:** only remove content the manifest owns ([d6eec05](https://github.com/SebastianLuser/claude-code-arcane/commit/d6eec05475bb491c8853078d9920c7dc4544e7b3))
7
+
8
+ ## [2.9.1](https://github.com/SebastianLuser/claude-code-arcane/compare/v2.9.0...v2.9.1) (2026-09-04)
9
+
10
+
11
+ ### Bug Fixes
12
+
13
+ * **deps:** patch the two vulnerabilities that reach users ([ec0e25c](https://github.com/SebastianLuser/claude-code-arcane/commit/ec0e25cfd57ce15bf13896bee91ca22d6c3a58cf))
14
+
15
+ # [2.9.0](https://github.com/SebastianLuser/claude-code-arcane/compare/v2.8.0...v2.9.0) (2026-09-04)
16
+
17
+
18
+ ### Bug Fixes
19
+
20
+ * **settings:** point this repo's status line at its own hooks/statusline.sh ([3b180da](https://github.com/SebastianLuser/claude-code-arcane/commit/3b180daed519095f246b1db3d0038698efa28b80))
21
+ * **tests:** pin ARCANE_HOME so npm test cannot write to real installs ([a3836cc](https://github.com/SebastianLuser/claude-code-arcane/commit/a3836cce4820b5651a92df9ad38d00510a74ec38))
22
+ * **update:** refresh statusline.sh and output-styles, and honor --force ([9ebdb20](https://github.com/SebastianLuser/claude-code-arcane/commit/9ebdb2070501837607d7998fe426cd6578c1cbcd))
23
+ * **update:** sync unhashed files past the version gate and activate the style ([0a998be](https://github.com/SebastianLuser/claude-code-arcane/commit/0a998be8ab6cc8848862c83ed547377ab0dd3a8d))
24
+
25
+
26
+ ### Features
27
+
28
+ * **core:** ship the arcane output style and a code-comments rule ([2a981bf](https://github.com/SebastianLuser/claude-code-arcane/commit/2a981bf847d8c41143da1353dd230f98ca7f7e11))
29
+
30
+
31
+ ### Performance Improvements
32
+
33
+ * **statusline:** cut ~2s of process spawns from the status line ([108afe4](https://github.com/SebastianLuser/claude-code-arcane/commit/108afe409f3a51e54e22587317bbebeba04f3e07))
34
+
1
35
  # [2.8.0](https://github.com/SebastianLuser/claude-code-arcane/compare/v2.7.1...v2.8.0) (2026-08-27)
2
36
 
3
37
 
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # Claude Code Arcane
2
2
 
3
- > **437 skills, 115 agents, 17 hooks and 30 rules for Claude Code — selective deploy by profile.**
3
+ > **437 skills, 115 agents, 17 hooks and 31 rules for Claude Code — selective deploy by profile.**
4
4
 
5
5
  A configuration harness installable via `npx`. Pick a **profile** that matches your stack and only the relevant tools get installed into your project's `.claude/` directory.
6
6
 
@@ -439,7 +439,7 @@ claude-code-arcane/
439
439
  ├── profiles/ # 38 profiles (YAML)
440
440
  ├── agents/ # 17 dirs, 109 agents (Markdown)
441
441
  ├── hooks/ # 17 lifecycle hooks (Bash)
442
- ├── rules/ # 30 rules (Markdown)
442
+ ├── rules/ # 31 rules (Markdown)
443
443
  ├── templates/ # Gamedev templates
444
444
  ├── docs/ # Documentation
445
445
  └── skills-selftest/ # QA framework
@@ -87,7 +87,6 @@ If an ADR exists for this system:
87
87
  - State machines must have explicit transition tables
88
88
  - No direct references to UI code (use events/signals)
89
89
  - Frame-rate independent logic (delta time everywhere)
90
- - Document the design doc each feature implements in code comments
91
90
 
92
91
  ### What This Agent Must NOT Do
93
92
 
package/dist/cli.js CHANGED
@@ -26,6 +26,7 @@ function parseProfile(filePath) {
26
26
  },
27
27
  agents: raw.agents ?? [],
28
28
  hooks: raw.hooks ?? [],
29
+ output_styles: raw.output_styles ?? [],
29
30
  permissions: {
30
31
  allow: perms.allow ?? [],
31
32
  deny: perms.deny ?? []
@@ -67,6 +68,7 @@ function mergeProfiles(profilesDir, profileNames) {
67
68
  const seenRulesGamedev = /* @__PURE__ */ new Set();
68
69
  const seenAgents = /* @__PURE__ */ new Set();
69
70
  const seenHooks = /* @__PURE__ */ new Set();
71
+ const seenOutputStyles = /* @__PURE__ */ new Set();
70
72
  const allPermAllow = [];
71
73
  const allPermDeny = [];
72
74
  const skills = [];
@@ -74,6 +76,7 @@ function mergeProfiles(profilesDir, profileNames) {
74
76
  const rulesGamedev = [];
75
77
  const agents = [];
76
78
  const hooks = [];
79
+ const outputStyles = [];
77
80
  for (const name of namesToLoad) {
78
81
  const filePath = path.join(profilesDir, `${name}.yaml`);
79
82
  if (!fs.existsSync(filePath)) {
@@ -112,6 +115,12 @@ function mergeProfiles(profilesDir, profileNames) {
112
115
  hooks.push(h);
113
116
  }
114
117
  }
118
+ for (const o of profile.output_styles) {
119
+ if (!seenOutputStyles.has(o)) {
120
+ seenOutputStyles.add(o);
121
+ outputStyles.push(o);
122
+ }
123
+ }
115
124
  allPermAllow.push(...profile.permissions.allow);
116
125
  allPermDeny.push(...profile.permissions.deny);
117
126
  }
@@ -121,6 +130,7 @@ function mergeProfiles(profilesDir, profileNames) {
121
130
  rules: { universal: rulesUniv, gamedev: rulesGamedev },
122
131
  agents,
123
132
  hooks,
133
+ output_styles: outputStyles,
124
134
  permissions: {
125
135
  allow: [...new Set(allPermAllow)],
126
136
  deny: [...new Set(allPermDeny)]
@@ -732,6 +742,7 @@ function computeContentHashes(claudeDir) {
732
742
  const skillsDir = path7.join(claudeDir, "skills");
733
743
  if (fs6.existsSync(skillsDir)) {
734
744
  for (const entry of fs6.readdirSync(skillsDir, { withFileTypes: true })) {
745
+ if (entry.name.startsWith("_")) continue;
735
746
  if (entry.isDirectory()) {
736
747
  const dirHash = hashDirectory(path7.join(skillsDir, entry.name));
737
748
  const combined = createHash("sha256").update(JSON.stringify(dirHash)).digest("hex");
@@ -805,6 +816,7 @@ function computeSourceHashes(packageRoot, skills, rules, agents) {
805
816
  const hooksDir = path7.join(packageRoot, "hooks");
806
817
  if (fs6.existsSync(hooksDir)) {
807
818
  for (const entry of fs6.readdirSync(hooksDir, { withFileTypes: true })) {
819
+ if (entry.name === "statusline.sh") continue;
808
820
  if (!entry.isDirectory()) {
809
821
  hashes.hooks[entry.name] = hashFile(path7.join(hooksDir, entry.name));
810
822
  }
@@ -990,6 +1002,7 @@ var Installer = class {
990
1002
  this.copyHooks();
991
1003
  this.copySkills();
992
1004
  this.copyRules();
1005
+ this.copyOutputStyles();
993
1006
  this.copyAgents();
994
1007
  this.copyDocs();
995
1008
  this.generateSettings();
@@ -1024,6 +1037,9 @@ var Installer = class {
1024
1037
  for (const d of ["skills", "rules", "agents", "docs"]) {
1025
1038
  ensureDir(path8.join(this.claudeDir, d));
1026
1039
  }
1040
+ if (this.merged.output_styles.length > 0) {
1041
+ ensureDir(path8.join(this.claudeDir, "output-styles"));
1042
+ }
1027
1043
  }
1028
1044
  get hasStatusline() {
1029
1045
  return this.merged.loaded.includes("statusline");
@@ -1147,6 +1163,26 @@ var Installer = class {
1147
1163
  }
1148
1164
  }
1149
1165
  }
1166
+ copyOutputStyles() {
1167
+ if (this.merged.output_styles.length === 0) return;
1168
+ this.log("\nOutput styles:");
1169
+ for (const style of this.merged.output_styles) {
1170
+ const src = path8.join(this.root, "output-styles", `${style}.md`);
1171
+ if (!fs7.existsSync(src)) {
1172
+ this.log(` WARN: Output style '${style}.md' not found`);
1173
+ continue;
1174
+ }
1175
+ if (this.dryRun) {
1176
+ this.log(` [dry-run] ${style}.md`);
1177
+ } else {
1178
+ fs7.copyFileSync(
1179
+ src,
1180
+ path8.join(this.claudeDir, "output-styles", `${style}.md`)
1181
+ );
1182
+ this.log(` [ok] ${style}.md`);
1183
+ }
1184
+ }
1185
+ }
1150
1186
  copyAgents() {
1151
1187
  if (this.merged.agents.length === 0) return;
1152
1188
  this.log("\nAgents:");
@@ -1204,6 +1240,10 @@ var Installer = class {
1204
1240
  command: "bash .claude/statusline.sh"
1205
1241
  };
1206
1242
  }
1243
+ const activeStyle = this.merged.output_styles[0];
1244
+ if (activeStyle) {
1245
+ settings.outputStyle = activeStyle;
1246
+ }
1207
1247
  if (this.dryRun) {
1208
1248
  this.log(
1209
1249
  ` [dry-run] settings.json (${this.merged.permissions.allow.length} allow, ${this.merged.permissions.deny.length} deny)`
@@ -1290,7 +1330,7 @@ function storeInCache(version, contentDir) {
1290
1330
  fs8.rmSync(versionDir, { recursive: true, force: true });
1291
1331
  }
1292
1332
  fs8.mkdirSync(versionDir, { recursive: true });
1293
- for (const dir of ["skills", "rules", "agents", "hooks", "profiles", "templates", "docs"]) {
1333
+ for (const dir of ["skills", "rules", "agents", "hooks", "profiles", "templates", "docs", "output-styles"]) {
1294
1334
  const src = path9.join(contentDir, dir);
1295
1335
  if (fs8.existsSync(src)) {
1296
1336
  copyDirRecursive(src, path9.join(versionDir, dir));
@@ -1847,6 +1887,7 @@ async function addCommand(items = []) {
1847
1887
  rules: { universal: manifest.installed_rules, gamedev: [] },
1848
1888
  agents: manifest.installed_agents,
1849
1889
  hooks: [],
1890
+ output_styles: [],
1850
1891
  permissions: { allow: [], deny: [] }
1851
1892
  };
1852
1893
  writeManifest(target, merged, manifest.profile_command, root);
@@ -2275,6 +2316,7 @@ function safeVersion() {
2275
2316
  }
2276
2317
 
2277
2318
  // src/commands/update.ts
2319
+ var TRASH_DIR = ".arcane-trash";
2278
2320
  async function updateCommand(opts) {
2279
2321
  if (opts.here) {
2280
2322
  await updateTarget(process.cwd(), opts);
@@ -2355,16 +2397,34 @@ async function updateTarget(target, opts) {
2355
2397
  const root = await contentSource.getContentRoot();
2356
2398
  const currentVersion = await contentSource.getVersion();
2357
2399
  const installedVersion = manifest.source_version ?? manifest.arcane_version;
2400
+ const profileNamesForSync = manifest.profile_command.split("+").filter(Boolean);
2401
+ const claudeDirForSync = path17.join(target, ".claude");
2402
+ const earlySync = syncUnhashedFiles(
2403
+ root,
2404
+ claudeDirForSync,
2405
+ mergeProfiles(path17.join(root, "profiles"), profileNamesForSync),
2406
+ opts.dryRun ?? false
2407
+ );
2408
+ if (earlySync.length > 0 && !opts.quiet) {
2409
+ const prefix = opts.dryRun ? "[dry-run] " : "";
2410
+ console.log(`
2411
+ ${prefix}${chalk7.cyan("Refreshed:")}`);
2412
+ for (const label of earlySync) {
2413
+ console.log(` ~ ${label}`);
2414
+ }
2415
+ }
2358
2416
  if (installedVersion === currentVersion && !opts.force) {
2359
2417
  if (!opts.quiet) {
2360
- console.log(chalk7.green(`Already up to date (v${currentVersion}).`));
2418
+ console.log(
2419
+ earlySync.length > 0 ? chalk7.green(`Refreshed ${earlySync.length} file(s); otherwise up to date (v${currentVersion}).`) : chalk7.green(`Already up to date (v${currentVersion}).`)
2420
+ );
2361
2421
  }
2362
2422
  return {
2363
2423
  target,
2364
- status: "up-to-date",
2424
+ status: earlySync.length > 0 ? "updated" : "up-to-date",
2365
2425
  fromVersion: installedVersion,
2366
2426
  toVersion: currentVersion,
2367
- updated: 0,
2427
+ updated: earlySync.length,
2368
2428
  skipped: 0,
2369
2429
  removed: 0
2370
2430
  };
@@ -2382,24 +2442,28 @@ Update available: ${chalk7.red(installedVersion)} -> ${chalk7.green(currentVersi
2382
2442
  const sourceHashes = computeSourceHashes(root, merged.skills, allRules, merged.agents);
2383
2443
  const claudeDir = path17.join(target, ".claude");
2384
2444
  const installedHashes = computeContentHashes(claudeDir);
2445
+ const unhashed = earlySync;
2385
2446
  const items = computeUpdatePlan(
2386
2447
  manifest.content_hashes ?? null,
2387
2448
  installedHashes,
2388
- sourceHashes
2449
+ sourceHashes,
2450
+ makeOwnershipCheck(manifest),
2451
+ granularAgentDivisions(merged.agents)
2389
2452
  );
2390
2453
  const updates = items.filter((i) => i.action === "update" || i.action === "add" || i.action === "conflict");
2391
2454
  const skipped = items.filter((i) => i.action === "skip-customized" || i.action === "skip-unchanged");
2392
2455
  const removed = items.filter((i) => i.action === "remove");
2456
+ const forcible = opts.force ? items.filter((i) => i.action === "skip-customized") : [];
2393
2457
  if (!opts.quiet) {
2394
2458
  printUpdateSummary(items, opts.dryRun ?? false);
2395
2459
  }
2396
- if (updates.length === 0 && removed.length === 0) {
2460
+ if (updates.length === 0 && removed.length === 0 && forcible.length === 0) {
2397
2461
  if (!opts.dryRun) {
2398
2462
  updateManifestFields(target, {
2399
2463
  arcane_version: currentVersion,
2400
2464
  source_version: currentVersion,
2401
2465
  updated_at: (/* @__PURE__ */ new Date()).toISOString().replace(/\.\d{3}Z$/, "Z"),
2402
- content_hashes: installedHashes,
2466
+ content_hashes: stripForeign(installedHashes, items),
2403
2467
  installed_skills: merged.skills,
2404
2468
  installed_rules: allRules,
2405
2469
  installed_agents: merged.agents,
@@ -2408,7 +2472,10 @@ Update available: ${chalk7.red(installedVersion)} -> ${chalk7.green(currentVersi
2408
2472
  });
2409
2473
  }
2410
2474
  if (!opts.quiet) {
2411
- console.log(chalk7.green("\nNo changes to apply."));
2475
+ console.log(
2476
+ unhashed.length > 0 ? chalk7.green(`
2477
+ Refreshed ${unhashed.length} file(s); no other changes to apply.`) : chalk7.green("\nNo changes to apply.")
2478
+ );
2412
2479
  }
2413
2480
  return {
2414
2481
  target,
@@ -2437,7 +2504,7 @@ Update available: ${chalk7.red(installedVersion)} -> ${chalk7.green(currentVersi
2437
2504
  arcane_version: currentVersion,
2438
2505
  source_version: currentVersion,
2439
2506
  updated_at: (/* @__PURE__ */ new Date()).toISOString().replace(/\.\d{3}Z$/, "Z"),
2440
- content_hashes: newHashes,
2507
+ content_hashes: stripForeign(newHashes, items),
2441
2508
  installed_skills: merged.skills,
2442
2509
  installed_rules: allRules,
2443
2510
  installed_agents: merged.agents,
@@ -2467,7 +2534,56 @@ function printGeneralSummary(results, isDryRun) {
2467
2534
  ${prefix}Done. ${results.length} installation${results.length === 1 ? "" : "s"} processed:`));
2468
2535
  console.log(` ${chalk7.green(updated)} ${isDryRun ? "would change" : "updated"}, ${chalk7.dim(unchanged + " unchanged")}`);
2469
2536
  }
2470
- function computeUpdatePlan(manifestHashes, installedHashes, sourceHashes) {
2537
+ function makeOwnershipCheck(manifest) {
2538
+ const hashes = manifest.content_hashes;
2539
+ if (hashes) {
2540
+ return (type, name) => name in (hashes[type] ?? {});
2541
+ }
2542
+ const skills = new Set(manifest.installed_skills ?? []);
2543
+ const rules = new Set(
2544
+ (manifest.installed_rules ?? []).map((r) => r.endsWith(".md") ? r : `${r}.md`)
2545
+ );
2546
+ const agents = new Set(
2547
+ (manifest.installed_agents ?? []).map((a) => parseAgentEntry(a).division)
2548
+ );
2549
+ return (type, name) => {
2550
+ switch (type) {
2551
+ case "skills":
2552
+ return skills.has(name);
2553
+ case "rules":
2554
+ return rules.has(name);
2555
+ case "agents":
2556
+ return agents.has(name);
2557
+ case "hooks":
2558
+ return true;
2559
+ }
2560
+ };
2561
+ }
2562
+ function granularAgentDivisions(entries) {
2563
+ const whole = /* @__PURE__ */ new Set();
2564
+ const partial = /* @__PURE__ */ new Set();
2565
+ for (const raw of entries) {
2566
+ const { division, agent } = parseAgentEntry(raw);
2567
+ (agent ? partial : whole).add(division);
2568
+ }
2569
+ for (const division of whole) partial.delete(division);
2570
+ return partial;
2571
+ }
2572
+ function stripForeign(hashes, items) {
2573
+ const stripped = {
2574
+ skills: { ...hashes.skills },
2575
+ rules: { ...hashes.rules },
2576
+ agents: { ...hashes.agents },
2577
+ hooks: { ...hashes.hooks }
2578
+ };
2579
+ for (const item of items) {
2580
+ if (item.action !== "keep-foreign") continue;
2581
+ const bucket = `${item.type}s`;
2582
+ delete stripped[bucket][item.name];
2583
+ }
2584
+ return stripped;
2585
+ }
2586
+ function computeUpdatePlan(manifestHashes, installedHashes, sourceHashes, owned, exemptAgentDivisions = /* @__PURE__ */ new Set()) {
2471
2587
  const items = [];
2472
2588
  for (const contentType of ["skills", "rules", "agents", "hooks"]) {
2473
2589
  const itemType = contentType === "skills" ? "skill" : contentType === "rules" ? "rule" : contentType === "agents" ? "agent" : "hook";
@@ -2480,10 +2596,11 @@ function computeUpdatePlan(manifestHashes, installedHashes, sourceHashes) {
2480
2596
  ...Object.keys(source)
2481
2597
  ]);
2482
2598
  for (const key of allKeys) {
2599
+ if (contentType === "agents" && exemptAgentDivisions.has(key)) continue;
2483
2600
  const mHash = manifest[key] ?? null;
2484
2601
  const iHash = installed[key] ?? null;
2485
2602
  const sHash = source[key] ?? null;
2486
- const action = resolveAction(mHash, iHash, sHash);
2603
+ const action = resolveAction(mHash, iHash, sHash, owned(contentType, key));
2487
2604
  if (action) {
2488
2605
  items.push({ type: itemType, name: key, action });
2489
2606
  }
@@ -2491,12 +2608,13 @@ function computeUpdatePlan(manifestHashes, installedHashes, sourceHashes) {
2491
2608
  }
2492
2609
  return items;
2493
2610
  }
2494
- function resolveAction(manifestHash, installedHash, sourceHash) {
2611
+ function resolveAction(manifestHash, installedHash, sourceHash, owned) {
2495
2612
  if (!sourceHash && !installedHash) return null;
2496
2613
  if (sourceHash && !installedHash) return "add";
2497
- if (!sourceHash && installedHash) return "remove";
2614
+ if (!sourceHash && installedHash) return owned ? "remove" : "keep-foreign";
2498
2615
  if (!manifestHash) {
2499
2616
  if (installedHash === sourceHash) return "skip-unchanged";
2617
+ if (!owned) return "conflict";
2500
2618
  return "update";
2501
2619
  }
2502
2620
  if (manifestHash === installedHash && installedHash === sourceHash) return "skip-unchanged";
@@ -2512,6 +2630,7 @@ function printUpdateSummary(items, isDryRun) {
2512
2630
  const conflicts = items.filter((i) => i.action === "conflict");
2513
2631
  const customized = items.filter((i) => i.action === "skip-customized");
2514
2632
  const removed = items.filter((i) => i.action === "remove");
2633
+ const foreign = items.filter((i) => i.action === "keep-foreign");
2515
2634
  if (updates.length > 0) {
2516
2635
  console.log(`
2517
2636
  ${prefix}${chalk7.cyan("Will update:")}`);
@@ -2528,14 +2647,23 @@ ${prefix}${chalk7.cyan("Will add:")}`);
2528
2647
  }
2529
2648
  if (removed.length > 0) {
2530
2649
  console.log(`
2531
- ${prefix}${chalk7.cyan("Will remove:")}`);
2650
+ ${prefix}${chalk7.cyan("Will remove")} ${chalk7.dim(`(moved to .claude/${TRASH_DIR}/)`)}:`);
2532
2651
  for (const item of removed) {
2533
2652
  console.log(` ${chalk7.red("-")} ${item.type}: ${item.name}`);
2534
2653
  }
2535
2654
  }
2536
- if (conflicts.length > 0) {
2655
+ if (foreign.length > 0) {
2537
2656
  console.log(`
2538
- ${prefix}${chalk7.yellow("Conflicts (will backup + update):")}`);
2657
+ ${chalk7.cyan("Left alone")} ${chalk7.dim("(not installed by Arcane)")}:`);
2658
+ for (const item of foreign) {
2659
+ console.log(` ${chalk7.dim("=")} ${item.type}: ${item.name}`);
2660
+ }
2661
+ }
2662
+ if (conflicts.length > 0) {
2663
+ console.log(
2664
+ `
2665
+ ${prefix}${chalk7.yellow("Conflicts (will update)")} ${chalk7.dim(`(old copy to .claude/${TRASH_DIR}/)`)}:`
2666
+ );
2539
2667
  for (const item of conflicts) {
2540
2668
  console.log(` ${chalk7.yellow("!")} ${item.type}: ${item.name}`);
2541
2669
  }
@@ -2550,6 +2678,7 @@ ${prefix}${chalk7.dim("Skipped (locally customized):")}`);
2550
2678
  }
2551
2679
  function applyUpdates(items, root, claudeDir, merged, force) {
2552
2680
  for (const item of items) {
2681
+ if (item.action === "keep-foreign") continue;
2553
2682
  if (item.action === "skip-unchanged" || item.action === "skip-customized") {
2554
2683
  if (item.action === "skip-customized" && force) {
2555
2684
  applyItem(item, root, claudeDir);
@@ -2557,7 +2686,7 @@ function applyUpdates(items, root, claudeDir, merged, force) {
2557
2686
  continue;
2558
2687
  }
2559
2688
  if (item.action === "conflict") {
2560
- backupItem(item, claudeDir);
2689
+ quarantineItem(item, claudeDir);
2561
2690
  applyItem(item, root, claudeDir);
2562
2691
  continue;
2563
2692
  }
@@ -2566,10 +2695,85 @@ function applyUpdates(items, root, claudeDir, merged, force) {
2566
2695
  continue;
2567
2696
  }
2568
2697
  if (item.action === "remove") {
2569
- removeItem(item, claudeDir);
2698
+ quarantineItem(item, claudeDir);
2570
2699
  }
2571
2700
  }
2572
2701
  }
2702
+ function syncUnhashedFiles(root, claudeDir, merged, dryRun) {
2703
+ const targets = [];
2704
+ const statuslineDst = path17.join(claudeDir, "statusline.sh");
2705
+ if (merged.loaded.includes("statusline") || fs14.existsSync(statuslineDst)) {
2706
+ targets.push({
2707
+ src: path17.join(root, "hooks", "statusline.sh"),
2708
+ dst: statuslineDst,
2709
+ label: "statusline.sh"
2710
+ });
2711
+ }
2712
+ for (const style of merged.output_styles) {
2713
+ targets.push({
2714
+ src: path17.join(root, "output-styles", `${style}.md`),
2715
+ dst: path17.join(claudeDir, "output-styles", `${style}.md`),
2716
+ label: `output-styles/${style}.md`
2717
+ });
2718
+ }
2719
+ for (const raw of merged.agents) {
2720
+ const { division, agent } = parseAgentEntry(raw);
2721
+ if (!agent) continue;
2722
+ targets.push({
2723
+ src: path17.join(root, "agents", division, `${agent}.md`),
2724
+ dst: path17.join(claudeDir, "agents", division, `${agent}.md`),
2725
+ label: `agents/${division}/${agent}.md`
2726
+ });
2727
+ }
2728
+ if (merged.rules.gamedev.length > 0) {
2729
+ const templatesSrc = path17.join(root, "templates", "gamedev");
2730
+ if (fs14.existsSync(templatesSrc)) {
2731
+ for (const rel of fs14.readdirSync(templatesSrc, { recursive: true })) {
2732
+ const src = path17.join(templatesSrc, rel);
2733
+ if (!fs14.statSync(src).isFile()) continue;
2734
+ targets.push({
2735
+ src,
2736
+ dst: path17.join(claudeDir, "skills", "_templates", rel),
2737
+ label: `skills/_templates/${String(rel).split(path17.sep).join("/")}`
2738
+ });
2739
+ }
2740
+ }
2741
+ }
2742
+ const changed = [];
2743
+ for (const t of targets) {
2744
+ if (!fs14.existsSync(t.src)) continue;
2745
+ const src = fs14.readFileSync(t.src);
2746
+ if (fs14.existsSync(t.dst) && src.equals(fs14.readFileSync(t.dst))) continue;
2747
+ changed.push(t.label);
2748
+ if (dryRun) continue;
2749
+ if (fs14.existsSync(t.dst)) {
2750
+ const bak = `${t.dst}.bak`;
2751
+ if (fs14.existsSync(bak)) fs14.rmSync(bak, { force: true });
2752
+ fs14.renameSync(t.dst, bak);
2753
+ }
2754
+ fs14.mkdirSync(path17.dirname(t.dst), { recursive: true });
2755
+ fs14.copyFileSync(t.src, t.dst);
2756
+ }
2757
+ const activeStyle = merged.output_styles[0];
2758
+ if (activeStyle) {
2759
+ const settingsPath = path17.join(claudeDir, "settings.json");
2760
+ if (fs14.existsSync(settingsPath)) {
2761
+ try {
2762
+ const settings = JSON.parse(fs14.readFileSync(settingsPath, "utf-8"));
2763
+ if (settings.outputStyle !== activeStyle) {
2764
+ changed.push(`settings.json (outputStyle: ${activeStyle})`);
2765
+ if (!dryRun) {
2766
+ settings.outputStyle = activeStyle;
2767
+ fs14.writeFileSync(settingsPath, `${JSON.stringify(settings, null, 2)}
2768
+ `, "utf-8");
2769
+ }
2770
+ }
2771
+ } catch {
2772
+ }
2773
+ }
2774
+ }
2775
+ return changed;
2776
+ }
2573
2777
  function applyItem(item, root, claudeDir) {
2574
2778
  const src = getSourcePath(item, root);
2575
2779
  const dst = getInstalledPath(item, claudeDir);
@@ -2587,20 +2791,15 @@ function applyItem(item, root, claudeDir) {
2587
2791
  fs14.copyFileSync(src, dst);
2588
2792
  }
2589
2793
  }
2590
- function backupItem(item, claudeDir) {
2794
+ function quarantineItem(item, claudeDir) {
2591
2795
  const installed = getInstalledPath(item, claudeDir);
2592
2796
  if (!fs14.existsSync(installed)) return;
2593
- const backupPath = installed + ".bak";
2594
- if (fs14.existsSync(backupPath)) {
2595
- fs14.rmSync(backupPath, { recursive: true, force: true });
2596
- }
2597
- fs14.renameSync(installed, backupPath);
2598
- }
2599
- function removeItem(item, claudeDir) {
2600
- const installed = getInstalledPath(item, claudeDir);
2601
- if (fs14.existsSync(installed)) {
2602
- fs14.rmSync(installed, { recursive: true, force: true });
2797
+ const quarantine = path17.join(claudeDir, TRASH_DIR, `${item.type}s`, item.name);
2798
+ ensureDir(path17.dirname(quarantine));
2799
+ if (fs14.existsSync(quarantine)) {
2800
+ fs14.rmSync(quarantine, { recursive: true, force: true });
2603
2801
  }
2802
+ fs14.renameSync(installed, quarantine);
2604
2803
  }
2605
2804
  function getSourcePath(item, root) {
2606
2805
  switch (item.type) {
@@ -2,7 +2,7 @@
2
2
 
3
3
  ## What is Arcane?
4
4
 
5
- Arcane is a configuration harness for [Claude Code](https://docs.anthropic.com/en/docs/claude-code). It provides a curated library of **437 skills**, **115 agents**, **17 hooks**, and **30 rules** that you selectively deploy into any project. Instead of shipping everything to every project (wasting tokens and degrading performance), you pick a **profile** that matches your stack and only the relevant tools get installed.
5
+ Arcane is a configuration harness for [Claude Code](https://docs.anthropic.com/en/docs/claude-code). It provides a curated library of **437 skills**, **115 agents**, **17 hooks**, and **31 rules** that you selectively deploy into any project. Instead of shipping everything to every project (wasting tokens and degrading performance), you pick a **profile** that matches your stack and only the relevant tools get installed.
6
6
 
7
7
  ## Table of Contents
8
8
 
@@ -146,10 +146,26 @@ Shows installed profiles, skill count, rules, agents, installation date, and wor
146
146
  ```bash
147
147
  npx claude-code-arcane update
148
148
 
149
+ # Only this repo, instead of every registered installation
150
+ npx claude-code-arcane update --here
151
+
152
+ # Preview
153
+ npx claude-code-arcane update --dry-run
154
+
149
155
  # Quiet mode (for hooks — fails silently)
150
156
  npx claude-code-arcane update --quiet
151
157
  ```
152
158
 
159
+ **What it will and will not touch.** Arcane only updates or removes content its own
160
+ manifest records having installed. A skill or agent you wrote by hand in `.claude/`
161
+ is reported as `Left alone (not installed by Arcane)` and never modified — including
162
+ under `--force`, which only exists to overwrite *Arcane's* files that you edited
163
+ locally. Files you edited that Arcane does own are skipped as `customized`.
164
+
165
+ When a skill really does leave the profile, it is moved to `.claude/.arcane-trash/`
166
+ rather than deleted, so a wrong call is recoverable. The trash sits outside
167
+ `.claude/skills/`, so a removed skill stops loading immediately.
168
+
153
169
  ### `npx claude-code-arcane clean` — Remove Arcane from a project
154
170
 
155
171
  ```bash
@@ -15,12 +15,17 @@ fi
15
15
  # Skills/agents Claude can actually invoke = global (~/.claude) + project (.claude),
16
16
  # deduped by name (project overrides global). Follow -L so .claude/skills junctions count.
17
17
  # Top-level only (mindepth/maxdepth 2) so bundled sub-skills don't inflate the total.
18
+ #
19
+ # Names are extracted with a single awk, never `xargs -n1 dirname|basename`: that spawned one
20
+ # process per file (~160 here), which cost ~1.7s of the ~2.5s total on Windows and made the
21
+ # status line miss its execution window under load — Claude Code drops the output on a
22
+ # non-zero exit or an abort, so the bar vanished at random.
18
23
  SKILLS=$( { find -L "$HOME/.claude/skills" -mindepth 2 -maxdepth 2 -name "SKILL.md" 2>/dev/null;
19
24
  find -L .claude/skills -mindepth 2 -maxdepth 2 -name "SKILL.md" 2>/dev/null; } \
20
- | xargs -n1 dirname 2>/dev/null | xargs -n1 basename 2>/dev/null | sort -u | wc -l | tr -d ' ')
25
+ | awk -F/ 'NF>1{print $(NF-1)}' | sort -u | wc -l | tr -d ' ')
21
26
  AGENTS=$( { find -L "$HOME/.claude/agents" -name "*.md" 2>/dev/null;
22
27
  find -L .claude/agents -name "*.md" 2>/dev/null; } \
23
- | xargs -n1 basename 2>/dev/null | sort -u | wc -l | tr -d ' ')
28
+ | awk -F/ '{print $NF}' | sort -u | wc -l | tr -d ' ')
24
29
 
25
30
  PYCMD=$(command -v python 2>/dev/null || command -v python3 2>/dev/null || command -v py 2>/dev/null)
26
31
 
@@ -117,8 +122,16 @@ PREFIX="🔮 ${PROJECT}"
117
122
 
118
123
  PARTS="${PREFIX}"
119
124
  [ -n "$BRANCH" ] && PARTS="${PARTS} | 🌿 ${BRANCH}"
120
- PARTS="${PARTS} | 🛠️ ${SKILLS} skills"
121
- [ "$AGENTS" -gt 0 ] 2>/dev/null && PARTS="${PARTS} | 🤖 ${AGENTS} agents"
125
+ PARTS="${PARTS} | 🛠️ ${SKILLS:-0} skills"
126
+ case "$AGENTS" in
127
+ ''|*[!0-9]*) ;;
128
+ 0) ;;
129
+ *) PARTS="${PARTS} | 🤖 ${AGENTS} agents" ;;
130
+ esac
122
131
  [ -n "$EXTRA" ] && PARTS="${PARTS} | ${EXTRA}"
123
132
 
124
133
  echo "$PARTS"
134
+
135
+ # Claude Code discards the status line on a non-zero exit, so never inherit one from the
136
+ # last conditional above.
137
+ exit 0
@@ -0,0 +1,53 @@
1
+ ---
2
+ name: arcane
3
+ description: Respuestas compactas, tablas para comparar, lenguaje literal y estructura legible — pensado para lectura con TEA y dislexia.
4
+ ---
5
+
6
+ # Arcane
7
+
8
+ Respondé de forma compacta, literal y estructurada. Estas instrucciones son sobre **la
9
+ forma** de la respuesta, no sobre el alcance del trabajo: no reducen lo que hay que hacer ni
10
+ autorizan entregar menos.
11
+
12
+ ## Largo
13
+
14
+ - **La respuesta primero.** Primera línea: qué pasó o qué contestás. El detalle va después,
15
+ para quien lo quiera.
16
+ - **Sin preámbulo.** No repitas la pregunta, no anuncies lo que vas a hacer, no cierres
17
+ resumiendo lo que acabás de decir.
18
+ - **Un tema por párrafo, tres líneas como máximo.** Si un párrafo crece, partilo o pasalo a
19
+ lista.
20
+ - **Cortá cuando terminaste.** Nada de relleno, cierres corteses ni ofertas genéricas de
21
+ ayuda.
22
+ - **No enumeres lo que descartaste.** Si evaluaste opciones y elegiste, decí la elegida y el
23
+ porqué en una línea.
24
+
25
+ ## Estructura
26
+
27
+ - **Tabla siempre que compares.** Dos o más cosas con dos o más atributos (opciones,
28
+ versiones, proyectos, trade-offs) van en tabla, no en prosa ni en bullets paralelos. Una
29
+ columna por atributo, una fila por cosa.
30
+ - **Listas para pasos y hallazgos.** Prosa solo para el razonamiento que une las cosas.
31
+ - **Títulos cuando hay más de un tema.** Que se pueda saltar a la parte que importa sin
32
+ leer todo.
33
+ - **`backticks` en todo identificador** — archivos, comandos, ramas, funciones, flags.
34
+ - **Bold para lo importante**, no italics: es más fácil de decodificar. Nunca MAYÚSCULAS
35
+ para enfatizar, deforman la silueta de la palabra.
36
+
37
+ ## Lenguaje
38
+
39
+ - **Literal.** Sin ironía, sarcasmo, modismos, metáforas ni preguntas retóricas. Si algo
40
+ está mal, decilo derecho.
41
+ - **Un término por concepto, siempre el mismo.** No alternes sinónimos para la misma cosa:
42
+ si es `worktree`, es `worktree` en toda la respuesta.
43
+ - **Oraciones cortas, una idea cada una.** Preferí punto sobre subordinada.
44
+ - **Voz activa y sujeto explícito.** Decí quién hace qué.
45
+ - **La incertidumbre se declara, no se insinúa.** "No sé si X" o "no lo verifiqué", en vez
46
+ de prosa condicional que deja al lector adivinando el nivel de confianza.
47
+ - **Decí explícitamente qué esperás del otro.** Si hace falta una decisión, que se vea que
48
+ hace falta y cuáles son las alternativas.
49
+
50
+ ## Números y datos
51
+
52
+ - **El dato antes que el adjetivo.** "62 commits atrás" en vez de "bastante atrasado".
53
+ - **Unidad y fecha completas.** `2026-09-04`, no "ayer"; `312 KB`, no "poco".
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-code-arcane",
3
- "version": "2.8.0",
3
+ "version": "2.9.2",
4
4
  "description": "Skills, agents, hooks and rules for Claude Code — installable via npx claude-code-arcane",
5
5
  "type": "module",
6
6
  "bin": {
@@ -15,6 +15,7 @@
15
15
  "rules/",
16
16
  "templates/",
17
17
  "docs/",
18
+ "output-styles/",
18
19
  "CHANGELOG.md"
19
20
  ],
20
21
  "scripts": {
@@ -32,9 +32,13 @@ rules:
32
32
  - prototype-code
33
33
  - test-standards
34
34
  - implementation-workflow
35
+ - code-comments
35
36
  gamedev:
36
37
  []
37
38
 
39
+ output_styles:
40
+ - arcane
41
+
38
42
  agents:
39
43
  - quality
40
44
 
@@ -0,0 +1,63 @@
1
+ ---
2
+ paths:
3
+ - "**/*.cs"
4
+ - "**/*.ts"
5
+ - "**/*.tsx"
6
+ - "**/*.js"
7
+ - "**/*.jsx"
8
+ - "**/*.py"
9
+ - "**/*.go"
10
+ - "**/*.java"
11
+ - "**/*.kt"
12
+ - "**/*.swift"
13
+ - "**/*.rb"
14
+ - "**/*.rs"
15
+ - "**/*.php"
16
+ - "**/*.dart"
17
+ - "**/*.c"
18
+ - "**/*.cpp"
19
+ - "**/*.cc"
20
+ - "**/*.h"
21
+ - "**/*.hpp"
22
+ - "**/*.sh"
23
+ - "**/*.sql"
24
+ - "**/*.shader"
25
+ - "**/*.hlsl"
26
+ - "**/*.glsl"
27
+ - "**/*.gd"
28
+ - "**/*.lua"
29
+ ---
30
+
31
+ # Code Comments Rule
32
+
33
+ Comments are written in **English**, whatever language the conversation is in.
34
+
35
+ ## Never write
36
+
37
+ - **Session narration.** No comment records what was decided, changed, tried or removed
38
+ while working. Not `// removed the old validation`, not `// now uses a dictionary instead
39
+ of a list`, not `// moved above the null check as discussed`, not `// changed from 30 to
40
+ 50`. The commit message carries the story; the code carries the result.
41
+ - **WHAT restated.** The code already says what it does. `// increment the counter` above
42
+ `counter++` is noise.
43
+ - **Ticket, task or doc references.** No `// added for JIRA-123`, no `// part of sprint04`,
44
+ no `// implements the boss design doc`. Traceability lives in the commit and the PR, where
45
+ it stays accurate and searchable.
46
+ - **Attribution or dates.** No `// author:`, no `// created 2026-09-04`. Git owns that and
47
+ never goes stale.
48
+ - **Multi-paragraph docstrings on simple functions.**
49
+ - **`// used by X`.** Tooling answers that better and this rots the first time X moves.
50
+
51
+ ## Write
52
+
53
+ - **The WHY that the code cannot show.** A constant that looks arbitrary, an ordering that
54
+ matters, a deliberate break from the surrounding pattern.
55
+ - **Workarounds**, with a link to the upstream bug.
56
+ - **Non-obvious invariants or preconditions** a caller has to respect.
57
+ - **Doc comments on public APIs** consumed from outside the module. Private ones need none.
58
+ - **`TODO(@owner, YYYY-MM-DD): description`.** Never a bare `TODO`.
59
+
60
+ ## Shape
61
+
62
+ One line, unless the reason honestly needs two. A comment that needs a paragraph to explain
63
+ the code beneath it is a signal to fix the code instead.
@@ -14,7 +14,6 @@ paths:
14
14
  - Voice budget is a hard limit: define max simultaneous voices per category and drop by priority, never let it grow unbounded
15
15
  - Audio must degrade silently — a missing bank or failed event NEVER throws into the gameplay frame
16
16
  - Keep the audio update off the render-critical path; audio work budget is 5-10% of frame time
17
- - Reference the audio bible section each event implements in code comments
18
17
 
19
18
  ## Examples
20
19
 
@@ -11,7 +11,6 @@ paths:
11
11
  - Every gameplay system must implement a clear interface
12
12
  - State machines must have explicit transition tables with documented states
13
13
  - Write unit tests for all gameplay logic — separate logic from presentation
14
- - Document which design doc each feature implements in code comments
15
14
  - No static singletons for game state — use dependency injection
16
15
 
17
16
  ## Examples
@@ -20,7 +20,6 @@ visual quality, performance, and cross-platform compatibility.
20
20
  - Group related parameters (Unity: `[Header]`, Unreal: Category)
21
21
  - Comment non-obvious calculations (especially math-heavy sections)
22
22
  - No magic numbers — use named constants or documented uniform values
23
- - Include authorship and purpose comment at the top of each shader file
24
23
 
25
24
  ## Performance Requirements
26
25
  - Document the target platform and complexity budget for each shader