nccgs 1.0.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.
Files changed (278) hide show
  1. package/.claude/agents/nccgs-accessibility-specialist.md +26 -0
  2. package/.claude/agents/nccgs-adversarial-reviewer.md +26 -0
  3. package/.claude/agents/nccgs-ai-programmer.md +26 -0
  4. package/.claude/agents/nccgs-analytics-engineer.md +26 -0
  5. package/.claude/agents/nccgs-art-direction-lead.md +26 -0
  6. package/.claude/agents/nccgs-audio-direction-lead.md +26 -0
  7. package/.claude/agents/nccgs-creative-director.md +26 -0
  8. package/.claude/agents/nccgs-documentation-manager.md +26 -0
  9. package/.claude/agents/nccgs-economy-designer.md +26 -0
  10. package/.claude/agents/nccgs-engine-programmer.md +26 -0
  11. package/.claude/agents/nccgs-game-design-lead.md +26 -0
  12. package/.claude/agents/nccgs-game-designer.md +26 -0
  13. package/.claude/agents/nccgs-gameplay-programmer.md +26 -0
  14. package/.claude/agents/nccgs-level-designer.md +26 -0
  15. package/.claude/agents/nccgs-live-ops-designer.md +26 -0
  16. package/.claude/agents/nccgs-localization-lead.md +26 -0
  17. package/.claude/agents/nccgs-narrative-lead.md +26 -0
  18. package/.claude/agents/nccgs-network-programmer.md +26 -0
  19. package/.claude/agents/nccgs-performance-analyst.md +26 -0
  20. package/.claude/agents/nccgs-producer.md +26 -0
  21. package/.claude/agents/nccgs-production-coordinator.md +23 -0
  22. package/.claude/agents/nccgs-programming-lead.md +26 -0
  23. package/.claude/agents/nccgs-prototyper.md +26 -0
  24. package/.claude/agents/nccgs-qa-engineer.md +26 -0
  25. package/.claude/agents/nccgs-qa-lead.md +26 -0
  26. package/.claude/agents/nccgs-release-engineer.md +26 -0
  27. package/.claude/agents/nccgs-release-lead.md +26 -0
  28. package/.claude/agents/nccgs-security-engineer.md +26 -0
  29. package/.claude/agents/nccgs-sound-designer.md +26 -0
  30. package/.claude/agents/nccgs-systems-designer.md +26 -0
  31. package/.claude/agents/nccgs-technical-architect.md +26 -0
  32. package/.claude/agents/nccgs-technical-artist.md +26 -0
  33. package/.claude/agents/nccgs-technical-director.md +26 -0
  34. package/.claude/agents/nccgs-tools-programmer.md +26 -0
  35. package/.claude/agents/nccgs-ui-programmer.md +26 -0
  36. package/.claude/agents/nccgs-unity-build-specialist.md +26 -0
  37. package/.claude/agents/nccgs-unity-content-specialist.md +26 -0
  38. package/.claude/agents/nccgs-unity-implementer.md +26 -0
  39. package/.claude/agents/nccgs-unity-rendering-specialist.md +26 -0
  40. package/.claude/agents/nccgs-unity-systems-specialist.md +26 -0
  41. package/.claude/agents/nccgs-unity-ui-specialist.md +26 -0
  42. package/.claude/agents/nccgs-ux-designer.md +26 -0
  43. package/.claude/agents/nccgs-verification-engineer.md +26 -0
  44. package/.claude/agents/nccgs-world-builder.md +26 -0
  45. package/.claude/agents/nccgs-writer.md +26 -0
  46. package/.claude/nccgs/THIRD_PARTY_NOTICES.md +13 -0
  47. package/.claude/nccgs/VERSION +1 -0
  48. package/.claude/nccgs/constitution.md +131 -0
  49. package/.claude/nccgs/hooks/agent-audit.mjs +10 -0
  50. package/.claude/nccgs/hooks/common.mjs +41 -0
  51. package/.claude/nccgs/hooks/post-compact.mjs +1 -0
  52. package/.claude/nccgs/hooks/pre-compact.mjs +8 -0
  53. package/.claude/nccgs/hooks/protect-git.mjs +22 -0
  54. package/.claude/nccgs/hooks/protect-write.mjs +17 -0
  55. package/.claude/nccgs/hooks/session-start.mjs +15 -0
  56. package/.claude/nccgs/hooks/session-stop.mjs +7 -0
  57. package/.claude/nccgs/protocols/agent-contract.md +34 -0
  58. package/.claude/nccgs/protocols/context-packets.md +13 -0
  59. package/.claude/nccgs/protocols/evidence.md +15 -0
  60. package/.claude/nccgs/protocols/model-routing.md +22 -0
  61. package/.claude/nccgs/protocols/orchestration.md +26 -0
  62. package/.claude/nccgs/protocols/unity-boundary.md +11 -0
  63. package/.claude/nccgs/settings.fragment.json +77 -0
  64. package/.claude/nccgs/studio.json +347 -0
  65. package/.claude/nccgs/tools/configure-models.mjs +39 -0
  66. package/.claude/nccgs/unity-skills-manifest.json +727 -0
  67. package/.claude/nccgs/workflow-catalog.json +317 -0
  68. package/.claude/rules/nccgs-canonical-docs.md +19 -0
  69. package/.claude/rules/nccgs-editor-tools.md +12 -0
  70. package/.claude/rules/nccgs-localization.md +12 -0
  71. package/.claude/rules/nccgs-networking.md +13 -0
  72. package/.claude/rules/nccgs-performance.md +14 -0
  73. package/.claude/rules/nccgs-rendering.md +16 -0
  74. package/.claude/rules/nccgs-security.md +13 -0
  75. package/.claude/rules/nccgs-tests.md +18 -0
  76. package/.claude/rules/nccgs-ui.md +14 -0
  77. package/.claude/rules/nccgs-unity-assets.md +21 -0
  78. package/.claude/rules/nccgs-unity-code.md +22 -0
  79. package/.claude/skills/accessibility-review/SKILL.md +14 -0
  80. package/.claude/skills/architecture-decision/SKILL.md +14 -0
  81. package/.claude/skills/asset-audit/SKILL.md +14 -0
  82. package/.claude/skills/audit/SKILL.md +16 -0
  83. package/.claude/skills/audit/references/dimensions.md +46 -0
  84. package/.claude/skills/balance-review/SKILL.md +14 -0
  85. package/.claude/skills/bug-triage/SKILL.md +14 -0
  86. package/.claude/skills/build-live-game/SKILL.md +317 -0
  87. package/.claude/skills/build-live-game/references/achievements.md +779 -0
  88. package/.claude/skills/build-live-game/references/apis.md +280 -0
  89. package/.claude/skills/build-live-game/references/authentication.md +437 -0
  90. package/.claude/skills/build-live-game/references/battlepass.md +860 -0
  91. package/.claude/skills/build-live-game/references/cloud-code.md +563 -0
  92. package/.claude/skills/build-live-game/references/cloud-save.md +474 -0
  93. package/.claude/skills/build-live-game/references/deployment.md +216 -0
  94. package/.claude/skills/build-live-game/references/player-account.md +813 -0
  95. package/.claude/skills/build-live-game/references/remote-config.md +96 -0
  96. package/.claude/skills/build-live-game/references/tooling.md +431 -0
  97. package/.claude/skills/closure/SKILL.md +14 -0
  98. package/.claude/skills/code-review/SKILL.md +14 -0
  99. package/.claude/skills/compatibility-review/SKILL.md +14 -0
  100. package/.claude/skills/context-pack/SKILL.md +14 -0
  101. package/.claude/skills/dependency-review/SKILL.md +14 -0
  102. package/.claude/skills/design/SKILL.md +20 -0
  103. package/.claude/skills/design-review/SKILL.md +14 -0
  104. package/.claude/skills/evidence-review/SKILL.md +14 -0
  105. package/.claude/skills/hotfix/SKILL.md +14 -0
  106. package/.claude/skills/implement-in-app-purchases/README.md +233 -0
  107. package/.claude/skills/implement-in-app-purchases/SKILL.md +158 -0
  108. package/.claude/skills/implement-in-app-purchases/references/api-notes.md +562 -0
  109. package/.claude/skills/implement-in-app-purchases/references/codeless-catalog.md +331 -0
  110. package/.claude/skills/implement-in-app-purchases/references/convert-adapty.md +268 -0
  111. package/.claude/skills/implement-in-app-purchases/references/convert-essentialkit.md +239 -0
  112. package/.claude/skills/implement-in-app-purchases/references/convert-revenuecat.md +275 -0
  113. package/.claude/skills/implement-in-app-purchases/references/convert-unipay.md +145 -0
  114. package/.claude/skills/implement-in-app-purchases/references/migration-v4-to-v5.md +204 -0
  115. package/.claude/skills/implement-in-app-purchases/references/path-add-iap-to-new-project.md +198 -0
  116. package/.claude/skills/implement-in-app-purchases/references/path-convert-native-google-billing.md +356 -0
  117. package/.claude/skills/implement-in-app-purchases/references/path-convert-native-storekit.md +446 -0
  118. package/.claude/skills/implement-in-app-purchases/references/path-implement-iap-d2c.md +680 -0
  119. package/.claude/skills/implement-in-app-purchases/references/platform-notes.md +204 -0
  120. package/.claude/skills/implement-in-app-purchases/references/pre-check.md +205 -0
  121. package/.claude/skills/incident-recovery/SKILL.md +14 -0
  122. package/.claude/skills/initialize-ai-navigation/SKILL.md +95 -0
  123. package/.claude/skills/initialize-ai-navigation/references/navigation-system.md +794 -0
  124. package/.claude/skills/levelplay-unity-integration/CHANGELOG.md +57 -0
  125. package/.claude/skills/levelplay-unity-integration/README.md +74 -0
  126. package/.claude/skills/levelplay-unity-integration/SKILL.md +1126 -0
  127. package/.claude/skills/levelplay-unity-integration/references/banner-api.md +920 -0
  128. package/.claude/skills/levelplay-unity-integration/references/best-practices.md +536 -0
  129. package/.claude/skills/levelplay-unity-integration/references/ilrd-api.md +337 -0
  130. package/.claude/skills/levelplay-unity-integration/references/initialization-api.md +630 -0
  131. package/.claude/skills/levelplay-unity-integration/references/interstitial-api.md +899 -0
  132. package/.claude/skills/levelplay-unity-integration/references/ios-setup.md +491 -0
  133. package/.claude/skills/levelplay-unity-integration/references/migration-sdk-9.md +666 -0
  134. package/.claude/skills/levelplay-unity-integration/references/privacy-settings.md +608 -0
  135. package/.claude/skills/levelplay-unity-integration/references/rewarded-api.md +902 -0
  136. package/.claude/skills/localization/SKILL.md +135 -0
  137. package/.claude/skills/localization/references/api-notes.md +76 -0
  138. package/.claude/skills/localization/resources/L10nBatchProcessor.cs +69 -0
  139. package/.claude/skills/localization/resources/LocalizedFontAsset.cs +18 -0
  140. package/.claude/skills/localize-game/SKILL.md +14 -0
  141. package/.claude/skills/migrate-project/SKILL.md +21 -0
  142. package/.claude/skills/migrate-project/references/procedure.md +63 -0
  143. package/.claude/skills/milestone-review/SKILL.md +14 -0
  144. package/.claude/skills/new-unity-project/SKILL.md +179 -0
  145. package/.claude/skills/optimize-audio/SKILL.md +199 -0
  146. package/.claude/skills/optimize-audio/resources/audio-import-api.md +146 -0
  147. package/.claude/skills/optimize-audio/resources/platform-settings.md +48 -0
  148. package/.claude/skills/optimize-text-mesh-pro/SKILL.md +182 -0
  149. package/.claude/skills/optimize-web/SKILL.md +393 -0
  150. package/.claude/skills/optimize-web/resources/WebOptimizer.cs +21 -0
  151. package/.claude/skills/optimize-web/resources/toktx-examples.sh +11 -0
  152. package/.claude/skills/performance-audit/SKILL.md +14 -0
  153. package/.claude/skills/physics-3d-collision/SKILL.md +442 -0
  154. package/.claude/skills/physics-3d-collision/references/troubleshooting.md +41 -0
  155. package/.claude/skills/physics-3d-collision/resources/CollisionDebugger.cs +33 -0
  156. package/.claude/skills/plan-feature/SKILL.md +14 -0
  157. package/.claude/skills/playtest/SKILL.md +14 -0
  158. package/.claude/skills/project-stage/SKILL.md +14 -0
  159. package/.claude/skills/prototype-feature/SKILL.md +14 -0
  160. package/.claude/skills/qa-plan/SKILL.md +14 -0
  161. package/.claude/skills/release/SKILL.md +18 -0
  162. package/.claude/skills/release-readiness/SKILL.md +14 -0
  163. package/.claude/skills/retrospective/SKILL.md +14 -0
  164. package/.claude/skills/review/SKILL.md +16 -0
  165. package/.claude/skills/security-audit/SKILL.md +14 -0
  166. package/.claude/skills/setup-multiplayer-services/SKILL.md +39 -0
  167. package/.claude/skills/setup-multiplayer-services/references/dgs-entrypoint.md +79 -0
  168. package/.claude/skills/setup-multiplayer-services/references/entrypoints.md +213 -0
  169. package/.claude/skills/setup-multiplayer-services/references/examples.md +33 -0
  170. package/.claude/skills/setup-multiplayer-services/references/implementation-fit.md +30 -0
  171. package/.claude/skills/setup-multiplayer-services/references/underlying-services.md +11 -0
  172. package/.claude/skills/setup-multiplayer-services/references/workflows-prerequisites.md +17 -0
  173. package/.claude/skills/setup-vivox-voice-chat/SKILL.md +118 -0
  174. package/.claude/skills/setup-vivox-voice-chat/evals/.env.example +6 -0
  175. package/.claude/skills/setup-vivox-voice-chat/evals/README.md +101 -0
  176. package/.claude/skills/setup-vivox-voice-chat/evals/promptfooconfig.yaml +32 -0
  177. package/.claude/skills/setup-vivox-voice-chat/evals/tests/init-and-login.yaml +76 -0
  178. package/.claude/skills/setup-vivox-voice-chat/evals/tests/text-chat.yaml +65 -0
  179. package/.claude/skills/setup-vivox-voice-chat/evals/tests/voice-channels.yaml +74 -0
  180. package/.claude/skills/setup-vivox-voice-chat/references/events-and-participants.md +79 -0
  181. package/.claude/skills/setup-vivox-voice-chat/references/init-and-login.md +92 -0
  182. package/.claude/skills/setup-vivox-voice-chat/references/text-chat.md +93 -0
  183. package/.claude/skills/setup-vivox-voice-chat/references/troubleshooting.md +47 -0
  184. package/.claude/skills/setup-vivox-voice-chat/references/voice-channels.md +90 -0
  185. package/.claude/skills/shader-graph-create-custom-node/SKILL.md +25 -0
  186. package/.claude/skills/shader-graph-create-custom-node/resources/all_hints.hlsl +182 -0
  187. package/.claude/skills/sprint-plan/SKILL.md +14 -0
  188. package/.claude/skills/sprite-editor/SKILL.md +66 -0
  189. package/.claude/skills/sprite-editor/references/api_reference.md +151 -0
  190. package/.claude/skills/sprite-editor/references/background.md +112 -0
  191. package/.claude/skills/sprite-editor/references/templates.md +72 -0
  192. package/.claude/skills/sprite-editor/scripts/AutomaticSliceTexture.cs +40 -0
  193. package/.claude/skills/sprite-editor/scripts/GenerateNewSpriteRects.cs +200 -0
  194. package/.claude/skills/sprite-editor/scripts/GetTextureSourceImageSize.cs +32 -0
  195. package/.claude/skills/sprite-editor/scripts/GetTextureToSlice.cs +55 -0
  196. package/.claude/skills/sprite-editor/scripts/GridSliceTexture.cs +40 -0
  197. package/.claude/skills/sprite-editor/scripts/IsometricSliceTexture.cs +141 -0
  198. package/.claude/skills/sprite-editor/scripts/README.md +134 -0
  199. package/.claude/skills/sprite-editor/scripts/SetPivotExample.cs +58 -0
  200. package/.claude/skills/sprite-editor/scripts/SpriteToPng.cs +88 -0
  201. package/.claude/skills/status/SKILL.md +16 -0
  202. package/.claude/skills/story-readiness/SKILL.md +14 -0
  203. package/.claude/skills/test/SKILL.md +16 -0
  204. package/.claude/skills/ui/SKILL.md +142 -0
  205. package/.claude/skills/ui-imgui/SKILL.md +186 -0
  206. package/.claude/skills/ui-imgui/references/gui-elements.md +156 -0
  207. package/.claude/skills/ui-imgui/references/templates.md +141 -0
  208. package/.claude/skills/ui-review/SKILL.md +14 -0
  209. package/.claude/skills/ui-ugui/SKILL.md +282 -0
  210. package/.claude/skills/ui-ugui/references/scrollview-setup.md +29 -0
  211. package/.claude/skills/ui-uitk/SKILL.md +235 -0
  212. package/.claude/skills/ui-uitk/references/common-issues.md +74 -0
  213. package/.claude/skills/ui-uitk/references/custom-elements.md +241 -0
  214. package/.claude/skills/ui-uitk/references/painter2d.md +282 -0
  215. package/.claude/skills/ui-uitk/references/pointermanipulator-guide.md +94 -0
  216. package/.claude/skills/ui-uitk/references/svg-icons.md +136 -0
  217. package/.claude/skills/ui-uitk/references/ui-runtime-binding.md +234 -0
  218. package/.claude/skills/ui-uitk/references/uss-guide.md +138 -0
  219. package/.claude/skills/unity-cli/CHANGELOG.md +233 -0
  220. package/.claude/skills/unity-cli/SECURITY.md +22 -0
  221. package/.claude/skills/unity-cli/SKILL.md +414 -0
  222. package/.claude/skills/unity-cli/references/auth-license-cloud.md +146 -0
  223. package/.claude/skills/unity-cli/references/build-run-test.md +349 -0
  224. package/.claude/skills/unity-cli/references/collaboration.md +472 -0
  225. package/.claude/skills/unity-cli/references/config-hub.md +103 -0
  226. package/.claude/skills/unity-cli/references/diagnostics-maintenance.md +326 -0
  227. package/.claude/skills/unity-cli/references/editors-install.md +327 -0
  228. package/.claude/skills/unity-cli/references/integration-advanced.md +472 -0
  229. package/.claude/skills/unity-cli/references/projects-templates.md +574 -0
  230. package/.claude/skills/unity-package-management/SKILL.md +304 -0
  231. package/.claude/skills/unity-package-management/references/select-packages.md +108 -0
  232. package/.claude/skills/urp-postprocessing/SKILL.md +188 -0
  233. package/.claude/skills/urp-postprocessing/references/code-templates.md +119 -0
  234. package/.claude/skills/urp-postprocessing/references/effect-reference.md +86 -0
  235. package/.claude/skills/validate-urp-render-graph-renderer-feature/SKILL.md +269 -0
  236. package/.claude/skills/work/SKILL.md +36 -0
  237. package/.claude/skills/work/references/classification.md +41 -0
  238. package/.claude/skills/work/references/closure.md +50 -0
  239. package/.claude/skills/work/references/feature-contracts.md +32 -0
  240. package/.claude/skills/work/references/verification.md +26 -0
  241. package/CLAUDE.md +6 -0
  242. package/LICENSE +21 -0
  243. package/README.md +162 -0
  244. package/THIRD_PARTY_NOTICES.md +23 -0
  245. package/UPGRADING.md +32 -0
  246. package/VERSION +1 -0
  247. package/docs/ARCHITECTURE.md +73 -0
  248. package/docs/HUONG-DAN-MIGRATE-VA-SU-DUNG.md +324 -0
  249. package/docs/MIGRATION-MATRIX.md +23 -0
  250. package/docs/PROJECT-POLICY.md +67 -0
  251. package/docs/WORKFLOWS.md +61 -0
  252. package/package-assets/setup-vivox-voice-chat-evals.gitignore +7 -0
  253. package/package.json +41 -0
  254. package/scaffold/.nccgs/bugs/.gitkeep +1 -0
  255. package/scaffold/.nccgs/closures/.gitkeep +1 -0
  256. package/scaffold/.nccgs/context/.gitkeep +1 -0
  257. package/scaffold/.nccgs/decisions/.gitkeep +1 -0
  258. package/scaffold/.nccgs/evidence/.gitkeep +1 -0
  259. package/scaffold/.nccgs/features/.gitkeep +1 -0
  260. package/scaffold/.nccgs/migrations/.gitkeep +1 -0
  261. package/scaffold/.nccgs/playtests/.gitkeep +1 -0
  262. package/scaffold/.nccgs/project.yaml +103 -0
  263. package/scaffold/.nccgs/requirements.yaml +10 -0
  264. package/scaffold/.nccgs/reviews/.gitkeep +1 -0
  265. package/scaffold/.nccgs/state.md +40 -0
  266. package/scaffold/.nccgs/templates/agent-handoff.md +25 -0
  267. package/scaffold/.nccgs/templates/architecture-decision.md +27 -0
  268. package/scaffold/.nccgs/templates/closure-record.md +51 -0
  269. package/scaffold/.nccgs/templates/context-packet.yaml +17 -0
  270. package/scaffold/.nccgs/templates/evidence-record.md +23 -0
  271. package/scaffold/.nccgs/templates/feature-contract.md +35 -0
  272. package/scaffold/.nccgs/templates/migration-plan.md +40 -0
  273. package/scaffold/.nccgs/templates/waiver.md +11 -0
  274. package/scripts/cli.mjs +56 -0
  275. package/scripts/install.mjs +267 -0
  276. package/scripts/sync-unity-skills.mjs +126 -0
  277. package/scripts/validate.mjs +205 -0
  278. package/tests/framework.test.mjs +121 -0
@@ -0,0 +1,267 @@
1
+ import { spawnSync } from "node:child_process";
2
+ import crypto from "node:crypto";
3
+ import fs from "node:fs";
4
+ import path from "node:path";
5
+ import { fileURLToPath } from "node:url";
6
+
7
+ const scriptDir = path.dirname(fileURLToPath(import.meta.url));
8
+ const root = path.resolve(scriptDir, "..");
9
+ const args = process.argv.slice(2);
10
+ const projectIndex = args.indexOf("--project");
11
+ const dryRun = args.includes("--dry-run");
12
+ const force = args.includes("--force");
13
+ const allowNonUnity = args.includes("--allow-non-unity");
14
+ const modelProfileIndex = args.indexOf("--model-profile");
15
+ const requestedModelProfile = modelProfileIndex >= 0 ? args[modelProfileIndex + 1] : null;
16
+
17
+ if (projectIndex >= 0 && !args[projectIndex + 1]) {
18
+ console.error("--project requires a directory path.");
19
+ process.exit(2);
20
+ }
21
+
22
+ const project = projectIndex >= 0 ? path.resolve(args[projectIndex + 1]) : process.cwd();
23
+ if (!fs.existsSync(project) || !fs.statSync(project).isDirectory()) {
24
+ console.error(`Project directory does not exist: ${project}`);
25
+ process.exit(2);
26
+ }
27
+ if (project === root) {
28
+ console.error("The NCCGS package directory cannot be its own installation target.");
29
+ process.exit(2);
30
+ }
31
+
32
+ const unityMarkers = ["Assets", "Packages", "ProjectSettings"];
33
+ const missingMarkers = unityMarkers.filter((item) => !fs.existsSync(path.join(project, item)));
34
+ if (missingMarkers.length && !allowNonUnity) {
35
+ console.error(`Target does not look like a Unity project; missing: ${missingMarkers.join(", ")}`);
36
+ console.error("Use --allow-non-unity only when intentionally scaffolding before Unity project creation.");
37
+ process.exit(2);
38
+ }
39
+
40
+ const validation = spawnSync(process.execPath, [path.join(scriptDir, "validate.mjs")], {
41
+ cwd: root,
42
+ encoding: "utf8",
43
+ });
44
+ if (validation.status !== 0) {
45
+ process.stderr.write(validation.stdout ?? "");
46
+ process.stderr.write(validation.stderr ?? "");
47
+ console.error("NCCGS package validation failed; installation aborted.");
48
+ process.exit(1);
49
+ }
50
+
51
+ const frameworkSource = path.join(root, ".claude");
52
+ const frameworkFiles = walk(frameworkSource).map((file) => ({
53
+ source: file,
54
+ relative: path.relative(root, file),
55
+ destination: path.join(project, path.relative(root, file)),
56
+ }));
57
+ const npmExcludedUnityFile = ".claude/skills/setup-vivox-voice-chat/evals/.gitignore";
58
+ if (!frameworkFiles.some((item) => posix(item.relative) === npmExcludedUnityFile)) {
59
+ const fallback = path.join(root, "package-assets", "setup-vivox-voice-chat-evals.gitignore");
60
+ if (fs.existsSync(fallback)) {
61
+ frameworkFiles.push({
62
+ source: fallback,
63
+ relative: npmExcludedUnityFile.split("/").join(path.sep),
64
+ destination: path.join(project, ...npmExcludedUnityFile.split("/"))
65
+ });
66
+ frameworkFiles.sort((a, b) => a.relative.localeCompare(b.relative));
67
+ }
68
+ }
69
+ const installStatePath = path.join(project, ".claude", "nccgs", "install-state.json");
70
+ let previousInstallState = null;
71
+ if (fs.existsSync(installStatePath)) {
72
+ try { previousInstallState = JSON.parse(fs.readFileSync(installStatePath, "utf8")); }
73
+ catch (error) {
74
+ console.error(`Invalid NCCGS install state at ${installStatePath}: ${error.message}`);
75
+ process.exit(2);
76
+ }
77
+ }
78
+ const currentFrameworkNames = new Set(frameworkFiles.map((item) => posix(item.relative)));
79
+ const staleFrameworkFiles = (previousInstallState?.files ?? [])
80
+ .filter((item) => !currentFrameworkNames.has(item.relative))
81
+ .map((item) => ({ ...item, destination: path.join(project, ...item.relative.split("/")) }))
82
+ .filter((item) => fs.existsSync(item.destination));
83
+
84
+ const conflicts = frameworkFiles.filter((item) => {
85
+ if (!fs.existsSync(item.destination)) return false;
86
+ return !sameFile(item.source, item.destination);
87
+ });
88
+
89
+ if ((conflicts.length || staleFrameworkFiles.length) && !force) {
90
+ console.error(`Installation found ${conflicts.length} conflicting and ${staleFrameworkFiles.length} stale framework file(s):`);
91
+ for (const item of conflicts.slice(0, 30)) console.error(`- ${posix(item.relative)}`);
92
+ for (const item of staleFrameworkFiles.slice(0, 30)) console.error(`- stale: ${item.relative}`);
93
+ if (conflicts.length > 30) console.error(`- ... ${conflicts.length - 30} more`);
94
+ console.error("Re-run with --force to back up and replace these framework files.");
95
+ process.exit(2);
96
+ }
97
+
98
+ const newFiles = frameworkFiles.filter((item) => !fs.existsSync(item.destination));
99
+ const identicalFiles = frameworkFiles.length - newFiles.length - conflicts.length;
100
+ const scaffoldFiles = walk(path.join(root, "scaffold")).map((file) => ({
101
+ source: file,
102
+ relative: path.relative(path.join(root, "scaffold"), file),
103
+ destination: path.join(project, path.relative(path.join(root, "scaffold"), file)),
104
+ }));
105
+ const newScaffold = scaffoldFiles.filter((item) => !fs.existsSync(item.destination));
106
+ const settingsPlan = planManagedSettings(project);
107
+ const targetPolicyPath = path.join(project, ".nccgs", "project.yaml");
108
+ const targetPolicy = fs.existsSync(targetPolicyPath) ? fs.readFileSync(targetPolicyPath, "utf8") : "";
109
+ const policyModelProfile = targetPolicy.match(/^models:\s*$[\s\S]*?^\s+profile:\s*([^#\r\n]+)/m)?.[1]?.trim();
110
+ const modelProfile = requestedModelProfile || policyModelProfile || "quality";
111
+ if (!new Set(["quality", "balanced", "inherit"]).has(modelProfile)) {
112
+ console.error(`Unknown model profile: ${modelProfile}`);
113
+ process.exit(2);
114
+ }
115
+
116
+ console.log(`NCCGS installation target: ${project}`);
117
+ console.log(`Framework files: ${frameworkFiles.length} (${newFiles.length} new, ${identicalFiles} identical, ${conflicts.length} replace)`);
118
+ console.log(`Stale framework files to back up and remove: ${staleFrameworkFiles.length}`);
119
+ console.log(`Project scaffold files to initialize: ${newScaffold.length}`);
120
+ console.log(`Managed Claude settings: ${settingsPlan.changed ? "update" : "already current"}`);
121
+ console.log(`Agent model profile: ${modelProfile}`);
122
+ if (dryRun) {
123
+ console.log("Dry run complete; no files changed.");
124
+ process.exit(0);
125
+ }
126
+
127
+ const stamp = new Date().toISOString().replace(/[:.]/g, "-");
128
+ const backupRoot = path.join(project, ".nccgs", "backups", stamp);
129
+ for (const item of conflicts) {
130
+ const backup = path.join(backupRoot, item.relative);
131
+ fs.mkdirSync(path.dirname(backup), { recursive: true });
132
+ fs.copyFileSync(item.destination, backup);
133
+ }
134
+ for (const item of staleFrameworkFiles) {
135
+ const backup = path.join(backupRoot, ...item.relative.split("/"));
136
+ fs.mkdirSync(path.dirname(backup), { recursive: true });
137
+ fs.copyFileSync(item.destination, backup);
138
+ fs.unlinkSync(item.destination);
139
+ }
140
+
141
+ for (const item of frameworkFiles) {
142
+ fs.mkdirSync(path.dirname(item.destination), { recursive: true });
143
+ fs.copyFileSync(item.source, item.destination);
144
+ }
145
+
146
+ for (const item of newScaffold) {
147
+ fs.mkdirSync(path.dirname(item.destination), { recursive: true });
148
+ fs.copyFileSync(item.source, item.destination);
149
+ }
150
+
151
+ installManagedSettings(settingsPlan, backupRoot);
152
+ installClaudeImport(project, backupRoot);
153
+
154
+ const modelConfiguration = spawnSync(process.execPath, [
155
+ path.join(project, ".claude", "nccgs", "tools", "configure-models.mjs"),
156
+ "--project", project,
157
+ "--profile", modelProfile,
158
+ ], { cwd: project, encoding: "utf8" });
159
+ if (modelConfiguration.status !== 0) {
160
+ process.stderr.write(modelConfiguration.stdout ?? "");
161
+ process.stderr.write(modelConfiguration.stderr ?? "");
162
+ console.error("NCCGS was copied, but agent model configuration failed.");
163
+ process.exit(1);
164
+ }
165
+
166
+ writeInstallState(project, modelProfile, frameworkFiles, installStatePath);
167
+
168
+ console.log("NCCGS installation complete.");
169
+ if (conflicts.length) console.log(`Replaced files backed up under: ${backupRoot}`);
170
+ console.log("Next: open Claude Code and run /migrate-project review for an existing project, or /status for a configured project.");
171
+
172
+ function walk(directory) {
173
+ const output = [];
174
+ for (const entry of fs.readdirSync(directory, { withFileTypes: true })) {
175
+ const full = path.join(directory, entry.name);
176
+ if (entry.isDirectory()) output.push(...walk(full));
177
+ else if (entry.isFile()) output.push(full);
178
+ }
179
+ return output.sort((a, b) => a.localeCompare(b));
180
+ }
181
+
182
+ function sameFile(left, right) {
183
+ const a = fs.readFileSync(left);
184
+ const b = fs.readFileSync(right);
185
+ return a.length === b.length && a.equals(b);
186
+ }
187
+
188
+ function hashFile(file) {
189
+ return crypto.createHash("sha256").update(fs.readFileSync(file)).digest("hex");
190
+ }
191
+
192
+ function posix(value) {
193
+ return value.split(path.sep).join("/");
194
+ }
195
+
196
+ function planManagedSettings(projectRoot) {
197
+ const settingsPath = path.join(projectRoot, ".claude", "settings.json");
198
+ let current = {};
199
+ if (fs.existsSync(settingsPath)) {
200
+ try {
201
+ current = JSON.parse(fs.readFileSync(settingsPath, "utf8"));
202
+ } catch (error) {
203
+ console.error(`Cannot merge invalid JSON settings at ${settingsPath}: ${error.message}`);
204
+ process.exit(2);
205
+ }
206
+ }
207
+ const fragment = JSON.parse(fs.readFileSync(path.join(root, ".claude", "nccgs", "settings.fragment.json"), "utf8"));
208
+ const merged = structuredClone(current);
209
+ merged.permissions ??= {};
210
+ for (const key of ["allow", "deny", "ask"]) {
211
+ const existing = Array.isArray(merged.permissions[key]) ? merged.permissions[key] : [];
212
+ const additions = Array.isArray(fragment.permissions?.[key]) ? fragment.permissions[key] : [];
213
+ if (existing.length || additions.length) merged.permissions[key] = [...new Set([...existing, ...additions])];
214
+ }
215
+ merged.hooks ??= {};
216
+ for (const [event, groups] of Object.entries(fragment.hooks ?? {})) {
217
+ const existing = Array.isArray(merged.hooks[event]) ? merged.hooks[event] : [];
218
+ const retained = existing.filter((group) => !JSON.stringify(group).includes(".claude/nccgs/hooks/"));
219
+ merged.hooks[event] = [...retained, ...groups];
220
+ }
221
+ const nextText = `${JSON.stringify(merged, null, 2)}\n`;
222
+ const currentText = fs.existsSync(settingsPath) ? fs.readFileSync(settingsPath, "utf8") : "";
223
+ return { settingsPath, existed: fs.existsSync(settingsPath), nextText, changed: currentText !== nextText };
224
+ }
225
+
226
+ function installManagedSettings(plan, backupDirectory) {
227
+ if (!plan.changed) return;
228
+ if (plan.existed) {
229
+ const backup = path.join(backupDirectory, ".claude", "settings.json");
230
+ fs.mkdirSync(path.dirname(backup), { recursive: true });
231
+ fs.copyFileSync(plan.settingsPath, backup);
232
+ }
233
+ fs.mkdirSync(path.dirname(plan.settingsPath), { recursive: true });
234
+ fs.writeFileSync(plan.settingsPath, plan.nextText, "utf8");
235
+ }
236
+
237
+ function writeInstallState(projectRoot, profile, files, statePath) {
238
+ const state = {
239
+ schemaVersion: 1,
240
+ frameworkVersion: fs.readFileSync(path.join(root, "VERSION"), "utf8").trim(),
241
+ modelProfile: profile,
242
+ files: files.map((item) => ({
243
+ relative: posix(item.relative),
244
+ sha256: hashFile(path.join(projectRoot, item.relative))
245
+ }))
246
+ };
247
+ fs.mkdirSync(path.dirname(statePath), { recursive: true });
248
+ fs.writeFileSync(statePath, `${JSON.stringify(state, null, 2)}\n`, "utf8");
249
+ }
250
+
251
+ function installClaudeImport(projectRoot, backupDirectory) {
252
+ const claudePath = path.join(projectRoot, "CLAUDE.md");
253
+ const begin = "<!-- NCCGS:BEGIN -->";
254
+ const block = `${begin}\n@.claude/nccgs/constitution.md\n<!-- NCCGS:END -->`;
255
+ if (!fs.existsSync(claudePath)) {
256
+ fs.copyFileSync(path.join(root, "CLAUDE.md"), claudePath);
257
+ return;
258
+ }
259
+
260
+ const current = fs.readFileSync(claudePath, "utf8");
261
+ if (current.includes(begin) || current.includes("@.claude/nccgs/constitution.md")) return;
262
+
263
+ fs.mkdirSync(backupDirectory, { recursive: true });
264
+ fs.copyFileSync(claudePath, path.join(backupDirectory, "CLAUDE.md"));
265
+ const separator = current.endsWith("\n") ? "\n" : "\n\n";
266
+ fs.writeFileSync(claudePath, `${current}${separator}${block}\n`, "utf8");
267
+ }
@@ -0,0 +1,126 @@
1
+ import crypto from "node:crypto";
2
+ import fs from "node:fs";
3
+ import os from "node:os";
4
+ import path from "node:path";
5
+ import { fileURLToPath } from "node:url";
6
+
7
+ const scriptDir = path.dirname(fileURLToPath(import.meta.url));
8
+ const root = path.resolve(scriptDir, "..");
9
+ const args = process.argv.slice(2);
10
+ const dryRun = args.includes("--dry-run");
11
+ const sourceIndex = args.indexOf("--source");
12
+
13
+ if (sourceIndex < 0 || !args[sourceIndex + 1]) {
14
+ console.error("Usage: node scripts/sync-unity-skills.mjs --source <UnitySkills directory> [--dry-run]");
15
+ process.exit(2);
16
+ }
17
+
18
+ const source = path.resolve(args[sourceIndex + 1]);
19
+ if (!fs.existsSync(source) || !fs.statSync(source).isDirectory()) {
20
+ console.error(`Unity Skills source directory does not exist: ${source}`);
21
+ process.exit(2);
22
+ }
23
+
24
+ const reserved = new Set(["work", "design", "audit", "migrate-project", "test", "review", "release", "status"]);
25
+ const skillNames = fs.readdirSync(source, { withFileTypes: true })
26
+ .filter((entry) => entry.isDirectory() && fs.existsSync(path.join(source, entry.name, "SKILL.md")))
27
+ .map((entry) => entry.name)
28
+ .sort((a, b) => a.localeCompare(b));
29
+
30
+ if (!skillNames.length) {
31
+ console.error(`No Unity Skills containing SKILL.md were found in: ${source}`);
32
+ process.exit(2);
33
+ }
34
+
35
+ for (const name of skillNames) {
36
+ if (reserved.has(name)) {
37
+ console.error(`Unity Skill collides with an NCCGS public skill: ${name}`);
38
+ process.exit(2);
39
+ }
40
+ }
41
+
42
+ const skillsRoot = path.join(root, ".claude", "skills");
43
+ const manifestPath = path.join(root, ".claude", "nccgs", "unity-skills-manifest.json");
44
+ let previous = { skills: [] };
45
+ if (fs.existsSync(manifestPath)) {
46
+ previous = JSON.parse(fs.readFileSync(manifestPath, "utf8"));
47
+ }
48
+ const previousNames = new Set((previous.skills ?? []).map((item) => item.name));
49
+
50
+ for (const name of skillNames) {
51
+ const destination = path.join(skillsRoot, name);
52
+ if (fs.existsSync(destination) && !previousNames.has(name)) {
53
+ console.error(`Destination collides with a non-vendored skill: .claude/skills/${name}`);
54
+ process.exit(2);
55
+ }
56
+ }
57
+
58
+ console.log(`${dryRun ? "Would sync" : "Syncing"} ${skillNames.length} Unity Skills from ${source}`);
59
+ for (const name of skillNames) console.log(`- ${name}`);
60
+ if (dryRun) process.exit(0);
61
+
62
+ fs.mkdirSync(skillsRoot, { recursive: true });
63
+ fs.mkdirSync(path.dirname(manifestPath), { recursive: true });
64
+
65
+ const stamp = new Date().toISOString().replace(/[:.]/g, "-");
66
+ const stage = fs.mkdtempSync(path.join(os.tmpdir(), "nccgs-unity-skills-"));
67
+ const backupRoot = path.join(root, ".nccgs-maintenance", "unity-skills-backups", stamp);
68
+
69
+ try {
70
+ for (const name of skillNames) {
71
+ fs.cpSync(path.join(source, name), path.join(stage, name), { recursive: true });
72
+ }
73
+
74
+ for (const name of new Set([...previousNames, ...skillNames])) {
75
+ const destination = path.join(skillsRoot, name);
76
+ if (!fs.existsSync(destination)) continue;
77
+ fs.mkdirSync(backupRoot, { recursive: true });
78
+ fs.renameSync(destination, path.join(backupRoot, name));
79
+ }
80
+
81
+ for (const name of skillNames) {
82
+ fs.cpSync(path.join(stage, name), path.join(skillsRoot, name), { recursive: true });
83
+ }
84
+
85
+ const manifest = {
86
+ schema_version: 1,
87
+ source_repository: "https://github.com/Unity-Technologies/skills",
88
+ source_label: path.basename(source),
89
+ source_revision: "unknown-local-copy",
90
+ synced_at: new Date().toISOString(),
91
+ license: "Unity Companion License for Unity-dependent projects",
92
+ license_url: "https://unity3d.com/legal/licenses/unity_companion_license",
93
+ policy: "Vendored files are upstream-managed and must remain unmodified.",
94
+ skills: skillNames.map((name) => ({
95
+ name,
96
+ files: walk(path.join(skillsRoot, name)).map((file) => ({
97
+ path: posix(path.relative(path.join(skillsRoot, name), file)),
98
+ bytes: fs.statSync(file).size,
99
+ sha256: crypto.createHash("sha256").update(fs.readFileSync(file)).digest("hex"),
100
+ })),
101
+ })),
102
+ };
103
+ fs.writeFileSync(manifestPath, `${JSON.stringify(manifest, null, 2)}\n`, "utf8");
104
+ console.log(`Unity Skills manifest written: ${manifestPath}`);
105
+ if (fs.existsSync(backupRoot)) console.log(`Previous snapshot backed up: ${backupRoot}`);
106
+ } catch (error) {
107
+ console.error(`Unity Skills sync failed: ${error.message}`);
108
+ console.error(`The package may be restored from: ${backupRoot}`);
109
+ process.exit(1);
110
+ } finally {
111
+ if (fs.existsSync(stage)) fs.rmSync(stage, { recursive: true, force: true });
112
+ }
113
+
114
+ function walk(directory) {
115
+ const output = [];
116
+ for (const entry of fs.readdirSync(directory, { withFileTypes: true })) {
117
+ const full = path.join(directory, entry.name);
118
+ if (entry.isDirectory()) output.push(...walk(full));
119
+ else if (entry.isFile()) output.push(full);
120
+ }
121
+ return output.sort((a, b) => a.localeCompare(b));
122
+ }
123
+
124
+ function posix(value) {
125
+ return value.split(path.sep).join("/");
126
+ }
@@ -0,0 +1,205 @@
1
+ import crypto from "node:crypto";
2
+ import fs from "node:fs";
3
+ import path from "node:path";
4
+ import { spawnSync } from "node:child_process";
5
+ import { fileURLToPath } from "node:url";
6
+
7
+ const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
8
+ const errors = [];
9
+ const warnings = [];
10
+ const posix = (value) => value.split(path.sep).join("/");
11
+
12
+ function walk(directory) {
13
+ if (!fs.existsSync(directory)) return [];
14
+ const output = [];
15
+ for (const entry of fs.readdirSync(directory, { withFileTypes: true })) {
16
+ const full = path.join(directory, entry.name);
17
+ if (entry.isDirectory()) output.push(...walk(full));
18
+ else if (entry.isFile()) output.push(full);
19
+ }
20
+ return output.sort((a, b) => a.localeCompare(b));
21
+ }
22
+
23
+ function sha256(file) {
24
+ return crypto.createHash("sha256").update(fs.readFileSync(file)).digest("hex");
25
+ }
26
+
27
+ function frontmatter(file) {
28
+ const text = fs.readFileSync(file, "utf8");
29
+ const match = text.match(/^---\r?\n([\s\S]*?)\r?\n---(?:\r?\n|$)/);
30
+ if (!match) return null;
31
+ const result = {};
32
+ for (const line of match[1].split(/\r?\n/)) {
33
+ const item = line.match(/^([A-Za-z0-9_-]+):\s*(.*)$/);
34
+ if (item) result[item[1]] = item[2].replace(/^['"]|['"]$/g, "").trim();
35
+ }
36
+ return result;
37
+ }
38
+
39
+ function requireFile(relative) {
40
+ if (!fs.existsSync(path.join(root, relative))) errors.push(`Missing required file: ${relative}`);
41
+ }
42
+
43
+ for (const relative of [
44
+ "VERSION", "README.md", "CLAUDE.md", "LICENSE", "package.json",
45
+ ".claude/nccgs/VERSION", ".claude/nccgs/constitution.md",
46
+ ".claude/nccgs/studio.json", ".claude/nccgs/workflow-catalog.json",
47
+ ".claude/nccgs/settings.fragment.json", ".claude/nccgs/unity-skills-manifest.json",
48
+ ".claude/nccgs/protocols/agent-contract.md", ".claude/nccgs/protocols/orchestration.md",
49
+ ".claude/nccgs/protocols/model-routing.md", ".claude/nccgs/protocols/context-packets.md",
50
+ ".claude/nccgs/protocols/evidence.md", ".claude/nccgs/protocols/unity-boundary.md",
51
+ "scaffold/.nccgs/project.yaml", "scaffold/.nccgs/state.md",
52
+ "scaffold/.nccgs/requirements.yaml", "scaffold/.nccgs/templates/context-packet.yaml",
53
+ "scaffold/.nccgs/templates/closure-record.md", "package-assets/setup-vivox-voice-chat-evals.gitignore",
54
+ "scripts/cli.mjs", "scripts/install.mjs",
55
+ "scripts/sync-unity-skills.mjs", "tests/framework.test.mjs"
56
+ ]) requireFile(relative);
57
+
58
+ const versions = [
59
+ fs.readFileSync(path.join(root, "VERSION"), "utf8").trim(),
60
+ JSON.parse(fs.readFileSync(path.join(root, "package.json"), "utf8")).version,
61
+ fs.readFileSync(path.join(root, ".claude/nccgs/VERSION"), "utf8").trim()
62
+ ];
63
+ if (new Set(versions).size !== 1) errors.push(`Version mismatch: ${versions.join(" / ")}`);
64
+
65
+ const packageJson = JSON.parse(fs.readFileSync(path.join(root, "package.json"), "utf8"));
66
+ if (packageJson.private === true) errors.push("package.json is marked private and cannot be published.");
67
+ if (packageJson.bin?.nccgs !== "scripts/cli.mjs") errors.push("package.json does not expose the nccgs CLI.");
68
+
69
+ const rootClaude = fs.readFileSync(path.join(root, "CLAUDE.md"), "utf8");
70
+ if (!rootClaude.includes("@.claude/nccgs/constitution.md")) errors.push("CLAUDE.md does not import the NCCGS constitution.");
71
+
72
+ const studio = JSON.parse(fs.readFileSync(path.join(root, ".claude/nccgs/studio.json"), "utf8"));
73
+ const agentNames = new Set();
74
+ for (const agent of studio.agents ?? []) {
75
+ if (agentNames.has(agent.name)) errors.push(`Duplicate catalog agent: ${agent.name}`);
76
+ agentNames.add(agent.name);
77
+ const file = path.join(root, ".claude", "agents", `${agent.name}.md`);
78
+ if (!fs.existsSync(file)) { errors.push(`Catalog agent file missing: ${agent.name}`); continue; }
79
+ const metadata = frontmatter(file);
80
+ if (metadata?.name !== agent.name || !metadata?.description) errors.push(`Invalid agent frontmatter: ${agent.name}`);
81
+ const expected = studio.modelProfiles[studio.defaultProfile]?.[agent.modelClass];
82
+ if (metadata?.model !== expected) errors.push(`Agent model mismatch: ${agent.name} expected ${expected}, found ${metadata?.model}`);
83
+ if ((metadata?.tools ?? "").split(/,\s*/).includes("Task")) errors.push(`Agent may recursively delegate: ${agent.name}`);
84
+ if (!fs.readFileSync(file, "utf8").includes("protocols/agent-contract.md")) errors.push(`Agent does not load common contract: ${agent.name}`);
85
+ if (fs.readFileSync(file, "utf8").split(/\r?\n/).length > 80) errors.push(`Agent entrypoint exceeds 80 lines: ${agent.name}`);
86
+ }
87
+
88
+ const actualAgents = walk(path.join(root, ".claude/agents")).filter((file) => file.endsWith(".md"));
89
+ for (const file of actualAgents) {
90
+ if (!agentNames.has(path.basename(file, ".md"))) errors.push(`Uncataloged NCCGS agent: ${posix(path.relative(root, file))}`);
91
+ }
92
+
93
+ const workflows = JSON.parse(fs.readFileSync(path.join(root, ".claude/nccgs/workflow-catalog.json"), "utf8"));
94
+ const workflowNames = new Set();
95
+ for (const workflow of workflows.workflows ?? []) {
96
+ if (workflowNames.has(workflow.name)) errors.push(`Duplicate workflow: ${workflow.name}`);
97
+ workflowNames.add(workflow.name);
98
+ const file = path.join(root, ".claude", "skills", workflow.name, "SKILL.md");
99
+ if (!fs.existsSync(file)) { errors.push(`Workflow skill missing: ${workflow.name}`); continue; }
100
+ const metadata = frontmatter(file);
101
+ if (metadata?.name !== workflow.name || !metadata?.description) errors.push(`Invalid workflow frontmatter: ${workflow.name}`);
102
+ const lines = fs.readFileSync(file, "utf8").split(/\r?\n/).length;
103
+ if (lines > 120) errors.push(`Workflow entrypoint exceeds 120 lines: ${workflow.name}`);
104
+ if (fs.existsSync(path.join(path.dirname(file), "README.md"))) errors.push(`NCCGS-owned skill contains redundant README: ${workflow.name}`);
105
+ for (const role of workflow.primaryRoles ?? []) if (!agentNames.has(role)) errors.push(`Workflow ${workflow.name} references unknown role ${role}`);
106
+ }
107
+ for (const entry of workflows.entrypoints ?? []) if (!workflowNames.has(entry)) errors.push(`Missing entrypoint workflow: ${entry}`);
108
+
109
+ const projectScaffold = fs.readFileSync(path.join(root, "scaffold/.nccgs/project.yaml"), "utf8");
110
+ for (const pattern of [
111
+ /^schema_version:\s*2\s*$/m, /^studio:\s*$/m, /^models:\s*$/m,
112
+ /^context:\s*$/m, /^closure:\s*$/m, /max_agent_depth:\s*1/m,
113
+ /single_writer_per_path:\s*true/m, /independent_verification:\s*true/m,
114
+ /canonical_record:\s*"\.nccgs\/closures\/\{feature_id\}\.md"/m
115
+ ]) if (!pattern.test(projectScaffold)) errors.push(`Project scaffold lacks schema invariant: ${pattern}`);
116
+
117
+ const settingsFragment = JSON.parse(fs.readFileSync(path.join(root, ".claude/nccgs/settings.fragment.json"), "utf8"));
118
+ const hookCommands = [];
119
+ for (const groups of Object.values(settingsFragment.hooks ?? {})) {
120
+ for (const group of groups) for (const hook of group.hooks ?? []) hookCommands.push(hook.command);
121
+ }
122
+ if (hookCommands.length !== new Set(hookCommands).size) errors.push("Managed hook command is duplicated.");
123
+ for (const command of hookCommands) {
124
+ const match = command.match(/^node\s+([^\s]+\.mjs)/);
125
+ if (!match) { errors.push(`Managed hook is not a Node script: ${command}`); continue; }
126
+ const file = path.join(root, ...match[1].split("/"));
127
+ if (!fs.existsSync(file)) { errors.push(`Managed hook script missing: ${match[1]}`); continue; }
128
+ const check = spawnSync(process.execPath, ["--check", file], { encoding: "utf8" });
129
+ if (check.status !== 0) errors.push(`Hook syntax failure ${match[1]}: ${check.stderr.trim()}`);
130
+ }
131
+ for (const file of [path.join(root, ".claude/nccgs/tools/configure-models.mjs"), path.join(root, "scripts/cli.mjs"), path.join(root, "scripts/install.mjs")]) {
132
+ const check = spawnSync(process.execPath, ["--check", file], { encoding: "utf8" });
133
+ if (check.status !== 0) errors.push(`Script syntax failure ${posix(path.relative(root, file))}: ${check.stderr.trim()}`);
134
+ }
135
+
136
+ const rules = walk(path.join(root, ".claude/rules")).filter((file) => file.endsWith(".md"));
137
+ if (rules.length < 11) errors.push(`Expected at least 11 path-scoped rules, found ${rules.length}.`);
138
+
139
+ const manifestPath = path.join(root, ".claude/nccgs/unity-skills-manifest.json");
140
+ const npmFallbackFiles = new Map([
141
+ ["setup-vivox-voice-chat/evals/.gitignore", path.join(root, "package-assets/setup-vivox-voice-chat-evals.gitignore")]
142
+ ]);
143
+ let manifest;
144
+ try { manifest = JSON.parse(fs.readFileSync(manifestPath, "utf8")); }
145
+ catch (error) { errors.push(`Unity Skills manifest is invalid JSON: ${error.message}`); }
146
+ const allSkillNames = new Set();
147
+ for (const directory of fs.readdirSync(path.join(root, ".claude/skills"), { withFileTypes: true })) {
148
+ if (!directory.isDirectory()) continue;
149
+ const skillFile = path.join(root, ".claude/skills", directory.name, "SKILL.md");
150
+ if (!fs.existsSync(skillFile)) { warnings.push(`Skill directory has no SKILL.md: ${directory.name}`); continue; }
151
+ const metadata = frontmatter(skillFile);
152
+ if (!metadata?.name || !metadata?.description) errors.push(`Skill lacks name/description: ${directory.name}`);
153
+ if (metadata?.name !== directory.name) errors.push(`Skill folder/name mismatch: ${directory.name} != ${metadata?.name}`);
154
+ if (allSkillNames.has(metadata?.name)) errors.push(`Duplicate skill name: ${metadata?.name}`);
155
+ allSkillNames.add(metadata?.name);
156
+ }
157
+
158
+ if (manifest) {
159
+ for (const skill of manifest.skills ?? []) {
160
+ const skillDir = path.join(root, ".claude/skills", skill.name);
161
+ if (!fs.existsSync(skillDir)) { errors.push(`Vendored Unity Skill missing: ${skill.name}`); continue; }
162
+ const expectedFiles = new Set();
163
+ for (const item of skill.files ?? []) {
164
+ expectedFiles.add(item.path);
165
+ let file = path.join(skillDir, ...item.path.split("/"));
166
+ if (!fs.existsSync(file)) file = npmFallbackFiles.get(`${skill.name}/${item.path}`) ?? file;
167
+ if (!fs.existsSync(file)) { errors.push(`Vendored Unity Skill file missing: ${skill.name}/${item.path}`); continue; }
168
+ if (sha256(file) !== item.sha256) errors.push(`Vendored Unity Skill modified: ${skill.name}/${item.path}`);
169
+ }
170
+ for (const file of walk(skillDir)) {
171
+ const relative = posix(path.relative(skillDir, file));
172
+ if (!expectedFiles.has(relative)) errors.push(`Untracked file in vendored Unity Skill: ${skill.name}/${relative}`);
173
+ }
174
+ }
175
+ }
176
+
177
+ const markdownRoots = [path.join(root, "README.md"), path.join(root, "docs"), path.join(root, ".claude/nccgs"), ...[...workflowNames].map((name) => path.join(root, ".claude/skills", name))];
178
+ for (const item of markdownRoots) {
179
+ if (!fs.existsSync(item)) continue;
180
+ const files = fs.statSync(item).isDirectory() ? walk(item).filter((file) => file.endsWith(".md")) : [item];
181
+ for (const file of files) {
182
+ const text = fs.readFileSync(file, "utf8");
183
+ for (const match of text.matchAll(/\[[^\]]+\]\(([^)]+)\)/g)) {
184
+ const link = match[1].trim();
185
+ if (!link || link.startsWith("#") || /^[a-z]+:/i.test(link)) continue;
186
+ const target = path.resolve(path.dirname(file), link.split("#")[0]);
187
+ if (!fs.existsSync(target)) errors.push(`Broken relative link in ${posix(path.relative(root, file))}: ${link}`);
188
+ }
189
+ }
190
+ }
191
+
192
+ for (const warning of warnings) console.warn(`WARN: ${warning}`);
193
+ if (errors.length) {
194
+ for (const error of errors) console.error(`ERROR: ${error}`);
195
+ console.error(`\nNCCGS validation failed with ${errors.length} error(s).`);
196
+ process.exit(1);
197
+ }
198
+
199
+ console.log("NCCGS validation passed.");
200
+ console.log(`NCCGS workflows: ${workflowNames.size} (${workflows.entrypoints.length} entrypoints)`);
201
+ console.log(`Studio agents: ${agentNames.size}`);
202
+ console.log(`Path-scoped rules: ${rules.length}`);
203
+ console.log(`Managed hooks: ${hookCommands.length}`);
204
+ console.log(`Vendored Unity Skills: ${manifest?.skills?.length ?? 0}`);
205
+ console.log(`Discoverable skills total: ${allSkillNames.size}`);