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 +34 -0
- package/README.md +2 -2
- package/agents/game/gameplay-programmer.md +0 -1
- package/dist/cli.js +228 -29
- package/docs/USER-GUIDE.md +17 -1
- package/hooks/statusline.sh +17 -4
- package/output-styles/arcane.md +53 -0
- package/package.json +2 -1
- package/profiles/core.yaml +4 -0
- package/rules/code-comments.md +63 -0
- package/rules/gamedev/audio-code.md +0 -1
- package/rules/gamedev/gameplay-code.md +0 -1
- package/rules/gamedev/shader-code.md +0 -1
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
|
|
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/ #
|
|
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(
|
|
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:
|
|
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(
|
|
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
|
|
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 (
|
|
2655
|
+
if (foreign.length > 0) {
|
|
2537
2656
|
console.log(`
|
|
2538
|
-
${
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
2794
|
+
function quarantineItem(item, claudeDir) {
|
|
2591
2795
|
const installed = getInstalledPath(item, claudeDir);
|
|
2592
2796
|
if (!fs14.existsSync(installed)) return;
|
|
2593
|
-
const
|
|
2594
|
-
|
|
2595
|
-
|
|
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) {
|
package/docs/USER-GUIDE.md
CHANGED
|
@@ -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 **
|
|
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
|
package/hooks/statusline.sh
CHANGED
|
@@ -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
|
-
|
|
|
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
|
-
|
|
|
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
|
-
|
|
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.
|
|
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": {
|
package/profiles/core.yaml
CHANGED
|
@@ -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
|