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
@@ -6,13 +6,46 @@ import path from "node:path";
6
6
  import { spawnSync } from "node:child_process";
7
7
  import test from "node:test";
8
8
  import { fileURLToPath } from "node:url";
9
+ import { compile, renderAgent } from "../.claude/nccgs/tools/compile-policy.mjs";
9
10
 
10
11
  const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
11
12
  const studio = JSON.parse(fs.readFileSync(path.join(root, ".claude/nccgs/studio.json"), "utf8"));
12
13
  const workflows = JSON.parse(fs.readFileSync(path.join(root, ".claude/nccgs/workflow-catalog.json"), "utf8"));
14
+ const pipeline = JSON.parse(fs.readFileSync(path.join(root, ".claude/nccgs/pipeline.json"), "utf8"));
15
+ function copyAgentFixture(temp) {
16
+ fs.cpSync(path.join(root, ".claude/nccgs"), path.join(temp, ".claude/nccgs"), { recursive: true });
17
+ fs.mkdirSync(path.join(temp, ".claude/agents"), { recursive: true });
18
+ for (const agent of studio.agents) {
19
+ const template = fs.readFileSync(path.join(root, ".claude/nccgs/agent-library", `${agent.name}.md`), "utf8");
20
+ fs.writeFileSync(path.join(temp, ".claude/agents", `${agent.name}.md`), renderAgent(template, pipeline.profiles["astra-claude"].models[agent.modelClass]));
21
+ }
22
+ }
13
23
  const frameworkVersion = fs.readFileSync(path.join(root, "VERSION"), "utf8").trim();
14
24
  const sha256 = (value) => crypto.createHash("sha256").update(value).digest("hex");
15
25
 
26
+ test("canonical policy changes invalidate generated routing until compilation", () => {
27
+ const temp = fs.mkdtempSync(path.join(os.tmpdir(), "nccgs-policy-test-"));
28
+ try {
29
+ copyAgentFixture(temp);
30
+ compile(temp, true);
31
+ assert.deepEqual(compile(temp), []);
32
+ const file = path.join(temp, ".claude/nccgs/pipeline.json");
33
+ const policy = JSON.parse(fs.readFileSync(file, "utf8"));
34
+ policy.profiles["astra-claude"].models.review.effort = "medium";
35
+ fs.writeFileSync(file, JSON.stringify(policy));
36
+ const stale = compile(temp);
37
+ assert.ok(stale.includes(".claude/agents/nccgs-adversarial-reviewer.md"));
38
+ assert.ok(stale.includes(".claude/nccgs/workflow-catalog.json"));
39
+ compile(temp, true);
40
+ assert.deepEqual(compile(temp), []);
41
+ const catalog = JSON.parse(fs.readFileSync(path.join(temp, ".claude/nccgs/studio.json"), "utf8"));
42
+ assert.equal(catalog.effortProfiles["astra-claude"].review, "medium");
43
+ } finally {
44
+ assert.ok(path.resolve(temp).startsWith(path.resolve(os.tmpdir()) + path.sep + "nccgs-policy-test-"));
45
+ fs.rmSync(temp, { recursive: true, force: true });
46
+ }
47
+ });
48
+
16
49
  test("studio roles are unique and workflow references resolve", () => {
17
50
  const roles = new Set(studio.agents.map((agent) => agent.name));
18
51
  assert.equal(roles.size, studio.agents.length);
@@ -27,8 +60,116 @@ test("quality profile assigns all model classes", () => {
27
60
  }
28
61
  });
29
62
 
63
+ test("optional specialists survive updates and can be disabled without changing core agents", () => {
64
+ const temp = fs.mkdtempSync(path.join(os.tmpdir(), "nccgs-optional-agents-"));
65
+ try {
66
+ for (const marker of ["Assets", "Packages", "ProjectSettings"]) fs.mkdirSync(path.join(temp, marker));
67
+ const run = (...args) => spawnSync(process.execPath, [path.join(root, "scripts/cli.mjs"), ...args], { cwd: temp, encoding: "utf8" });
68
+ const install = run("install");
69
+ assert.equal(install.status, 0, install.stderr || install.stdout);
70
+ const role = "nccgs-network-programmer";
71
+ const file = path.join(temp, ".claude/agents", `${role}.md`);
72
+ assert.ok(!fs.existsSync(file));
73
+ assert.equal(run("agents", "--enable", role).status, 0);
74
+ assert.match(fs.readFileSync(file, "utf8"), /model: claude-opus-5-5/);
75
+ const update = run("update");
76
+ assert.equal(update.status, 0, update.stderr || update.stdout);
77
+ assert.ok(fs.existsSync(file));
78
+ assert.equal(run("doctor").status, 0);
79
+ assert.equal(run("agents", "--disable", role).status, 0);
80
+ assert.ok(!fs.existsSync(file));
81
+ assert.notEqual(run("agents", "--disable", "nccgs-adversarial-reviewer").status, 0);
82
+ assert.equal(fs.readdirSync(path.join(temp, ".claude/agents")).length, pipeline.coreAgents.length);
83
+ } finally { fs.rmSync(temp, { recursive: true, force: true }); }
84
+ });
85
+
86
+ test("default install configures Opus coding and independent Fable review without erasing settings", () => {
87
+ const temp = fs.mkdtempSync(path.join(os.tmpdir(), "nccgs-astra-install-"));
88
+ try {
89
+ for (const marker of ["Assets", "Packages", "ProjectSettings", ".claude"]) fs.mkdirSync(path.join(temp, marker));
90
+ const settingsPath = path.join(temp, ".claude/settings.json");
91
+ fs.writeFileSync(settingsPath, JSON.stringify({ model: "sonnet", effortLevel: "medium", modelSettings: { "claude-opus-5-5": { effortLevel: "low" }, "claude-sonnet-4-6": { effortLevel: "medium" } }, permissions: { allow: ["Bash(git status*)"] }, env: { NCCGS_TEST: "preserved" } }));
92
+ const run = (...args) => spawnSync(process.execPath, [path.join(root, "scripts/cli.mjs"), ...args], { cwd: temp, encoding: "utf8" });
93
+ const dry = run("install", "--dry-run");
94
+ assert.equal(dry.status, 0, dry.stderr || dry.stdout);
95
+ assert.equal(JSON.parse(fs.readFileSync(settingsPath)).model, "sonnet");
96
+ assert.ok(!fs.existsSync(path.join(temp, ".claude/agents")));
97
+ const install = run("install");
98
+ assert.equal(install.status, 0, install.stderr || install.stdout);
99
+ const settings = JSON.parse(fs.readFileSync(settingsPath));
100
+ assert.equal(settings.model, "claude-opus-5-5");
101
+ assert.equal(settings.effortLevel, "xhigh");
102
+ assert.equal(settings.modelSettings["claude-opus-5-5"].effortLevel, "xhigh");
103
+ assert.equal(settings.modelSettings["claude-sonnet-4-6"].effortLevel, "medium");
104
+ assert.equal(settings.env.NCCGS_TEST, "preserved");
105
+ assert.deepEqual(settings.permissions.allow, ["Bash(git status*)"]);
106
+ for (const agent of studio.agents) {
107
+ if (!pipeline.coreAgents.includes(agent.name)) {
108
+ assert.ok(!fs.existsSync(path.join(temp, ".claude/agents", `${agent.name}.md`)));
109
+ continue;
110
+ }
111
+ const text = fs.readFileSync(path.join(temp, ".claude/agents", `${agent.name}.md`), "utf8");
112
+ const expected = agent.name === "nccgs-adversarial-reviewer" ? "fable" : "claude-opus-5-5";
113
+ assert.equal(text.match(/^model:\s*(\S+)/m)?.[1], expected, agent.name);
114
+ const expectedEffort = agent.name === "nccgs-adversarial-reviewer" ? "high" : "xhigh";
115
+ assert.equal(text.match(/^effort:\s*(\S+)/m)?.[1], expectedEffort, agent.name);
116
+ }
117
+ assert.match(fs.readFileSync(path.join(temp, ".nccgs/project.yaml"), "utf8"), /profile: astra-claude/);
118
+ assert.ok(fs.existsSync(path.join(temp, ".nccgs/templates/implementation-report.md")));
119
+ const update = run("update");
120
+ assert.equal(update.status, 0, update.stderr || update.stdout);
121
+ assert.match(update.stdout, /0 preserved override/);
122
+ const doctor = run("doctor");
123
+ assert.equal(doctor.status, 0, doctor.stderr || doctor.stdout);
124
+ } finally {
125
+ fs.rmSync(temp, { recursive: true, force: true });
126
+ }
127
+ });
128
+
129
+ test("profile changes clear effort pins and preserve skipped custom agents", () => {
130
+ const temp = fs.mkdtempSync(path.join(os.tmpdir(), "nccgs-effort-profile-"));
131
+ try {
132
+ fs.mkdirSync(path.join(temp, ".claude/nccgs"), { recursive: true });
133
+ fs.mkdirSync(path.join(temp, ".claude/agents"));
134
+ fs.mkdirSync(path.join(temp, ".nccgs"));
135
+ fs.copyFileSync(path.join(root, ".claude/nccgs/studio.json"), path.join(temp, ".claude/nccgs/studio.json"));
136
+ fs.writeFileSync(path.join(temp, ".nccgs/project.yaml"), "models:\n profile: astra-claude\n");
137
+ copyAgentFixture(temp);
138
+ const run = (profile, ...args) => spawnSync(process.execPath, [path.join(root, ".claude/nccgs/tools/configure-models.mjs"), "--project", temp, "--profile", profile, ...args], { encoding: "utf8" });
139
+ for (const profile of ["inherit", "quality", "balanced", "astra-claude"]) {
140
+ const result = run(profile);
141
+ assert.equal(result.status, 0, result.stderr || result.stdout);
142
+ for (const agent of studio.agents) {
143
+ if (profile === "astra-claude" && !pipeline.coreAgents.includes(agent.name)) {
144
+ assert.ok(!fs.existsSync(path.join(temp, ".claude/agents", `${agent.name}.md`)));
145
+ continue;
146
+ }
147
+ const text = fs.readFileSync(path.join(temp, ".claude/agents", `${agent.name}.md`), "utf8");
148
+ assert.equal(text.match(/^model:\s*(\S+)/m)?.[1] ?? null, studio.modelProfiles[profile][agent.modelClass], agent.name);
149
+ const expectedEffort = profile === "astra-claude"
150
+ ? (agent.name === "nccgs-adversarial-reviewer" ? "high" : "xhigh") : null;
151
+ assert.equal(text.match(/^effort:\s*(\S+)/m)?.[1] ?? null, expectedEffort, agent.name);
152
+ }
153
+ }
154
+ const file = path.join(temp, ".claude/agents/nccgs-adversarial-reviewer.md");
155
+ const before = fs.readFileSync(file, "utf8");
156
+ assert.equal(run("inherit", "--skip-path", ".claude/agents/nccgs-adversarial-reviewer.md").status, 0);
157
+ assert.equal(fs.readFileSync(file, "utf8"), before);
158
+ const invalid = run("unknown");
159
+ assert.notEqual(invalid.status, 0);
160
+ assert.equal(fs.readFileSync(file, "utf8"), before);
161
+ fs.writeFileSync(path.join(temp, ".claude/settings.json"), "invalid JSON");
162
+ assert.notEqual(run("astra-claude").status, 0);
163
+ assert.equal(fs.readFileSync(file, "utf8"), before);
164
+ const other = fs.readFileSync(path.join(temp, ".claude/agents/nccgs-unity-implementer.md"), "utf8");
165
+ assert.ok(!/^model:/m.test(other), "invalid settings must fail before rewriting agents");
166
+ } finally {
167
+ fs.rmSync(temp, { recursive: true, force: true });
168
+ }
169
+ });
170
+
30
171
  test("migrate-project defaults to a single full run", () => {
31
- const skill = fs.readFileSync(path.join(root, ".claude/skills/migrate-project/SKILL.md"), "utf8");
172
+ const skill = fs.readFileSync(path.join(root, ".claude/skills/migrate-project/references/legacy.md"), "utf8");
32
173
  assert.match(skill, /Default to `full`/);
33
174
  assert.match(skill, /review, plan, apply, and verify in one invocation/i);
34
175
  });
@@ -42,7 +183,7 @@ test("model configurator can remove and restore model pins", () => {
42
183
  fs.copyFileSync(path.join(root, ".claude/nccgs/studio.json"), path.join(temp, ".claude/nccgs/studio.json"));
43
184
  fs.copyFileSync(path.join(root, ".claude/nccgs/tools/configure-models.mjs"), path.join(temp, ".claude/nccgs/tools/configure-models.mjs"));
44
185
  fs.writeFileSync(path.join(temp, ".nccgs/project.yaml"), "models:\n profile: quality\n", "utf8");
45
- for (const agent of studio.agents) fs.copyFileSync(path.join(root, ".claude/agents", `${agent.name}.md`), path.join(temp, ".claude/agents", `${agent.name}.md`));
186
+ copyAgentFixture(temp);
46
187
  const tool = path.join(temp, ".claude/nccgs/tools/configure-models.mjs");
47
188
  assert.equal(spawnSync(process.execPath, [tool, "--project", temp, "--profile", "inherit"]).status, 0);
48
189
  assert.ok(!/^model:/m.test(fs.readFileSync(path.join(temp, ".claude/agents", `${studio.agents[0].name}.md`), "utf8")));
@@ -129,7 +270,7 @@ test("installer preserves project truth, merges settings, and applies inherit pr
129
270
  }
130
271
  });
131
272
 
132
- test("update replaces untouched managed files, blocks local edits, and doctor verifies state", () => {
273
+ test("update replaces untouched files, preserves local overrides, and doctor gates review", () => {
133
274
  const temp = fs.mkdtempSync(path.join(os.tmpdir(), "nccgs-idempotent-update-test-"));
134
275
  try {
135
276
  for (const marker of ["Assets", "Packages", "ProjectSettings"]) fs.mkdirSync(path.join(temp, marker));
@@ -148,20 +289,84 @@ test("update replaces untouched managed files, blocks local edits, and doctor ve
148
289
 
149
290
  const update = spawnSync(process.execPath, [path.join(root, "scripts/cli.mjs"), "update"], { cwd: temp, encoding: "utf8" });
150
291
  assert.equal(update.status, 0, update.stderr || update.stdout);
151
- assert.match(update.stdout, /1\.0\.0 -> 1\.1\.0/);
292
+ assert.match(update.stdout, new RegExp(`1\\.0\\.0 -> ${frameworkVersion.replace(/\./g, "\\.")}`));
152
293
  assert.match(update.stdout, /automatic update/);
153
294
  assert.match(update.stdout, /No project migration required/);
154
295
  assert.equal(fs.readFileSync(managedPath, "utf8"), fs.readFileSync(path.join(root, managedRelative), "utf8"));
155
296
 
156
- const doctor = spawnSync(process.execPath, [path.join(root, "scripts/cli.mjs"), "doctor"], { cwd: temp, encoding: "utf8" });
297
+ let doctor = spawnSync(process.execPath, [path.join(root, "scripts/cli.mjs"), "doctor"], { cwd: temp, encoding: "utf8" });
157
298
  assert.equal(doctor.status, 0, doctor.stderr || doctor.stdout);
158
299
  assert.match(doctor.stdout, /NCCGS doctor passed/);
159
300
 
160
301
  fs.writeFileSync(managedPath, "project-local override\n", "utf8");
161
- const blocked = spawnSync(process.execPath, [path.join(root, "scripts/cli.mjs"), "update", "--dry-run"], { cwd: temp, encoding: "utf8" });
162
- assert.equal(blocked.status, 2);
163
- assert.match(blocked.stderr, /modified/);
302
+ const preserved = spawnSync(process.execPath, [path.join(root, "scripts/cli.mjs"), "update"], { cwd: temp, encoding: "utf8" });
303
+ assert.equal(preserved.status, 0, preserved.stderr || preserved.stdout);
304
+ assert.match(preserved.stdout, /preserved override/);
305
+ assert.match(preserved.stdout, /Project migration required/);
164
306
  assert.equal(fs.readFileSync(managedPath, "utf8"), "project-local override\n");
307
+
308
+ const overridePath = path.join(temp, ".nccgs/framework-overrides.json");
309
+ const overrides = JSON.parse(fs.readFileSync(overridePath, "utf8"));
310
+ const override = overrides.overrides.find((item) => item.path === managedRelative);
311
+ assert.equal(override.status, "pending_review");
312
+ assert.equal(override.disposition, "preserve");
313
+
314
+ doctor = spawnSync(process.execPath, [path.join(root, "scripts/cli.mjs"), "doctor"], { cwd: temp, encoding: "utf8" });
315
+ assert.equal(doctor.status, 1);
316
+ assert.match(doctor.stderr, /awaits \/migrate-project review/);
317
+
318
+ const approve = spawnSync(process.execPath, [path.join(temp, ".claude/nccgs/tools/approve-overrides.mjs"), "--project", temp, "--approve-pending"], { cwd: temp, encoding: "utf8" });
319
+ assert.equal(approve.status, 0, approve.stderr || approve.stdout);
320
+ doctor = spawnSync(process.execPath, [path.join(root, "scripts/cli.mjs"), "doctor"], { cwd: temp, encoding: "utf8" });
321
+ assert.equal(doctor.status, 0, doctor.stderr || doctor.stdout);
322
+ } finally {
323
+ fs.rmSync(temp, { recursive: true, force: true });
324
+ }
325
+ });
326
+
327
+ test("doctor accepts a legacy CRLF raw hash after Git normalizes the file to LF", () => {
328
+ const temp = fs.mkdtempSync(path.join(os.tmpdir(), "nccgs-eol-test-"));
329
+ try {
330
+ for (const marker of ["Assets", "Packages", "ProjectSettings"]) fs.mkdirSync(path.join(temp, marker));
331
+ const install = spawnSync(process.execPath, [path.join(root, "scripts/cli.mjs"), "install"], { cwd: temp, encoding: "utf8" });
332
+ assert.equal(install.status, 0, install.stderr || install.stdout);
333
+
334
+ const relative = ".claude/nccgs/constitution.md";
335
+ const file = path.join(temp, ...relative.split("/"));
336
+ const lf = fs.readFileSync(file, "utf8").replace(/\r\n?/g, "\n");
337
+ fs.writeFileSync(file, lf, "utf8");
338
+ const statePath = path.join(temp, ".claude/nccgs/install-state.json");
339
+ const state = JSON.parse(fs.readFileSync(statePath, "utf8"));
340
+ const record = state.files.find((item) => item.relative === relative);
341
+ record.sha256 = sha256(lf.replace(/\n/g, "\r\n"));
342
+ delete record.canonicalSha256;
343
+ state.schemaVersion = 2;
344
+ fs.writeFileSync(statePath, `${JSON.stringify(state, null, 2)}\n`, "utf8");
345
+
346
+ const doctor = spawnSync(process.execPath, [path.join(root, "scripts/cli.mjs"), "doctor"], { cwd: temp, encoding: "utf8" });
347
+ assert.equal(doctor.status, 0, doctor.stderr || doctor.stdout);
348
+ } finally {
349
+ fs.rmSync(temp, { recursive: true, force: true });
350
+ }
351
+ });
352
+
353
+ test("update preserves an intentionally missing managed file until migration review", () => {
354
+ const temp = fs.mkdtempSync(path.join(os.tmpdir(), "nccgs-absent-override-test-"));
355
+ try {
356
+ for (const marker of ["Assets", "Packages", "ProjectSettings"]) fs.mkdirSync(path.join(temp, marker));
357
+ const install = spawnSync(process.execPath, [path.join(root, "scripts/cli.mjs"), "install"], { cwd: temp, encoding: "utf8" });
358
+ assert.equal(install.status, 0, install.stderr || install.stdout);
359
+
360
+ const relative = ".claude/skills/status/SKILL.md";
361
+ const file = path.join(temp, ...relative.split("/"));
362
+ fs.unlinkSync(file);
363
+ const update = spawnSync(process.execPath, [path.join(root, "scripts/cli.mjs"), "update"], { cwd: temp, encoding: "utf8" });
364
+ assert.equal(update.status, 0, update.stderr || update.stdout);
365
+ assert.ok(!fs.existsSync(file));
366
+ const manifest = JSON.parse(fs.readFileSync(path.join(temp, ".nccgs/framework-overrides.json"), "utf8"));
367
+ const override = manifest.overrides.find((item) => item.path === relative);
368
+ assert.equal(override.disposition, "absent");
369
+ assert.equal(override.status, "pending_review");
165
370
  } finally {
166
371
  fs.rmSync(temp, { recursive: true, force: true });
167
372
  }
@@ -0,0 +1,170 @@
1
+ import assert from "node:assert/strict";
2
+ import fs from "node:fs";
3
+ import os from "node:os";
4
+ import path from "node:path";
5
+ import { spawnSync } from "node:child_process";
6
+ import { fileURLToPath } from "node:url";
7
+ import test from "node:test";
8
+ import { initialize, snapshot, record, check, close, taskPath } from "../.claude/nccgs/tools/gates.mjs";
9
+ import { artifact, atomicJson, json, hash } from "../.claude/nccgs/tools/policy.mjs";
10
+ import { run } from "../.claude/nccgs/tools/task.mjs";
11
+ import { observe } from "../.claude/nccgs/tools/runtime.mjs";
12
+
13
+ const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
14
+ function fixture(options = {}) {
15
+ const project = fs.mkdtempSync(path.join(os.tmpdir(), "nccgs-gates-"));
16
+ const write = (name, value) => {
17
+ const file = path.join(project, name);
18
+ fs.mkdirSync(path.dirname(file), { recursive: true });
19
+ fs.writeFileSync(file, typeof value === "string" ? value : JSON.stringify(value));
20
+ return artifact(project, name);
21
+ };
22
+ for (const file of ["pipeline.json", "studio.json"]) write(`.claude/nccgs/${file}`, fs.readFileSync(path.join(root, ".claude/nccgs", file), "utf8"));
23
+ const policy = write(".nccgs/project.yaml", "schema_version: 2\nmodels:\n profile: astra-claude\n");
24
+ write(".nccgs/gates.json", { schemaVersion: 1, policySha256: policy.sha256, requiredChecks: ["regression"], requiredArtifacts: options.artifacts ?? [], manualGates: options.manual ?? [] });
25
+ write("Assets/game.cs", "// initial implementation\n");
26
+ write(".nccgs/plan.md", "Astra plan: preserve health invariant and verify regression.");
27
+ initialize(project, "feature", ".nccgs/plan.md");
28
+ let index = 0;
29
+ const snap = () => snapshot(project).sha256;
30
+ const task = () => json(taskPath(project, "feature"));
31
+ function runtime(stage, overrides = {}) {
32
+ const source = `.nccgs/evidence/raw-${++index}.json`;
33
+ write(source, { taskId: "feature", snapshot: snap(), sessionId: "session", agentId: stage,
34
+ observedModel: stage === "implementation" ? "claude-opus-5-5" : "claude-fable-5-1",
35
+ observedEffort: stage === "implementation" ? "xhigh" : "high", capturedAt: new Date().toISOString(),
36
+ status: "completed", provenance: { kind: "provider-log", reference: "test fixture: captured runtime request metadata" }, ...overrides });
37
+ return run(["observe", "--project", project, "--id", "feature", "--file", source]);
38
+ }
39
+ const impl = (overrides = {}) => record(project, "feature", "implementation", { snapshot: snap(), runtime: runtime("implementation"),
40
+ checks: [{ name: "regression", status: "PASS", evidence: write(`.nccgs/evidence/test-${++index}.log`, "fixture regression: PASS") }], ...overrides });
41
+ const review = (overrides = {}) => record(project, "feature", "review", { snapshot: snap(), runtime: runtime("review"), verdict: "PASS", criteriaMet: true, findings: [], evidence: write(`.nccgs/evidence/review-${++index}.md`, "Fixture independent review findings: none"), ...overrides });
42
+ const report = (overrides = {}) => record(project, "feature", "report", { snapshot: snap(), evidence: write(`.nccgs/evidence/report-${++index}.md`, "Fixture report, review reference and evidence"), ...overrides });
43
+ const approval = (stage, overrides = {}) => record(project, "feature", stage, { snapshot: snap(), evidence: write(`.nccgs/approvals/${stage}-${++index}.json`, {
44
+ actor: stage === "astra" ? "Astra" : "Product Owner", decision: stage === "astra" ? "PASS" : "ACCEPTED",
45
+ taskId: "feature", snapshot: snap(), decidedAt: new Date().toISOString(), statement: "Explicit TEST FIXTURE approval", reference: "test-only external decision event", ...overrides }) });
46
+ return { project, write, snap, task, runtime, impl, review, report, approval,
47
+ dispose() {
48
+ const resolved = path.resolve(project);
49
+ assert.ok(resolved.startsWith(path.resolve(os.tmpdir()) + path.sep + "nccgs-gates-"));
50
+ fs.rmSync(resolved, { recursive: true, force: true });
51
+ } };
52
+ }
53
+
54
+ test("end-to-end gates require Fable, Astra and explicit acceptance in order", () => {
55
+ const f = fixture();
56
+ try {
57
+ assert.throws(() => close(f.project, "feature"), /implementation: missing/);
58
+ assert.throws(() => f.report(), /implementation: missing/);
59
+ f.impl();
60
+ assert.throws(() => f.report(), /review: missing/);
61
+ f.review();
62
+ assert.equal(check(f.project, f.task(), "report").allowed, true);
63
+ f.report();
64
+ assert.throws(() => close(f.project, "feature"), /astra: missing/);
65
+ f.approval("astra");
66
+ assert.throws(() => close(f.project, "feature"), /acceptance: missing/);
67
+ f.approval("acceptance");
68
+ assert.equal(close(f.project, "feature").phase, "DONE");
69
+ } finally { f.dispose(); }
70
+ });
71
+
72
+ test("source additions, deletions and edits invalidate review and approvals", () => {
73
+ const f = fixture();
74
+ try {
75
+ f.impl(); f.review(); f.report(); f.approval("astra"); f.approval("acceptance"); close(f.project, "feature");
76
+ const before = f.snap();
77
+ f.write("Assets/new.cs", "untracked new source");
78
+ assert.notEqual(f.snap(), before);
79
+ assert.throws(() => close(f.project, "feature"), /stale/);
80
+ fs.unlinkSync(path.join(f.project, "Assets/game.cs"));
81
+ assert.throws(() => check(f.project, f.task(), "report"), /stale/);
82
+ f.impl();
83
+ assert.deepEqual(Object.keys(f.task().stages), ["implementation"]);
84
+ assert.equal(f.task().closed, undefined);
85
+ assert.ok(f.task().history.length > 0);
86
+ assert.throws(() => f.report(), /review: missing/);
87
+ } finally { f.dispose(); }
88
+ });
89
+
90
+ test("unknown effort, wrong model, fallback, and same-agent review cannot pass", () => {
91
+ const f = fixture();
92
+ try {
93
+ assert.throws(() => f.impl({ runtime: f.runtime("implementation", { observedEffort: "high" }) }), /effort/);
94
+ assert.throws(() => f.impl({ runtime: f.runtime("implementation", { observedModel: "claude-sonnet-4-6" }) }), /model/);
95
+ f.impl();
96
+ assert.throws(() => f.review({ runtime: f.runtime("review", { agentId: "implementation" }) }), /independent/);
97
+ assert.throws(() => f.review({ runtime: f.runtime("review", { modelsUsed: ["claude-fable-5-1", "claude-opus-5-5"] }) }), /fallback/);
98
+ const event = observe(f.project, { session_id: "session", tool_input: { subagent_type: "nccgs-adversarial-reviewer" }, tool_response: { resolvedModel: "claude-fable-5-1", status: "completed", agentId: "native-review" } }, "agent_result");
99
+ assert.equal(event.observedEffort, null);
100
+ assert.equal(event.requestedEffort, "high");
101
+ assert.ok(event.missing.includes("effort"));
102
+ const ref = f.write(".nccgs/evidence/unknown-effort.json", { ...event, taskId: "feature", snapshot: f.snap() });
103
+ assert.throws(() => f.review({ runtime: ref }), /effort/);
104
+ } finally { f.dispose(); }
105
+ });
106
+
107
+ test("negative review and rejection remain blocking; amended evidence invalidates results", () => {
108
+ const f = fixture();
109
+ try {
110
+ f.impl(); f.review({ verdict: "CHANGES_REQUIRED", findings: [{ severity: "HIGH", status: "OPEN" }] });
111
+ assert.throws(() => f.report(), /not passed/);
112
+ assert.throws(() => f.review({ findings: [{ severity: "HIGH", status: "OPEN" }] }), /blocking findings/);
113
+ f.review(); f.report(); f.approval("astra"); f.approval("acceptance");
114
+ f.approval("acceptance", { decision: "REJECTED" });
115
+ assert.throws(() => close(f.project, "feature"), /explicit Product Owner/);
116
+ const evidence = f.task().stages.review.evidence;
117
+ f.write(evidence.path, "altered review");
118
+ assert.throws(() => check(f.project, f.task(), "report"), /stale artifact/);
119
+ } finally { f.dispose(); }
120
+ });
121
+
122
+ test("required checks, closure artifacts, manual gates and policy hashes are enforced", () => {
123
+ const f = fixture({ artifacts: [".nccgs/evidence/build.log"], manual: ["playtest"] });
124
+ try {
125
+ assert.throws(() => f.impl({ checks: [{ name: "unrelated", status: "PASS", evidence: f.write(".nccgs/evidence/unrelated.log", "PASS") }] }), /Required check missing/);
126
+ f.impl(); f.review();
127
+ assert.throws(() => f.report(), /closure artifact missing/);
128
+ f.report({ artifacts: [f.write(".nccgs/evidence/build.log", "Build successful")] });
129
+ f.approval("astra");
130
+ assert.throws(() => f.approval("acceptance"), /Manual gate missing/);
131
+ f.approval("acceptance", { gates: [{ id: "playtest", decision: "ACCEPTED", reference: "test fixture playtest decision" }] });
132
+ close(f.project, "feature");
133
+ f.write(".nccgs/plan.md", "Changed plan");
134
+ assert.throws(() => close(f.project, "feature"), /stale artifact/);
135
+ } finally { f.dispose(); }
136
+ });
137
+
138
+ test("path traversal and premature/fabricated external sign-offs fail", () => {
139
+ const f = fixture();
140
+ try {
141
+ assert.throws(() => taskPath(f.project, "../escape"), /Invalid task/);
142
+ assert.throws(() => artifact(f.project, "../outside.txt"), /escapes/);
143
+ f.impl(); f.review(); f.report();
144
+ assert.throws(() => f.approval("astra", { actor: "Claude" }), /explicit Astra/);
145
+ assert.throws(() => f.approval("astra", { decidedAt: "2000-01-01T00:00:00.000Z" }), /predates/);
146
+ const old = process.env.CLAUDECODE;
147
+ try { process.env.CLAUDECODE = "1"; assert.throws(() => f.approval("astra"), /outside the Claude/); }
148
+ finally { if (old === undefined) delete process.env.CLAUDECODE; else process.env.CLAUDECODE = old; }
149
+ const guard = spawnSync(process.execPath, [path.join(root, ".claude/nccgs/hooks/closure-guard.mjs"), "pre"], {
150
+ cwd: f.project, encoding: "utf8", input: JSON.stringify({ session_id: "session", tool_input: { file_path: path.join(f.project, ".nccgs/state.md"), content: "Final state: DONE" } }) });
151
+ assert.equal(guard.status, 2);
152
+ assert.match(guard.stderr, /bound to an NCCGS task/);
153
+ } finally { f.dispose(); }
154
+ });
155
+
156
+ test("verification runner captures real exit status and rejects changed logs", () => {
157
+ const f = fixture();
158
+ try {
159
+ const execute = source => run(["verify", "--project", f.project, "--id", "feature", "--name", "regression", "--", process.execPath, "-e", source]);
160
+ const failed = execute("process.exit(3)");
161
+ assert.equal(failed.status, "FAIL");
162
+ assert.throws(() => f.impl({ checks: [failed] }), /failed\/unproven/);
163
+ const passed = execute("console.log('fixture verification passed')");
164
+ assert.equal(passed.status, "PASS");
165
+ f.impl({ checks: [passed] });
166
+ const execution = json(path.join(f.project, passed.evidence.path));
167
+ f.write(execution.log.path, "tampered log");
168
+ assert.throws(() => check(f.project, f.task(), "review"), /stale artifact/);
169
+ } finally { f.dispose(); }
170
+ });