nccgs 1.1.0 → 1.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (215) hide show
  1. package/.claude/agents/nccgs-adversarial-reviewer.md +7 -1
  2. package/.claude/agents/nccgs-engine-programmer.md +2 -1
  3. package/.claude/agents/nccgs-gameplay-programmer.md +2 -1
  4. package/.claude/agents/nccgs-qa-engineer.md +2 -1
  5. package/.claude/agents/nccgs-ui-programmer.md +2 -1
  6. package/.claude/agents/nccgs-unity-build-specialist.md +2 -1
  7. package/.claude/agents/nccgs-unity-implementer.md +2 -1
  8. package/.claude/agents/nccgs-unity-systems-specialist.md +2 -1
  9. package/.claude/agents/nccgs-unity-ui-specialist.md +2 -1
  10. package/.claude/agents/nccgs-verification-engineer.md +2 -1
  11. package/.claude/nccgs/VERSION +1 -1
  12. package/.claude/{agents → nccgs/agent-library}/nccgs-accessibility-specialist.md +0 -1
  13. package/.claude/nccgs/agent-library/nccgs-adversarial-reviewer.md +30 -0
  14. package/.claude/{agents → nccgs/agent-library}/nccgs-ai-programmer.md +0 -1
  15. package/.claude/{agents → nccgs/agent-library}/nccgs-analytics-engineer.md +0 -1
  16. package/.claude/{agents → nccgs/agent-library}/nccgs-art-direction-lead.md +0 -1
  17. package/.claude/{agents → nccgs/agent-library}/nccgs-audio-direction-lead.md +0 -1
  18. package/.claude/{agents → nccgs/agent-library}/nccgs-creative-director.md +0 -1
  19. package/.claude/{agents → nccgs/agent-library}/nccgs-documentation-manager.md +0 -1
  20. package/.claude/{agents → nccgs/agent-library}/nccgs-economy-designer.md +0 -1
  21. package/.claude/nccgs/agent-library/nccgs-engine-programmer.md +25 -0
  22. package/.claude/{agents → nccgs/agent-library}/nccgs-game-design-lead.md +0 -1
  23. package/.claude/{agents → nccgs/agent-library}/nccgs-game-designer.md +0 -1
  24. package/.claude/nccgs/agent-library/nccgs-gameplay-programmer.md +25 -0
  25. package/.claude/{agents → nccgs/agent-library}/nccgs-level-designer.md +0 -1
  26. package/.claude/{agents → nccgs/agent-library}/nccgs-live-ops-designer.md +0 -1
  27. package/.claude/{agents → nccgs/agent-library}/nccgs-localization-lead.md +0 -1
  28. package/.claude/{agents → nccgs/agent-library}/nccgs-narrative-lead.md +0 -1
  29. package/.claude/{agents → nccgs/agent-library}/nccgs-network-programmer.md +0 -1
  30. package/.claude/{agents → nccgs/agent-library}/nccgs-performance-analyst.md +0 -1
  31. package/.claude/{agents → nccgs/agent-library}/nccgs-producer.md +0 -1
  32. package/.claude/{agents → nccgs/agent-library}/nccgs-production-coordinator.md +0 -1
  33. package/.claude/{agents → nccgs/agent-library}/nccgs-programming-lead.md +0 -1
  34. package/.claude/{agents → nccgs/agent-library}/nccgs-prototyper.md +0 -1
  35. package/.claude/nccgs/agent-library/nccgs-qa-engineer.md +25 -0
  36. package/.claude/{agents → nccgs/agent-library}/nccgs-qa-lead.md +0 -1
  37. package/.claude/{agents → nccgs/agent-library}/nccgs-release-engineer.md +0 -1
  38. package/.claude/{agents → nccgs/agent-library}/nccgs-release-lead.md +0 -1
  39. package/.claude/{agents → nccgs/agent-library}/nccgs-security-engineer.md +0 -1
  40. package/.claude/{agents → nccgs/agent-library}/nccgs-sound-designer.md +0 -1
  41. package/.claude/{agents → nccgs/agent-library}/nccgs-systems-designer.md +0 -1
  42. package/.claude/{agents → nccgs/agent-library}/nccgs-technical-architect.md +0 -1
  43. package/.claude/{agents → nccgs/agent-library}/nccgs-technical-artist.md +0 -1
  44. package/.claude/{agents → nccgs/agent-library}/nccgs-technical-director.md +0 -1
  45. package/.claude/{agents → nccgs/agent-library}/nccgs-tools-programmer.md +0 -1
  46. package/.claude/nccgs/agent-library/nccgs-ui-programmer.md +25 -0
  47. package/.claude/nccgs/agent-library/nccgs-unity-build-specialist.md +25 -0
  48. package/.claude/{agents → nccgs/agent-library}/nccgs-unity-content-specialist.md +0 -1
  49. package/.claude/nccgs/agent-library/nccgs-unity-implementer.md +25 -0
  50. package/.claude/{agents → nccgs/agent-library}/nccgs-unity-rendering-specialist.md +0 -1
  51. package/.claude/nccgs/agent-library/nccgs-unity-systems-specialist.md +25 -0
  52. package/.claude/nccgs/agent-library/nccgs-unity-ui-specialist.md +25 -0
  53. package/.claude/{agents → nccgs/agent-library}/nccgs-ux-designer.md +0 -1
  54. package/.claude/nccgs/agent-library/nccgs-verification-engineer.md +25 -0
  55. package/.claude/{agents → nccgs/agent-library}/nccgs-world-builder.md +0 -1
  56. package/.claude/{agents → nccgs/agent-library}/nccgs-writer.md +0 -1
  57. package/.claude/nccgs/constitution.md +13 -0
  58. package/.claude/nccgs/hooks/agent-audit.mjs +6 -0
  59. package/.claude/nccgs/hooks/closure-guard.mjs +34 -0
  60. package/.claude/nccgs/hooks/runtime-observation.mjs +11 -0
  61. package/.claude/nccgs/hooks/session-start.mjs +8 -1
  62. package/.claude/nccgs/pipeline.json +176 -0
  63. package/.claude/nccgs/protocols/agent-contract.md +5 -0
  64. package/.claude/nccgs/protocols/astra-claude.md +77 -0
  65. package/.claude/nccgs/protocols/evidence.md +6 -0
  66. package/.claude/nccgs/protocols/model-routing.md +27 -5
  67. package/.claude/nccgs/protocols/orchestration.md +5 -0
  68. package/.claude/nccgs/protocols/task-gates.md +66 -0
  69. package/.claude/nccgs/routing.generated.md +11 -0
  70. package/.claude/nccgs/settings.fragment.json +73 -9
  71. package/.claude/nccgs/studio.json +187 -96
  72. package/.claude/nccgs/tools/approve-overrides.mjs +81 -0
  73. package/.claude/nccgs/tools/compile-policy.mjs +83 -0
  74. package/.claude/nccgs/tools/configure-models.mjs +76 -31
  75. package/.claude/nccgs/tools/gates.mjs +167 -0
  76. package/.claude/nccgs/tools/integrity.mjs +69 -0
  77. package/.claude/nccgs/tools/policy.mjs +62 -0
  78. package/.claude/nccgs/tools/runtime.mjs +40 -0
  79. package/.claude/nccgs/tools/task.mjs +77 -0
  80. package/.claude/nccgs/workflow-catalog.json +91 -14
  81. package/.claude/skills/accessibility-review/SKILL.md +6 -5
  82. package/.claude/skills/accessibility-review/references/astra.md +17 -0
  83. package/.claude/skills/accessibility-review/references/legacy.md +7 -0
  84. package/.claude/skills/architecture-decision/SKILL.md +6 -5
  85. package/.claude/skills/architecture-decision/references/astra.md +12 -0
  86. package/.claude/skills/architecture-decision/references/legacy.md +7 -0
  87. package/.claude/skills/asset-audit/SKILL.md +6 -5
  88. package/.claude/skills/asset-audit/references/astra.md +17 -0
  89. package/.claude/skills/asset-audit/references/legacy.md +7 -0
  90. package/.claude/skills/audit/SKILL.md +6 -7
  91. package/.claude/skills/audit/references/astra.md +17 -0
  92. package/.claude/skills/audit/references/legacy.md +9 -0
  93. package/.claude/skills/balance-review/SKILL.md +6 -5
  94. package/.claude/skills/balance-review/references/astra.md +17 -0
  95. package/.claude/skills/balance-review/references/legacy.md +7 -0
  96. package/.claude/skills/bug-triage/SKILL.md +6 -5
  97. package/.claude/skills/bug-triage/references/astra.md +20 -0
  98. package/.claude/skills/bug-triage/references/legacy.md +7 -0
  99. package/.claude/skills/closure/SKILL.md +5 -4
  100. package/.claude/skills/closure/references/astra.md +15 -0
  101. package/.claude/skills/closure/references/legacy.md +7 -0
  102. package/.claude/skills/compatibility-review/SKILL.md +6 -5
  103. package/.claude/skills/compatibility-review/references/astra.md +17 -0
  104. package/.claude/skills/compatibility-review/references/legacy.md +7 -0
  105. package/.claude/skills/context-pack/SKILL.md +5 -4
  106. package/.claude/skills/context-pack/references/astra.md +12 -0
  107. package/.claude/skills/context-pack/references/legacy.md +7 -0
  108. package/.claude/skills/dependency-review/SKILL.md +6 -5
  109. package/.claude/skills/dependency-review/references/astra.md +17 -0
  110. package/.claude/skills/dependency-review/references/legacy.md +7 -0
  111. package/.claude/skills/design/SKILL.md +6 -11
  112. package/.claude/skills/design/references/astra.md +12 -0
  113. package/.claude/skills/design/references/legacy.md +13 -0
  114. package/.claude/skills/design-review/SKILL.md +6 -5
  115. package/.claude/skills/design-review/references/astra.md +17 -0
  116. package/.claude/skills/design-review/references/legacy.md +7 -0
  117. package/.claude/skills/evidence-review/SKILL.md +6 -5
  118. package/.claude/skills/evidence-review/references/astra.md +17 -0
  119. package/.claude/skills/evidence-review/references/legacy.md +7 -0
  120. package/.claude/skills/hotfix/SKILL.md +6 -5
  121. package/.claude/skills/hotfix/references/astra.md +23 -0
  122. package/.claude/skills/hotfix/references/legacy.md +7 -0
  123. package/.claude/skills/incident-recovery/SKILL.md +6 -5
  124. package/.claude/skills/incident-recovery/references/astra.md +20 -0
  125. package/.claude/skills/incident-recovery/references/legacy.md +7 -0
  126. package/.claude/skills/localize-game/SKILL.md +6 -5
  127. package/.claude/skills/localize-game/references/astra.md +20 -0
  128. package/.claude/skills/localize-game/references/legacy.md +7 -0
  129. package/.claude/skills/migrate-project/SKILL.md +6 -14
  130. package/.claude/skills/migrate-project/references/astra.md +23 -0
  131. package/.claude/skills/migrate-project/references/legacy.md +19 -0
  132. package/.claude/skills/migrate-project/references/procedure.md +21 -1
  133. package/.claude/skills/milestone-review/SKILL.md +6 -5
  134. package/.claude/skills/milestone-review/references/astra.md +12 -0
  135. package/.claude/skills/milestone-review/references/legacy.md +7 -0
  136. package/.claude/skills/nccgs-code-review/SKILL.md +15 -0
  137. package/.claude/skills/nccgs-code-review/references/astra.md +15 -0
  138. package/.claude/skills/{code-review/SKILL.md → nccgs-code-review/references/legacy.md} +0 -7
  139. package/.claude/skills/performance-audit/SKILL.md +6 -5
  140. package/.claude/skills/performance-audit/references/astra.md +17 -0
  141. package/.claude/skills/performance-audit/references/legacy.md +7 -0
  142. package/.claude/skills/plan-feature/SKILL.md +6 -5
  143. package/.claude/skills/plan-feature/references/astra.md +12 -0
  144. package/.claude/skills/plan-feature/references/legacy.md +7 -0
  145. package/.claude/skills/playtest/SKILL.md +5 -4
  146. package/.claude/skills/playtest/references/astra.md +20 -0
  147. package/.claude/skills/playtest/references/legacy.md +7 -0
  148. package/.claude/skills/project-stage/SKILL.md +5 -4
  149. package/.claude/skills/project-stage/references/astra.md +12 -0
  150. package/.claude/skills/project-stage/references/legacy.md +7 -0
  151. package/.claude/skills/prototype-feature/SKILL.md +6 -5
  152. package/.claude/skills/prototype-feature/references/astra.md +20 -0
  153. package/.claude/skills/prototype-feature/references/legacy.md +7 -0
  154. package/.claude/skills/qa-plan/SKILL.md +6 -5
  155. package/.claude/skills/qa-plan/references/astra.md +12 -0
  156. package/.claude/skills/qa-plan/references/legacy.md +7 -0
  157. package/.claude/skills/release/SKILL.md +6 -9
  158. package/.claude/skills/release/references/astra.md +23 -0
  159. package/.claude/skills/release/references/legacy.md +11 -0
  160. package/.claude/skills/release-readiness/SKILL.md +6 -5
  161. package/.claude/skills/release-readiness/references/astra.md +17 -0
  162. package/.claude/skills/release-readiness/references/legacy.md +7 -0
  163. package/.claude/skills/retrospective/SKILL.md +6 -5
  164. package/.claude/skills/retrospective/references/astra.md +12 -0
  165. package/.claude/skills/retrospective/references/legacy.md +7 -0
  166. package/.claude/skills/review/SKILL.md +6 -7
  167. package/.claude/skills/review/references/astra.md +15 -0
  168. package/.claude/skills/review/references/legacy.md +9 -0
  169. package/.claude/skills/security-audit/SKILL.md +6 -5
  170. package/.claude/skills/security-audit/references/astra.md +17 -0
  171. package/.claude/skills/security-audit/references/legacy.md +7 -0
  172. package/.claude/skills/sprint-plan/SKILL.md +6 -5
  173. package/.claude/skills/sprint-plan/references/astra.md +12 -0
  174. package/.claude/skills/sprint-plan/references/legacy.md +7 -0
  175. package/.claude/skills/status/SKILL.md +5 -6
  176. package/.claude/skills/status/references/astra.md +13 -0
  177. package/.claude/skills/status/references/legacy.md +9 -0
  178. package/.claude/skills/story-readiness/SKILL.md +5 -4
  179. package/.claude/skills/story-readiness/references/astra.md +17 -0
  180. package/.claude/skills/story-readiness/references/legacy.md +7 -0
  181. package/.claude/skills/test/SKILL.md +6 -7
  182. package/.claude/skills/test/references/astra.md +20 -0
  183. package/.claude/skills/test/references/legacy.md +9 -0
  184. package/.claude/skills/ui-review/SKILL.md +6 -5
  185. package/.claude/skills/ui-review/references/astra.md +17 -0
  186. package/.claude/skills/ui-review/references/legacy.md +7 -0
  187. package/.claude/skills/work/SKILL.md +6 -27
  188. package/.claude/skills/work/references/astra.md +20 -0
  189. package/.claude/skills/work/references/closure.md +10 -0
  190. package/.claude/skills/work/references/feature-contracts.md +4 -0
  191. package/.claude/skills/work/references/legacy.md +29 -0
  192. package/README.md +67 -29
  193. package/UPGRADING.md +54 -4
  194. package/VERSION +1 -1
  195. package/docs/ARCHITECTURE.md +30 -13
  196. package/docs/HUONG-DAN-MIGRATE-VA-SU-DUNG.md +34 -322
  197. package/docs/MIGRATION-MATRIX.md +1 -1
  198. package/docs/NCCGS-1.3.md +125 -0
  199. package/docs/PROJECT-POLICY.md +18 -3
  200. package/docs/WORKFLOWS.md +20 -14
  201. package/package-assets/nccgs-project.gitignore +2 -0
  202. package/package.json +5 -3
  203. package/scaffold/.nccgs/gates.json +7 -0
  204. package/scaffold/.nccgs/project.yaml +6 -3
  205. package/scaffold/.nccgs/state.md +1 -1
  206. package/scaffold/.nccgs/templates/agent-handoff.md +10 -0
  207. package/scaffold/.nccgs/templates/closure-record.md +11 -0
  208. package/scaffold/.nccgs/templates/feature-contract.md +9 -0
  209. package/scaffold/.nccgs/templates/implementation-report.md +49 -0
  210. package/scripts/cli.mjs +10 -5
  211. package/scripts/doctor.mjs +102 -18
  212. package/scripts/install.mjs +221 -88
  213. package/scripts/validate.mjs +34 -6
  214. package/tests/framework.test.mjs +213 -8
  215. package/tests/gates.test.mjs +170 -0
@@ -1,8 +1,13 @@
1
1
  import { spawnSync } from "node:child_process";
2
- import crypto from "node:crypto";
3
2
  import fs from "node:fs";
4
3
  import path from "node:path";
5
4
  import { fileURLToPath } from "node:url";
5
+ import {
6
+ equivalentFiles,
7
+ fingerprintFile,
8
+ matchesFingerprint,
9
+ sameFingerprint
10
+ } from "../.claude/nccgs/tools/integrity.mjs";
6
11
 
7
12
  const scriptDir = path.dirname(fileURLToPath(import.meta.url));
8
13
  const root = path.resolve(scriptDir, "..");
@@ -52,92 +57,131 @@ if (validation.status !== 0) {
52
57
  const frameworkSource = path.join(root, ".claude");
53
58
  const frameworkFiles = walk(frameworkSource).map((file) => ({
54
59
  source: file,
55
- relative: path.relative(root, file),
60
+ relative: posix(path.relative(root, file)),
56
61
  destination: path.join(project, path.relative(root, file)),
57
62
  }));
58
63
  const npmExcludedUnityFile = ".claude/skills/setup-vivox-voice-chat/evals/.gitignore";
59
- if (!frameworkFiles.some((item) => posix(item.relative) === npmExcludedUnityFile)) {
64
+ if (!frameworkFiles.some((item) => item.relative === npmExcludedUnityFile)) {
60
65
  const fallback = path.join(root, "package-assets", "setup-vivox-voice-chat-evals.gitignore");
61
66
  if (fs.existsSync(fallback)) {
62
67
  frameworkFiles.push({
63
68
  source: fallback,
64
- relative: npmExcludedUnityFile.split("/").join(path.sep),
69
+ relative: npmExcludedUnityFile,
65
70
  destination: path.join(project, ...npmExcludedUnityFile.split("/"))
66
71
  });
67
72
  frameworkFiles.sort((a, b) => a.relative.localeCompare(b.relative));
68
73
  }
69
74
  }
75
+
70
76
  const installStatePath = path.join(project, ".claude", "nccgs", "install-state.json");
71
- let previousInstallState = null;
72
- if (fs.existsSync(installStatePath)) {
73
- try { previousInstallState = JSON.parse(fs.readFileSync(installStatePath, "utf8")); }
74
- catch (error) {
75
- console.error(`Invalid NCCGS install state at ${installStatePath}: ${error.message}`);
76
- process.exit(2);
77
- }
78
- }
77
+ const overrideManifestPath = path.join(project, ".nccgs", "framework-overrides.json");
78
+ const previousInstallState = readJsonIfPresent(installStatePath, "NCCGS install state");
79
+ const previousOverrideManifest = readJsonIfPresent(overrideManifestPath, "NCCGS framework override manifest");
79
80
  if (updateOnly && !previousInstallState) {
80
81
  console.error("NCCGS is not installed in this project. Run `npx nccgs@latest install` first.");
81
82
  process.exit(2);
82
83
  }
84
+ if (previousOverrideManifest && previousOverrideManifest.schemaVersion !== 1) {
85
+ console.error(`Unsupported framework override schema: ${previousOverrideManifest.schemaVersion ?? "unknown"}`);
86
+ process.exit(2);
87
+ }
88
+ for (const item of previousInstallState?.files ?? []) {
89
+ if (!isManagedRelative(item.relative)) {
90
+ console.error(`Unsafe managed path in install state: ${item.relative ?? "missing"}`);
91
+ process.exit(2);
92
+ }
93
+ }
94
+ for (const item of previousOverrideManifest?.overrides ?? []) {
95
+ if (!isManagedRelative(item.path)) {
96
+ console.error(`Unsafe path in framework override manifest: ${item.path ?? "missing"}`);
97
+ process.exit(2);
98
+ }
99
+ }
100
+
83
101
  const previousFiles = new Map((previousInstallState?.files ?? []).map((item) => [item.relative, item]));
84
- const currentFrameworkNames = new Set(frameworkFiles.map((item) => posix(item.relative)));
85
- const staleFrameworkFiles = (previousInstallState?.files ?? [])
102
+ const previousOverrides = new Map((previousOverrideManifest?.overrides ?? []).map((item) => [item.path, item]));
103
+ const currentFrameworkNames = new Set(frameworkFiles.map((item) => item.relative));
104
+ const currentVersion = fs.readFileSync(path.join(root, "VERSION"), "utf8").trim();
105
+
106
+ const filePlans = frameworkFiles.map((item) => planFrameworkFile(item));
107
+ const stalePlans = (previousInstallState?.files ?? [])
86
108
  .filter((item) => !currentFrameworkNames.has(item.relative))
87
- .map((item) => ({
109
+ .map((item) => planStaleFile(item))
110
+ .filter(Boolean);
111
+
112
+ const activeOverridePaths = new Set([
113
+ ...filePlans.filter((item) => item.override).map((item) => item.relative),
114
+ ...stalePlans.filter((item) => item.override).map((item) => item.relative)
115
+ ]);
116
+ const carriedOverrides = [];
117
+ for (const item of previousOverrides.values()) {
118
+ if (activeOverridePaths.has(item.path) || currentFrameworkNames.has(item.path) || previousFiles.has(item.path)) continue;
119
+ const destination = path.join(project, ...item.path.split("/"));
120
+ const absent = item.disposition === "absent";
121
+ const stillMatches = absent ? !fs.existsSync(destination) : matchesFingerprint(destination, item);
122
+ carriedOverrides.push({
88
123
  ...item,
89
- destination: path.join(project, ...item.relative.split("/")),
90
- unmodified: fs.existsSync(path.join(project, ...item.relative.split("/"))) && hashFile(path.join(project, ...item.relative.split("/"))) === item.sha256
91
- }))
92
- .filter((item) => fs.existsSync(item.destination));
93
-
94
- const differentFiles = frameworkFiles.filter((item) => {
95
- if (!fs.existsSync(item.destination)) return false;
96
- return !sameFile(item.source, item.destination);
97
- });
98
- const automaticReplacements = differentFiles.filter((item) => {
99
- const previous = previousFiles.get(posix(item.relative));
100
- return previous && hashFile(item.destination) === previous.sha256;
101
- });
102
- const conflicts = differentFiles.filter((item) => !automaticReplacements.includes(item));
103
- const automaticStale = staleFrameworkFiles.filter((item) => item.unmodified);
104
- const staleConflicts = staleFrameworkFiles.filter((item) => !item.unmodified);
105
-
106
- if ((conflicts.length || staleConflicts.length) && !force) {
107
- console.error(`NCCGS found ${conflicts.length} modified and ${staleConflicts.length} modified-stale framework file(s):`);
108
- for (const item of conflicts.slice(0, 30)) console.error(`- ${posix(item.relative)}`);
109
- for (const item of staleConflicts.slice(0, 30)) console.error(`- stale: ${item.relative}`);
110
- if (conflicts.length > 30) console.error(`- ... ${conflicts.length - 30} more`);
111
- console.error("These files differ from the hashes recorded at the previous install.");
112
- console.error("Review them, then re-run with --force to back up and replace intentional overrides.");
113
- process.exit(2);
124
+ status: item.status === "approved" && stillMatches ? "approved" : "pending_review",
125
+ reason: stillMatches ? item.reason : "Previously approved override changed and requires migration review."
126
+ });
114
127
  }
115
128
 
116
- const newFiles = frameworkFiles.filter((item) => !fs.existsSync(item.destination));
117
- const identicalFiles = frameworkFiles.length - newFiles.length - differentFiles.length;
129
+ const overrideRecords = [
130
+ ...filePlans.filter((item) => item.override).map((item) => item.override),
131
+ ...stalePlans.filter((item) => item.override).map((item) => item.override),
132
+ ...carriedOverrides
133
+ ].sort((a, b) => a.path.localeCompare(b.path));
134
+ const pendingOverrides = overrideRecords.filter((item) => item.status !== "approved");
135
+
136
+ const newFiles = filePlans.filter((item) => item.action === "install");
137
+ const currentFiles = filePlans.filter((item) => item.action === "current");
138
+ const automaticReplacements = filePlans.filter((item) => item.action === "replace");
139
+ const forcedReplacements = filePlans.filter((item) => item.action === "force-replace");
140
+ const preservedFiles = filePlans.filter((item) => item.action.startsWith("preserve"));
141
+ const automaticStale = stalePlans.filter((item) => item.action === "remove");
142
+ const forcedStale = stalePlans.filter((item) => item.action === "force-remove");
143
+ const preservedStale = stalePlans.filter((item) => item.action === "preserve-stale");
144
+
118
145
  const scaffoldFiles = walk(path.join(root, "scaffold")).map((file) => ({
119
146
  source: file,
120
147
  relative: path.relative(path.join(root, "scaffold"), file),
121
148
  destination: path.join(project, path.relative(path.join(root, "scaffold"), file)),
122
149
  }));
150
+ const npmExcludedScaffoldFile = ".nccgs/.gitignore";
151
+ if (!scaffoldFiles.some((item) => posix(item.relative) === npmExcludedScaffoldFile)) {
152
+ const fallback = path.join(root, "package-assets", "nccgs-project.gitignore");
153
+ if (fs.existsSync(fallback)) {
154
+ scaffoldFiles.push({
155
+ source: fallback,
156
+ relative: npmExcludedScaffoldFile.split("/").join(path.sep),
157
+ destination: path.join(project, ...npmExcludedScaffoldFile.split("/"))
158
+ });
159
+ scaffoldFiles.sort((a, b) => a.relative.localeCompare(b.relative));
160
+ }
161
+ }
123
162
  const newScaffold = scaffoldFiles.filter((item) => !fs.existsSync(item.destination));
124
163
  const settingsPlan = planManagedSettings(project);
125
164
  const targetPolicyPath = path.join(project, ".nccgs", "project.yaml");
126
165
  const targetPolicy = fs.existsSync(targetPolicyPath) ? fs.readFileSync(targetPolicyPath, "utf8") : "";
127
166
  const policyModelProfile = targetPolicy.match(/^models:\s*$[\s\S]*?^\s+profile:\s*([^#\r\n]+)/m)?.[1]?.trim();
128
- const modelProfile = requestedModelProfile || policyModelProfile || "quality";
129
- if (!new Set(["quality", "balanced", "inherit"]).has(modelProfile)) {
167
+ const studio = JSON.parse(fs.readFileSync(path.join(root, ".claude/nccgs/studio.json"), "utf8"));
168
+ const modelProfile = requestedModelProfile || policyModelProfile || studio.defaultProfile;
169
+ if (!Object.hasOwn(studio.modelProfiles, modelProfile)) {
130
170
  console.error(`Unknown model profile: ${modelProfile}`);
131
171
  process.exit(2);
132
172
  }
133
173
 
134
174
  console.log(`NCCGS installation target: ${project}`);
135
- console.log(`Framework version: ${previousInstallState?.frameworkVersion ?? "not installed"} -> ${fs.readFileSync(path.join(root, "VERSION"), "utf8").trim()}`);
136
- console.log(`Framework files: ${frameworkFiles.length} (${newFiles.length} new, ${identicalFiles} current, ${automaticReplacements.length} automatic update, ${conflicts.length} forced override)`);
137
- console.log(`Stale framework files: ${automaticStale.length} automatic removal, ${staleConflicts.length} forced removal`);
175
+ console.log(`Framework version: ${previousInstallState?.frameworkVersion ?? "not installed"} -> ${currentVersion}`);
176
+ console.log(`Framework files: ${frameworkFiles.length} (${newFiles.length} new, ${currentFiles.length} current, ${automaticReplacements.length} automatic update, ${preservedFiles.length} preserved override, ${forcedReplacements.length} forced override)`);
177
+ console.log(`Stale framework files: ${automaticStale.length} automatic removal, ${preservedStale.length} preserved override, ${forcedStale.length} forced removal`);
138
178
  console.log(`Project scaffold files to initialize: ${newScaffold.length}`);
139
179
  console.log(`Managed Claude settings: ${settingsPlan.changed ? "update" : "already current"}`);
140
180
  console.log(`Agent model profile: ${modelProfile}`);
181
+ if (pendingOverrides.length) {
182
+ console.log(`${pendingOverrides.length} project override(s) will be preserved for /migrate-project review:`);
183
+ for (const item of pendingOverrides.slice(0, 30)) console.log(`- ${item.path} (${item.disposition})`);
184
+ }
141
185
  if (dryRun) {
142
186
  console.log("Dry run complete; no files changed.");
143
187
  process.exit(0);
@@ -145,23 +189,21 @@ if (dryRun) {
145
189
 
146
190
  const stamp = new Date().toISOString().replace(/[:.]/g, "-");
147
191
  const backupRoot = path.join(project, ".nccgs", "backups", stamp);
148
- for (const item of differentFiles) {
149
- const backup = path.join(backupRoot, item.relative);
150
- fs.mkdirSync(path.dirname(backup), { recursive: true });
151
- fs.copyFileSync(item.destination, backup);
152
- }
153
- for (const item of staleFrameworkFiles) {
154
- const backup = path.join(backupRoot, ...item.relative.split("/"));
155
- fs.mkdirSync(path.dirname(backup), { recursive: true });
156
- fs.copyFileSync(item.destination, backup);
192
+ const replacedFiles = [...automaticReplacements, ...forcedReplacements];
193
+ const removedFiles = [...automaticStale, ...forcedStale];
194
+ for (const item of replacedFiles) backupFile(item.destination, path.join(backupRoot, ...item.relative.split("/")));
195
+ for (const item of removedFiles) {
196
+ backupFile(item.destination, path.join(backupRoot, ...item.relative.split("/")));
157
197
  fs.unlinkSync(item.destination);
158
198
  }
199
+ if (previousOverrideManifest && JSON.stringify(previousOverrideManifest.overrides ?? []) !== JSON.stringify(overrideRecords)) {
200
+ backupFile(overrideManifestPath, path.join(backupRoot, ".nccgs", "framework-overrides.json"));
201
+ }
159
202
 
160
- for (const item of frameworkFiles) {
203
+ for (const item of filePlans.filter((entry) => new Set(["install", "replace", "force-replace"]).has(entry.action))) {
161
204
  fs.mkdirSync(path.dirname(item.destination), { recursive: true });
162
205
  fs.copyFileSync(item.source, item.destination);
163
206
  }
164
-
165
207
  for (const item of newScaffold) {
166
208
  fs.mkdirSync(path.dirname(item.destination), { recursive: true });
167
209
  fs.copyFileSync(item.source, item.destination);
@@ -170,10 +212,14 @@ for (const item of newScaffold) {
170
212
  installManagedSettings(settingsPlan, backupRoot);
171
213
  installClaudeImport(project, backupRoot);
172
214
 
215
+ const skipAgentPaths = preservedFiles
216
+ .filter((item) => item.relative.startsWith(".claude/agents/"))
217
+ .flatMap((item) => ["--skip-path", item.relative]);
173
218
  const modelConfiguration = spawnSync(process.execPath, [
174
- path.join(project, ".claude", "nccgs", "tools", "configure-models.mjs"),
219
+ path.join(root, ".claude", "nccgs", "tools", "configure-models.mjs"),
175
220
  "--project", project,
176
221
  "--profile", modelProfile,
222
+ ...skipAgentPaths
177
223
  ], { cwd: project, encoding: "utf8" });
178
224
  if (modelConfiguration.status !== 0) {
179
225
  process.stderr.write(modelConfiguration.stdout ?? "");
@@ -182,12 +228,13 @@ if (modelConfiguration.status !== 0) {
182
228
  process.exit(1);
183
229
  }
184
230
 
185
- writeInstallState(project, modelProfile, frameworkFiles, installStatePath, previousInstallState);
231
+ writeOverrideManifest(overrideManifestPath, overrideRecords, previousOverrideManifest, currentVersion);
232
+ writeInstallState(project, modelProfile, frameworkFiles, installStatePath, previousInstallState, new Set(overrideRecords.map((item) => item.path)));
186
233
 
187
234
  const doctor = spawnSync(process.execPath, [
188
235
  path.join(scriptDir, "doctor.mjs"),
189
236
  "--project", project,
190
- "--allow-policy-migration"
237
+ "--allow-migration-required"
191
238
  ], { cwd: project, encoding: "utf8" });
192
239
  process.stdout.write(doctor.stdout ?? "");
193
240
  process.stderr.write(doctor.stderr ?? "");
@@ -197,10 +244,12 @@ if (doctor.status !== 0) {
197
244
  }
198
245
 
199
246
  console.log("NCCGS installation complete.");
200
- if (differentFiles.length || staleFrameworkFiles.length || settingsPlan.changed) console.log(`Changed shared files backed up under: ${backupRoot}`);
247
+ if (fs.existsSync(backupRoot)) {
248
+ console.log(`Replaced shared files were backed up under: ${backupRoot}`);
249
+ }
201
250
  const installedPolicy = fs.readFileSync(targetPolicyPath, "utf8");
202
251
  const installedPolicySchema = Number(installedPolicy.match(/^schema_version:\s*(\d+)\s*$/m)?.[1] ?? 0);
203
- if (installedPolicySchema !== 2) {
252
+ if (installedPolicySchema !== 2 || pendingOverrides.length || /^profile_contracts:/m.test(installedPolicy)) {
204
253
  console.log("Project migration required: open a new Claude Code session and run `/migrate-project` once.");
205
254
  } else if (!previousInstallState) {
206
255
  console.log("First NCCGS adoption: run `/migrate-project` once for an existing project; a new project can start with `/status`.");
@@ -208,7 +257,75 @@ if (installedPolicySchema !== 2) {
208
257
  console.log("No project migration required for this NCCGS update.");
209
258
  }
210
259
 
260
+ function planFrameworkFile(item) {
261
+ const previous = previousFiles.get(item.relative);
262
+ const existingOverride = previousOverrides.get(item.relative);
263
+ const upstream = fingerprintFile(item.source);
264
+ if (!fs.existsSync(item.destination)) {
265
+ if (!force && (previous || existingOverride?.disposition === "absent")) {
266
+ return { ...item, action: "preserve-absent", override: buildOverride(item, existingOverride, upstream, "absent") };
267
+ }
268
+ return { ...item, action: "install", override: null };
269
+ }
270
+ if (equivalentFiles(item.source, item.destination)) return { ...item, action: "current", override: null };
271
+ if (!force && previous && matchesFingerprint(item.destination, previous)) return { ...item, action: "replace", override: null };
272
+ if (force) return { ...item, action: "force-replace", override: null };
273
+ return { ...item, action: "preserve", override: buildOverride(item, existingOverride, upstream, "preserve") };
274
+ }
275
+
276
+ function planStaleFile(previous) {
277
+ const destination = path.join(project, ...previous.relative.split("/"));
278
+ if (!fs.existsSync(destination)) return null;
279
+ if (force) return { ...previous, relative: previous.relative, destination, action: "force-remove", override: null };
280
+ if (matchesFingerprint(destination, previous)) return { ...previous, relative: previous.relative, destination, action: "remove", override: null };
281
+ const existingOverride = previousOverrides.get(previous.relative);
282
+ const current = fingerprintFile(destination);
283
+ const stillApproved = existingOverride?.status === "approved" && matchesFingerprint(destination, existingOverride)
284
+ && existingOverride.upstreamSha256 === null;
285
+ return {
286
+ ...previous,
287
+ relative: previous.relative,
288
+ destination,
289
+ action: "preserve-stale",
290
+ override: {
291
+ path: previous.relative,
292
+ disposition: "preserve",
293
+ status: stillApproved ? "approved" : "pending_review",
294
+ ...current,
295
+ upstreamSha256: null,
296
+ upstreamCanonicalSha256: null,
297
+ retiredFromFramework: true,
298
+ reason: "Locally modified file was preserved after its framework path retired; review ownership during migration."
299
+ }
300
+ };
301
+ }
302
+
303
+ function buildOverride(item, existing, upstream, disposition) {
304
+ const current = disposition === "absent" ? { sha256: null, canonicalSha256: null } : fingerprintFile(item.destination);
305
+ const currentMatches = disposition === "absent"
306
+ ? existing?.disposition === "absent" && !fs.existsSync(item.destination)
307
+ : existing?.disposition === "preserve" && sameFingerprint(current, existing);
308
+ const previousUpstream = existing ? {
309
+ sha256: existing.upstreamSha256,
310
+ canonicalSha256: existing.upstreamCanonicalSha256
311
+ } : null;
312
+ const upstreamMatches = previousUpstream && sameFingerprint(upstream, previousUpstream);
313
+ const approved = existing?.status === "approved" && currentMatches && upstreamMatches;
314
+ return {
315
+ path: item.relative,
316
+ disposition,
317
+ status: approved ? "approved" : "pending_review",
318
+ ...current,
319
+ upstreamSha256: upstream.sha256,
320
+ upstreamCanonicalSha256: upstream.canonicalSha256 ?? null,
321
+ reason: approved
322
+ ? existing.reason
323
+ : "Project-specific framework change was preserved automatically; review and rebase it with /migrate-project."
324
+ };
325
+ }
326
+
211
327
  function walk(directory) {
328
+ if (!fs.existsSync(directory)) return [];
212
329
  const output = [];
213
330
  for (const entry of fs.readdirSync(directory, { withFileTypes: true })) {
214
331
  const full = path.join(directory, entry.name);
@@ -218,27 +335,36 @@ function walk(directory) {
218
335
  return output.sort((a, b) => a.localeCompare(b));
219
336
  }
220
337
 
221
- function sameFile(left, right) {
222
- const a = fs.readFileSync(left);
223
- const b = fs.readFileSync(right);
224
- return a.length === b.length && a.equals(b);
338
+ function posix(value) {
339
+ return value.split(path.sep).join("/");
225
340
  }
226
341
 
227
- function hashFile(file) {
228
- return crypto.createHash("sha256").update(fs.readFileSync(file)).digest("hex");
342
+ function isManagedRelative(value) {
343
+ if (typeof value !== "string" || !value.startsWith(".claude/")) return false;
344
+ const parts = value.split("/");
345
+ return !parts.includes("") && !parts.includes(".") && !parts.includes("..");
229
346
  }
230
347
 
231
- function posix(value) {
232
- return value.split(path.sep).join("/");
348
+ function readJsonIfPresent(file, label) {
349
+ if (!fs.existsSync(file)) return null;
350
+ try { return JSON.parse(fs.readFileSync(file, "utf8")); }
351
+ catch (error) {
352
+ console.error(`Invalid ${label} at ${file}: ${error.message}`);
353
+ process.exit(2);
354
+ }
355
+ }
356
+
357
+ function backupFile(source, destination) {
358
+ fs.mkdirSync(path.dirname(destination), { recursive: true });
359
+ fs.copyFileSync(source, destination);
233
360
  }
234
361
 
235
362
  function planManagedSettings(projectRoot) {
236
363
  const settingsPath = path.join(projectRoot, ".claude", "settings.json");
237
364
  let current = {};
238
365
  if (fs.existsSync(settingsPath)) {
239
- try {
240
- current = JSON.parse(fs.readFileSync(settingsPath, "utf8"));
241
- } catch (error) {
366
+ try { current = JSON.parse(fs.readFileSync(settingsPath, "utf8")); }
367
+ catch (error) {
242
368
  console.error(`Cannot merge invalid JSON settings at ${settingsPath}: ${error.message}`);
243
369
  process.exit(2);
244
370
  }
@@ -264,27 +390,37 @@ function planManagedSettings(projectRoot) {
264
390
 
265
391
  function installManagedSettings(plan, backupDirectory) {
266
392
  if (!plan.changed) return;
267
- if (plan.existed) {
268
- const backup = path.join(backupDirectory, ".claude", "settings.json");
269
- fs.mkdirSync(path.dirname(backup), { recursive: true });
270
- fs.copyFileSync(plan.settingsPath, backup);
271
- }
393
+ if (plan.existed) backupFile(plan.settingsPath, path.join(backupDirectory, ".claude", "settings.json"));
272
394
  fs.mkdirSync(path.dirname(plan.settingsPath), { recursive: true });
273
395
  fs.writeFileSync(plan.settingsPath, plan.nextText, "utf8");
274
396
  }
275
397
 
276
- function writeInstallState(projectRoot, profile, files, statePath, previousState) {
398
+ function writeOverrideManifest(file, overrides, previous, frameworkVersion) {
399
+ if (!overrides.length && !previous) return;
400
+ const now = new Date().toISOString();
401
+ const manifest = {
402
+ schemaVersion: 1,
403
+ frameworkVersion,
404
+ createdAt: previous?.createdAt ?? now,
405
+ updatedAt: now,
406
+ overrides
407
+ };
408
+ fs.mkdirSync(path.dirname(file), { recursive: true });
409
+ fs.writeFileSync(file, `${JSON.stringify(manifest, null, 2)}\n`, "utf8");
410
+ }
411
+
412
+ function writeInstallState(projectRoot, profile, files, statePath, previousState, overridePaths) {
277
413
  const now = new Date().toISOString();
278
414
  const state = {
279
- schemaVersion: 2,
280
- frameworkVersion: fs.readFileSync(path.join(root, "VERSION"), "utf8").trim(),
415
+ schemaVersion: 3,
416
+ frameworkVersion: currentVersion,
281
417
  previousFrameworkVersion: previousState?.frameworkVersion ?? null,
282
418
  modelProfile: profile,
283
419
  installedAt: previousState?.installedAt ?? now,
284
420
  updatedAt: now,
285
421
  files: files.map((item) => ({
286
- relative: posix(item.relative),
287
- sha256: hashFile(path.join(projectRoot, item.relative))
422
+ relative: item.relative,
423
+ ...(overridePaths.has(item.relative) ? fingerprintFile(item.source) : fingerprintFile(path.join(projectRoot, ...item.relative.split("/"))))
288
424
  }))
289
425
  };
290
426
  fs.mkdirSync(path.dirname(statePath), { recursive: true });
@@ -299,12 +435,9 @@ function installClaudeImport(projectRoot, backupDirectory) {
299
435
  fs.copyFileSync(path.join(root, "CLAUDE.md"), claudePath);
300
436
  return;
301
437
  }
302
-
303
438
  const current = fs.readFileSync(claudePath, "utf8");
304
439
  if (current.includes(begin) || current.includes("@.claude/nccgs/constitution.md")) return;
305
-
306
- fs.mkdirSync(backupDirectory, { recursive: true });
307
- fs.copyFileSync(claudePath, path.join(backupDirectory, "CLAUDE.md"));
440
+ backupFile(claudePath, path.join(backupDirectory, "CLAUDE.md"));
308
441
  const separator = current.endsWith("\n") ? "\n" : "\n\n";
309
442
  fs.writeFileSync(claudePath, `${current}${separator}${block}\n`, "utf8");
310
443
  }
@@ -3,6 +3,7 @@ import fs from "node:fs";
3
3
  import path from "node:path";
4
4
  import { spawnSync } from "node:child_process";
5
5
  import { fileURLToPath } from "node:url";
6
+ import { compile } from "../.claude/nccgs/tools/compile-policy.mjs";
6
7
 
7
8
  const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
8
9
  const errors = [];
@@ -44,17 +45,29 @@ for (const relative of [
44
45
  "VERSION", "README.md", "CLAUDE.md", "LICENSE", "package.json",
45
46
  ".claude/nccgs/VERSION", ".claude/nccgs/constitution.md",
46
47
  ".claude/nccgs/studio.json", ".claude/nccgs/workflow-catalog.json",
48
+ ".claude/nccgs/pipeline.json", ".claude/nccgs/protocols/task-gates.md",
49
+ ".claude/nccgs/tools/task.mjs", ".claude/nccgs/tools/gates.mjs", ".claude/nccgs/tools/runtime.mjs",
50
+ "scaffold/.nccgs/gates.json", "tests/gates.test.mjs",
47
51
  ".claude/nccgs/settings.fragment.json", ".claude/nccgs/unity-skills-manifest.json",
48
52
  ".claude/nccgs/protocols/agent-contract.md", ".claude/nccgs/protocols/orchestration.md",
49
53
  ".claude/nccgs/protocols/model-routing.md", ".claude/nccgs/protocols/context-packets.md",
54
+ ".claude/nccgs/protocols/astra-claude.md", "scaffold/.nccgs/templates/implementation-report.md",
50
55
  ".claude/nccgs/protocols/evidence.md", ".claude/nccgs/protocols/unity-boundary.md",
51
56
  "scaffold/.nccgs/project.yaml", "scaffold/.nccgs/state.md",
52
57
  "scaffold/.nccgs/requirements.yaml", "scaffold/.nccgs/templates/context-packet.yaml",
53
58
  "scaffold/.nccgs/templates/closure-record.md", "package-assets/setup-vivox-voice-chat-evals.gitignore",
59
+ "package-assets/nccgs-project.gitignore",
54
60
  "scripts/cli.mjs", "scripts/doctor.mjs", "scripts/install.mjs",
55
- "scripts/sync-unity-skills.mjs", "tests/framework.test.mjs"
61
+ "scripts/sync-unity-skills.mjs", "tests/framework.test.mjs",
62
+ ".claude/nccgs/tools/integrity.mjs", ".claude/nccgs/tools/approve-overrides.mjs"
56
63
  ]) requireFile(relative);
57
64
 
65
+ const scaffoldIgnore = path.join(root, "scaffold/.nccgs/.gitignore");
66
+ const scaffoldIgnoreFallback = path.join(root, "package-assets/nccgs-project.gitignore");
67
+ if (fs.existsSync(scaffoldIgnore) && fs.readFileSync(scaffoldIgnore, "utf8") !== fs.readFileSync(scaffoldIgnoreFallback, "utf8")) {
68
+ errors.push("Scaffold .nccgs/.gitignore differs from its npm fallback asset.");
69
+ }
70
+
58
71
  const versions = [
59
72
  fs.readFileSync(path.join(root, "VERSION"), "utf8").trim(),
60
73
  JSON.parse(fs.readFileSync(path.join(root, "package.json"), "utf8")).version,
@@ -70,17 +83,22 @@ const rootClaude = fs.readFileSync(path.join(root, "CLAUDE.md"), "utf8");
70
83
  if (!rootClaude.includes("@.claude/nccgs/constitution.md")) errors.push("CLAUDE.md does not import the NCCGS constitution.");
71
84
 
72
85
  const studio = JSON.parse(fs.readFileSync(path.join(root, ".claude/nccgs/studio.json"), "utf8"));
86
+ const pipeline = JSON.parse(fs.readFileSync(path.join(root, ".claude/nccgs/pipeline.json"), "utf8"));
87
+ for (const stale of compile(root)) errors.push(`Stale generated policy: ${stale}; run npm run policy:build`);
73
88
  const agentNames = new Set();
74
89
  for (const agent of studio.agents ?? []) {
75
90
  if (agentNames.has(agent.name)) errors.push(`Duplicate catalog agent: ${agent.name}`);
76
91
  agentNames.add(agent.name);
77
- const file = path.join(root, ".claude", "agents", `${agent.name}.md`);
92
+ const active = pipeline.profiles[pipeline.defaultProfile].activeAgents.includes(agent.name);
93
+ const file = path.join(root, active ? ".claude/agents" : ".claude/nccgs/agent-library", `${agent.name}.md`);
78
94
  if (!fs.existsSync(file)) { errors.push(`Catalog agent file missing: ${agent.name}`); continue; }
79
95
  const metadata = frontmatter(file);
80
96
  if (metadata?.name !== agent.name || !metadata?.description) errors.push(`Invalid agent frontmatter: ${agent.name}`);
81
97
  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}`);
98
+ if (active && metadata?.model !== expected) errors.push(`Agent model mismatch: ${agent.name} expected ${expected}, found ${metadata?.model}`);
99
+ const expectedEffort = studio.effortProfiles?.[studio.defaultProfile]?.[agent.modelClass];
100
+ if (active && metadata?.effort !== expectedEffort) errors.push(`Agent effort mismatch: ${agent.name} expected ${expectedEffort}, found ${metadata?.effort}`);
101
+ if ((metadata?.tools ?? "").split(/,\s*/).some(tool => ["Task", "Agent"].includes(tool))) errors.push(`Agent may recursively delegate: ${agent.name}`);
84
102
  if (!fs.readFileSync(file, "utf8").includes("protocols/agent-contract.md")) errors.push(`Agent does not load common contract: ${agent.name}`);
85
103
  if (fs.readFileSync(file, "utf8").split(/\r?\n/).length > 80) errors.push(`Agent entrypoint exceeds 80 lines: ${agent.name}`);
86
104
  }
@@ -88,6 +106,7 @@ for (const agent of studio.agents ?? []) {
88
106
  const actualAgents = walk(path.join(root, ".claude/agents")).filter((file) => file.endsWith(".md"));
89
107
  for (const file of actualAgents) {
90
108
  if (!agentNames.has(path.basename(file, ".md"))) errors.push(`Uncataloged NCCGS agent: ${posix(path.relative(root, file))}`);
109
+ if (!pipeline.coreAgents.includes(path.basename(file, ".md"))) errors.push(`Non-core agent active in source package: ${posix(path.relative(root, file))}`);
91
110
  }
92
111
 
93
112
  const workflows = JSON.parse(fs.readFileSync(path.join(root, ".claude/nccgs/workflow-catalog.json"), "utf8"));
@@ -103,6 +122,10 @@ for (const workflow of workflows.workflows ?? []) {
103
122
  if (lines > 120) errors.push(`Workflow entrypoint exceeds 120 lines: ${workflow.name}`);
104
123
  if (fs.existsSync(path.join(path.dirname(file), "README.md"))) errors.push(`NCCGS-owned skill contains redundant README: ${workflow.name}`);
105
124
  for (const role of workflow.primaryRoles ?? []) if (!agentNames.has(role)) errors.push(`Workflow ${workflow.name} references unknown role ${role}`);
125
+ for (const [profile, roles] of Object.entries(workflow.primaryRolesByProfile ?? {})) {
126
+ if (!studio.modelProfiles[profile]) errors.push(`Workflow ${workflow.name} references unknown profile ${profile}`);
127
+ for (const role of roles) if (!agentNames.has(role)) errors.push(`Workflow ${workflow.name} references unknown role ${role}`);
128
+ }
106
129
  }
107
130
  for (const entry of workflows.entrypoints ?? []) if (!workflowNames.has(entry)) errors.push(`Missing entrypoint workflow: ${entry}`);
108
131
 
@@ -128,7 +151,12 @@ for (const command of hookCommands) {
128
151
  const check = spawnSync(process.execPath, ["--check", file], { encoding: "utf8" });
129
152
  if (check.status !== 0) errors.push(`Hook syntax failure ${match[1]}: ${check.stderr.trim()}`);
130
153
  }
131
- for (const file of [path.join(root, ".claude/nccgs/tools/configure-models.mjs"), path.join(root, "scripts/cli.mjs"), path.join(root, "scripts/doctor.mjs"), path.join(root, "scripts/install.mjs")]) {
154
+ for (const file of [
155
+ ...walk(path.join(root, ".claude/nccgs/tools")).filter(file => file.endsWith(".mjs")),
156
+ path.join(root, "scripts/cli.mjs"),
157
+ path.join(root, "scripts/doctor.mjs"),
158
+ path.join(root, "scripts/install.mjs")
159
+ ]) {
132
160
  const check = spawnSync(process.execPath, ["--check", file], { encoding: "utf8" });
133
161
  if (check.status !== 0) errors.push(`Script syntax failure ${posix(path.relative(root, file))}: ${check.stderr.trim()}`);
134
162
  }
@@ -198,7 +226,7 @@ if (errors.length) {
198
226
 
199
227
  console.log("NCCGS validation passed.");
200
228
  console.log(`NCCGS workflows: ${workflowNames.size} (${workflows.entrypoints.length} entrypoints)`);
201
- console.log(`Studio agents: ${agentNames.size}`);
229
+ console.log(`Studio agents: ${agentNames.size} in library; ${pipeline.coreAgents.length} active by default`);
202
230
  console.log(`Path-scoped rules: ${rules.length}`);
203
231
  console.log(`Managed hooks: ${hookCommands.length}`);
204
232
  console.log(`Vendored Unity Skills: ${manifest?.skills?.length ?? 0}`);