nccgs 2.0.0 → 2.1.1

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 (82) hide show
  1. package/.claude/agents/nccgs-adversarial-reviewer.md +1 -1
  2. package/.claude/agents/nccgs-engine-programmer.md +1 -1
  3. package/.claude/agents/nccgs-fast-implementer.md +1 -1
  4. package/.claude/agents/nccgs-game-designer.md +1 -1
  5. package/.claude/agents/nccgs-gameplay-programmer.md +1 -1
  6. package/.claude/agents/nccgs-qa-engineer.md +1 -1
  7. package/.claude/agents/nccgs-task-scout.md +1 -1
  8. package/.claude/agents/nccgs-ui-programmer.md +1 -1
  9. package/.claude/agents/nccgs-unity-build-specialist.md +1 -1
  10. package/.claude/agents/nccgs-unity-implementer.md +1 -1
  11. package/.claude/agents/nccgs-unity-systems-specialist.md +1 -1
  12. package/.claude/agents/nccgs-unity-ui-specialist.md +1 -1
  13. package/.claude/agents/nccgs-verification-engineer.md +1 -1
  14. package/.claude/nccgs/MEMORY.md +9 -3
  15. package/.claude/nccgs/VERSION +1 -1
  16. package/.claude/nccgs/constitution.md +187 -176
  17. package/.claude/nccgs/hooks/closure-guard.mjs +2 -0
  18. package/.claude/nccgs/hooks/unity-transport-guard.mjs +11 -0
  19. package/.claude/nccgs/pipeline.json +260 -260
  20. package/.claude/nccgs/protocols/context-packets.md +5 -0
  21. package/.claude/nccgs/protocols/model-routing.md +46 -46
  22. package/.claude/nccgs/protocols/review-handoff.md +21 -1
  23. package/.claude/nccgs/protocols/shared-workflow-context.md +33 -0
  24. package/.claude/nccgs/protocols/unity-boundary.md +20 -0
  25. package/.claude/nccgs/routing.generated.md +4 -4
  26. package/.claude/nccgs/settings.fragment.json +84 -24
  27. package/.claude/nccgs/studio.json +53 -53
  28. package/.claude/nccgs/tools/autonomy.mjs +13 -0
  29. package/.claude/nccgs/tools/command-tokens.mjs +22 -0
  30. package/.claude/nccgs/tools/compile-policy.mjs +4 -0
  31. package/.claude/nccgs/tools/configure-models.mjs +4 -0
  32. package/.claude/nccgs/tools/hook-transport.mjs +34 -0
  33. package/.claude/nccgs/tools/installation-guard.mjs +55 -0
  34. package/.claude/nccgs/tools/native-completion.mjs +10 -1
  35. package/.claude/nccgs/tools/returned-handoff.mjs +11 -2
  36. package/.claude/nccgs/tools/shared-context-guard.mjs +60 -0
  37. package/.claude/nccgs/tools/shared-context.mjs +25 -0
  38. package/.claude/nccgs/tools/tool-guard.mjs +120 -131
  39. package/.claude/nccgs/tools/unity-transport.mjs +170 -0
  40. package/.claude/nccgs/workflow-catalog.json +4 -4
  41. package/CHANGELOG.md +235 -165
  42. package/CLAUDE.md +10 -8
  43. package/MEMORY.md +109 -56
  44. package/README.md +206 -168
  45. package/UPGRADING.md +130 -112
  46. package/VERSION +1 -1
  47. package/docs/ARCHITECTURE.md +131 -131
  48. package/docs/ASSET-PRODUCTION-GRAPHIFY-PLAN.md +230 -0
  49. package/docs/HUONG-DAN-MIGRATE-VA-SU-DUNG.md +117 -87
  50. package/docs/NCCGS-1.5.md +237 -237
  51. package/docs/PROJECT-POLICY.md +94 -94
  52. package/docs/RELEASE-STATUS.md +32 -7
  53. package/docs/UNINSTALL.md +134 -0
  54. package/docs/WFK17-HOOK-REPAIR.md +71 -0
  55. package/docs/WORKFLOWS.md +85 -85
  56. package/package.json +1 -1
  57. package/scaffold/.nccgs/project.yaml +109 -108
  58. package/scripts/benchmark-v15.mjs +173 -173
  59. package/scripts/cli.mjs +11 -2
  60. package/scripts/doctor.mjs +261 -261
  61. package/scripts/install.mjs +530 -515
  62. package/scripts/recover-session.mjs +89 -0
  63. package/scripts/run.mjs +22 -9
  64. package/scripts/supervise.mjs +4 -0
  65. package/scripts/uninstall.mjs +292 -0
  66. package/scripts/validate.mjs +285 -278
  67. package/tests/autonomy.test.mjs +126 -113
  68. package/tests/design-readiness.test.mjs +199 -199
  69. package/tests/fixtures/compiler-shutdown.cs +67 -64
  70. package/tests/fixtures/native-provider.mjs +61 -0
  71. package/tests/framework.test.mjs +740 -740
  72. package/tests/hook-repair.test.mjs +106 -0
  73. package/tests/lifecycle-v15.test.mjs +346 -300
  74. package/tests/native-completion-notices.test.mjs +181 -0
  75. package/tests/phase1.test.mjs +427 -416
  76. package/tests/phase2.test.mjs +250 -250
  77. package/tests/policy-v15.test.mjs +234 -234
  78. package/tests/returned-handoff-notices.test.mjs +170 -0
  79. package/tests/runtime-v15.test.mjs +1197 -1191
  80. package/tests/task-slices.test.mjs +285 -285
  81. package/tests/uninstall.test.mjs +477 -0
  82. package/tests/unity-transport.test.mjs +348 -0
@@ -1,234 +1,234 @@
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 test, { after, before } from "node:test";
7
- import { fileURLToPath } from "node:url";
8
- import { executionPolicy, resolveExecution } from "../.claude/nccgs/tools/policy.mjs";
9
- import { compile, generated } from "../.claude/nccgs/tools/compile-policy.mjs";
10
-
11
- const source = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
12
- const fixtures = [];
13
- let packageRoot;
14
- const temporary = () => { const file = fs.mkdtempSync(path.join(os.tmpdir(), "nccgs-v15-policy-")); fixtures.push(file); return file; };
15
- const write = (project, relative, value) => {
16
- const file = path.join(project, relative); fs.mkdirSync(path.dirname(file), { recursive: true });
17
- fs.writeFileSync(file, typeof value === "string" ? value : JSON.stringify(value, null, 2));
18
- };
19
- const read = (project, relative) => JSON.parse(fs.readFileSync(path.join(project, relative), "utf8"));
20
- const run = (project, ...args) => spawnSync(process.execPath, [path.join(packageRoot, "scripts/cli.mjs"), ...args], { cwd: project, encoding: "utf8" });
21
- const succeeds = result => assert.equal(result.status, 0, `${result.stdout}\n${result.stderr}`);
22
- const project = () => { const directory = temporary(); for (const marker of ["Assets", "Packages", "ProjectSettings"]) fs.mkdirSync(path.join(directory, marker)); return directory; };
23
- before(() => {
24
- packageRoot = temporary();
25
- fs.cpSync(source, packageRoot, { recursive: true });
26
- // Generate only this disposable package fixture. Source integration belongs to
27
- // the root build step, not tests or installer mutation.
28
- compile(packageRoot, true);
29
- });
30
- after(() => {
31
- for (const file of fixtures.reverse()) {
32
- assert.ok(path.resolve(file).startsWith(path.resolve(os.tmpdir()) + path.sep + "nccgs-v15-policy-"));
33
- fs.rmSync(file, { recursive: true, force: true });
34
- }
35
- });
36
-
37
- test("execution policy validates modes, types and compatibility obligations", () => {
38
- for (const mode of ["off", "advisory", "required"]) {
39
- const result = executionPolicy({ schemaVersion: 1, name: "standalone", externalReview: { mode } });
40
- assert.equal(result.externalReview.mode, mode);
41
- assert.equal(result.acceptanceRequired, false);
42
- assert.equal(result.requireObservedEffort, false);
43
- assert.equal(result.maxParallelWorkers, 2);
44
- }
45
- for (const value of [null, [], {}, { schemaVersion: 2, name: "standalone" },
46
- { schemaVersion: 1, name: "other" }, { schemaVersion: 1, name: "standalone", unexpected: true },
47
- { schemaVersion: 1, name: "standalone", externalReview: { mode: "auto" } },
48
- { schemaVersion: 1, name: "standalone", externalReview: null },
49
- { schemaVersion: 1, name: "standalone", externalReview: { mode: "off", actor: "Astra" } },
50
- { schemaVersion: 1, name: "standalone", acceptanceRequired: "false" },
51
- { schemaVersion: 1, name: "standalone", requireObservedEffort: null },
52
- { schemaVersion: 1, name: "standalone", maxParallelWorkers: 5 },
53
- { schemaVersion: 1, name: "standalone", maxParallelWorkers: 0 },
54
- { schemaVersion: 1, name: "standalone", maxParallelWorkers: null },
55
- { schemaVersion: 1, name: "compatibility", externalReview: { mode: "off" } },
56
- { schemaVersion: 1, name: "compatibility", acceptanceRequired: false }]) assert.throws(() => executionPolicy(value));
57
- });
58
-
59
- test("missing execution config resolves compatibility independently of model profile", () => {
60
- const target = project();
61
- fs.cpSync(path.join(packageRoot, ".claude/nccgs"), path.join(target, ".claude/nccgs"), { recursive: true });
62
- write(target, ".nccgs/project.yaml", "schema_version: 2\nmodels:\n profile: claude-standalone\n");
63
- const policy = resolveExecution(target);
64
- assert.equal(policy.name, "compatibility");
65
- assert.equal(policy.profileName, "claude-standalone");
66
- assert.equal(policy.externalReview.mode, "required");
67
- assert.equal(policy.acceptanceRequired, true);
68
- assert.equal(policy.requireObservedEffort, true);
69
- assert.equal(policy.maxParallelWorkers, 4);
70
- });
71
-
72
- test("legacy YAML without model selection resolves preserved metadata before package defaults", () => {
73
- const target = project();
74
- fs.cpSync(path.join(packageRoot, ".claude/nccgs"), path.join(target, ".claude/nccgs"), { recursive: true });
75
- const legacy = "schema_version: 1\nownership:\n protected_paths:\n - Assets/HandTuned/**\n";
76
- write(target, ".nccgs/project.yaml", legacy);
77
- write(target, ".nccgs/agent-activation.json", { schemaVersion: 1, profile: "quality" });
78
- assert.equal(resolveExecution(target).profileName, "quality");
79
- write(target, ".claude/nccgs/install-state.json", { schemaVersion: 3, modelProfile: "inherit" });
80
- assert.equal(resolveExecution(target).profileName, "inherit");
81
- assert.equal(resolveExecution(target).name, "compatibility");
82
- assert.equal(fs.readFileSync(path.join(target, ".nccgs/project.yaml"), "utf8"), legacy);
83
- write(target, ".nccgs/project.yaml", legacy + "models:\n profile: balanced\n");
84
- assert.equal(resolveExecution(target).profileName, "balanced");
85
- });
86
-
87
- test("compiler separates standalone ownership from preserved Astra contract", () => {
88
- const outputs = generated(packageRoot);
89
- const catalog = JSON.parse(outputs.get(".claude/nccgs/workflow-catalog.json"));
90
- assert.equal(catalog.executionContracts.standalone.planningOwner, "Claude Code");
91
- assert.equal(catalog.executionContracts.compatibility.planningOwner, "Astra");
92
- assert.equal(catalog.profileContracts["astra-claude"].reviewModel, "fable");
93
- assert.equal(catalog.profileContracts["claude-standalone"].reviewModel, "opus");
94
- for (const workflow of catalog.workflows) assert.equal(workflow.ownerByExecution.standalone, "Claude Code");
95
- assert.match(outputs.get(".claude/agents/nccgs-adversarial-reviewer.md"), /^model: opus$/m);
96
- assert.doesNotMatch(outputs.get(".claude/agents/nccgs-adversarial-reviewer.md"), /^model: fable$/m);
97
- });
98
-
99
- test("generated turn ceilings track the role template and stale documentation is detected", () => {
100
- const target = temporary();
101
- fs.cpSync(packageRoot, target, { recursive: true });
102
- const role = ".claude/nccgs/agent-library/nccgs-adversarial-reviewer.md";
103
- const template = fs.readFileSync(path.join(target, role), "utf8");
104
- write(target, role, template.replace(/^maxTurns: \d+$/m, "maxTurns: 199"));
105
- const outputs = generated(target), routing = outputs.get(".claude/nccgs/routing.generated.md");
106
- assert.match(routing, /\| nccgs-adversarial-reviewer \| 199 \|/);
107
- assert.match(outputs.get(".claude/agents/nccgs-adversarial-reviewer.md"), /^maxTurns: 199$/m);
108
- assert.ok(compile(target).includes(".claude/nccgs/routing.generated.md"));
109
- compile(target, true);
110
- assert.deepEqual(compile(target), []);
111
- });
112
-
113
- test("new install defaults to standalone without erasing custom settings and hooks", () => {
114
- const target = project();
115
- const custom = { matcher: "Read", hooks: [{ type: "command", command: "echo project-owned" }] };
116
- write(target, ".claude/settings.json", { env: { KEEP: "yes" }, hooks: { PreToolUse: [custom] }, permissions: { allow: ["Read(./docs/**)"] } });
117
- succeeds(run(target, "install", "--dry-run"));
118
- assert.equal(fs.existsSync(path.join(target, ".nccgs/execution.json")), false);
119
- succeeds(run(target, "install"));
120
- const policy = resolveExecution(target);
121
- assert.equal(policy.name, "standalone");
122
- assert.equal(policy.profileName, "claude-standalone");
123
- assert.equal(policy.externalReview.mode, "off");
124
- assert.deepEqual(policy.profile.models.standard, { model: "opus", effort: "medium" });
125
- const settings = read(target, ".claude/settings.json");
126
- assert.equal(settings.env.KEEP, "yes");
127
- assert.ok(settings.hooks.PreToolUse.some(item => JSON.stringify(item) === JSON.stringify(custom)));
128
- assert.ok(settings.permissions.allow.includes("Read(./docs/**)"));
129
- assert.equal(settings.model, "opus");
130
- succeeds(run(target, "doctor"));
131
- });
132
-
133
- test("update preserves missing legacy execution policy and exact legacy model routes", () => {
134
- const target = project();
135
- succeeds(run(target, "install", "--model-profile", "astra-claude"));
136
- fs.unlinkSync(path.join(target, ".nccgs/execution.json"));
137
- const historical = { schemaVersion: 1, id: "pending-legacy", stages: { plan: { actor: "Astra" } }, reviewBudget: { used: 2, limit: 3 } };
138
- write(target, ".nccgs/tasks/pending-legacy.json", historical);
139
- succeeds(run(target, "update"));
140
- assert.equal(fs.existsSync(path.join(target, ".nccgs/execution.json")), false);
141
- assert.equal(resolveExecution(target).name, "compatibility");
142
- assert.equal(resolveExecution(target).profileName, "astra-claude");
143
- assert.equal(read(target, ".claude/settings.json").model, "claude-opus-5-5");
144
- assert.deepEqual(read(target, ".nccgs/tasks/pending-legacy.json"), historical);
145
- });
146
-
147
- test("update preserves required external review and custom agent content", () => {
148
- const target = project();
149
- succeeds(run(target, "install"));
150
- const execution = { schemaVersion: 1, name: "standalone", externalReview: { mode: "required" }, acceptanceRequired: true, maxParallelWorkers: 1 };
151
- write(target, ".nccgs/execution.json", execution);
152
- const agent = ".claude/agents/nccgs-gameplay-programmer.md";
153
- const content = fs.readFileSync(path.join(target, agent), "utf8") + "\nProject-owned convention.\n";
154
- write(target, agent, content);
155
- succeeds(run(target, "update"));
156
- assert.deepEqual(read(target, ".nccgs/execution.json"), execution);
157
- assert.equal(fs.readFileSync(path.join(target, agent), "utf8"), content);
158
- assert.equal(read(target, ".nccgs/framework-overrides.json").overrides.find(item => item.path === agent).status, "pending_review");
159
- });
160
-
161
- test("execution migration previews, preserves task bytes and rolls back configuration", () => {
162
- const target = project();
163
- succeeds(run(target, "install", "--model-profile", "astra-claude"));
164
- fs.unlinkSync(path.join(target, ".nccgs/execution.json"));
165
- const taskText = '{"schemaVersion":1,"id":"active","reviewBudget":{"used":2,"limit":3},"stages":{}}\n';
166
- write(target, ".nccgs/tasks/active.json", taskText);
167
- const oldPolicy = fs.readFileSync(path.join(target, ".nccgs/project.yaml"), "utf8");
168
- const oldSettings = fs.readFileSync(path.join(target, ".claude/settings.json"), "utf8");
169
- const preview = run(target, "migrate-execution", "--to", "standalone", "--model-profile", "claude-standalone", "--dry-run");
170
- succeeds(preview);
171
- assert.match(preview.stdout, /Active tasks \(1\): active/);
172
- assert.equal(fs.existsSync(path.join(target, ".nccgs/backups/execution")), false);
173
- const applied = run(target, "migrate-execution", "--to", "standalone", "--model-profile", "claude-standalone", "--external-review", "advisory");
174
- succeeds(applied);
175
- const id = applied.stdout.match(/Execution migration ([0-9a-f-]{36}) applied/)[1];
176
- assert.equal(resolveExecution(target).externalReview.mode, "advisory");
177
- assert.equal(resolveExecution(target).profileName, "claude-standalone");
178
- assert.equal(fs.readFileSync(path.join(target, ".nccgs/tasks/active.json"), "utf8"), taskText);
179
- succeeds(run(target, "doctor"));
180
- succeeds(run(target, "migrate-execution", "--rollback", id, "--dry-run"));
181
- assert.equal(resolveExecution(target).name, "standalone");
182
- succeeds(run(target, "migrate-execution", "--rollback", id));
183
- assert.equal(resolveExecution(target).name, "compatibility");
184
- assert.equal(fs.readFileSync(path.join(target, ".nccgs/project.yaml"), "utf8"), oldPolicy);
185
- assert.equal(fs.readFileSync(path.join(target, ".claude/settings.json"), "utf8"), oldSettings);
186
- assert.equal(fs.readFileSync(path.join(target, ".nccgs/tasks/active.json"), "utf8"), taskText);
187
- succeeds(run(target, "doctor"));
188
- });
189
-
190
- test("rollback refuses changed configuration and backup tampering before writes", () => {
191
- const target = project();
192
- succeeds(run(target, "install"));
193
- const migration = run(target, "migrate-execution", "--to", "standalone", "--external-review", "required");
194
- succeeds(migration);
195
- assert.equal(resolveExecution(target).designReviewRequired, true, "changing external review must preserve the reviewed-design gate");
196
- const id = migration.stdout.match(/Execution migration ([0-9a-f-]{36}) applied/)[1];
197
- const settings = fs.readFileSync(path.join(target, ".claude/settings.json"), "utf8");
198
- write(target, ".claude/settings.json", settings + "\n");
199
- let result = run(target, "migrate-execution", "--rollback", id);
200
- assert.notEqual(result.status, 0);
201
- assert.match(result.stderr, /Configuration changed after migration/);
202
- assert.equal(resolveExecution(target).externalReview.mode, "required");
203
- write(target, ".claude/settings.json", settings);
204
- write(target, `.nccgs/backups/execution/${id}/files/.nccgs/execution.json`, "tampered");
205
- result = run(target, "migrate-execution", "--rollback", id);
206
- assert.notEqual(result.status, 0);
207
- assert.match(result.stderr, /backup missing or changed/);
208
- assert.equal(resolveExecution(target).externalReview.mode, "required");
209
- });
210
-
211
- test("doctor rejects malformed execution configuration", () => {
212
- const target = project();
213
- succeeds(run(target, "install"));
214
- write(target, ".nccgs/execution.json", { schemaVersion: 1, name: "standalone", externalReview: { mode: "skip" } });
215
- const result = run(target, "doctor");
216
- assert.notEqual(result.status, 0);
217
- assert.match(result.stderr, /Invalid execution policy/);
218
- });
219
-
220
- test("failed model migration restores prior execution and settings without touching tasks", () => {
221
- const target = project();
222
- succeeds(run(target, "install", "--model-profile", "astra-claude"));
223
- fs.unlinkSync(path.join(target, ".nccgs/execution.json"));
224
- write(target, ".claude/settings.json", "malformed-settings");
225
- const taskText = '{"id":"failed-migration","schemaVersion":1,"reviewBudget":{"used":2,"limit":3},"stages":{}}';
226
- write(target, ".nccgs/tasks/failed-migration.json", taskText);
227
- const result = run(target, "migrate-execution", "--to", "standalone", "--model-profile", "claude-standalone");
228
- assert.notEqual(result.status, 0);
229
- assert.equal(resolveExecution(target).name, "compatibility");
230
- assert.equal(fs.readFileSync(path.join(target, ".claude/settings.json"), "utf8"), "malformed-settings");
231
- assert.equal(fs.readFileSync(path.join(target, ".nccgs/tasks/failed-migration.json"), "utf8"), taskText);
232
- const archive = fs.readdirSync(path.join(target, ".nccgs/backups/execution"))[0];
233
- assert.equal(read(target, `.nccgs/backups/execution/${archive}/manifest.json`).status, "failed-restored");
234
- });
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 test, { after, before } from "node:test";
7
+ import { fileURLToPath } from "node:url";
8
+ import { executionPolicy, resolveExecution } from "../.claude/nccgs/tools/policy.mjs";
9
+ import { compile, generated } from "../.claude/nccgs/tools/compile-policy.mjs";
10
+
11
+ const source = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
12
+ const fixtures = [];
13
+ let packageRoot;
14
+ const temporary = () => { const file = fs.mkdtempSync(path.join(os.tmpdir(), "nccgs-v15-policy-")); fixtures.push(file); return file; };
15
+ const write = (project, relative, value) => {
16
+ const file = path.join(project, relative); fs.mkdirSync(path.dirname(file), { recursive: true });
17
+ fs.writeFileSync(file, typeof value === "string" ? value : JSON.stringify(value, null, 2));
18
+ };
19
+ const read = (project, relative) => JSON.parse(fs.readFileSync(path.join(project, relative), "utf8"));
20
+ const run = (project, ...args) => spawnSync(process.execPath, [path.join(packageRoot, "scripts/cli.mjs"), ...args], { cwd: project, encoding: "utf8" });
21
+ const succeeds = result => assert.equal(result.status, 0, `${result.stdout}\n${result.stderr}`);
22
+ const project = () => { const directory = temporary(); for (const marker of ["Assets", "Packages", "ProjectSettings"]) fs.mkdirSync(path.join(directory, marker)); return directory; };
23
+ before(() => {
24
+ packageRoot = temporary();
25
+ fs.cpSync(source, packageRoot, { recursive: true });
26
+ // Generate only this disposable package fixture. Source integration belongs to
27
+ // the root build step, not tests or installer mutation.
28
+ compile(packageRoot, true);
29
+ });
30
+ after(() => {
31
+ for (const file of fixtures.reverse()) {
32
+ assert.ok(path.resolve(file).startsWith(path.resolve(os.tmpdir()) + path.sep + "nccgs-v15-policy-"));
33
+ fs.rmSync(file, { recursive: true, force: true });
34
+ }
35
+ });
36
+
37
+ test("execution policy validates modes, types and compatibility obligations", () => {
38
+ for (const mode of ["off", "advisory", "required"]) {
39
+ const result = executionPolicy({ schemaVersion: 1, name: "standalone", externalReview: { mode } });
40
+ assert.equal(result.externalReview.mode, mode);
41
+ assert.equal(result.acceptanceRequired, false);
42
+ assert.equal(result.requireObservedEffort, false);
43
+ assert.equal(result.maxParallelWorkers, 2);
44
+ }
45
+ for (const value of [null, [], {}, { schemaVersion: 2, name: "standalone" },
46
+ { schemaVersion: 1, name: "other" }, { schemaVersion: 1, name: "standalone", unexpected: true },
47
+ { schemaVersion: 1, name: "standalone", externalReview: { mode: "auto" } },
48
+ { schemaVersion: 1, name: "standalone", externalReview: null },
49
+ { schemaVersion: 1, name: "standalone", externalReview: { mode: "off", actor: "Astra" } },
50
+ { schemaVersion: 1, name: "standalone", acceptanceRequired: "false" },
51
+ { schemaVersion: 1, name: "standalone", requireObservedEffort: null },
52
+ { schemaVersion: 1, name: "standalone", maxParallelWorkers: 5 },
53
+ { schemaVersion: 1, name: "standalone", maxParallelWorkers: 0 },
54
+ { schemaVersion: 1, name: "standalone", maxParallelWorkers: null },
55
+ { schemaVersion: 1, name: "compatibility", externalReview: { mode: "off" } },
56
+ { schemaVersion: 1, name: "compatibility", acceptanceRequired: false }]) assert.throws(() => executionPolicy(value));
57
+ });
58
+
59
+ test("missing execution config resolves compatibility independently of model profile", () => {
60
+ const target = project();
61
+ fs.cpSync(path.join(packageRoot, ".claude/nccgs"), path.join(target, ".claude/nccgs"), { recursive: true });
62
+ write(target, ".nccgs/project.yaml", "schema_version: 2\nmodels:\n profile: claude-standalone\n");
63
+ const policy = resolveExecution(target);
64
+ assert.equal(policy.name, "compatibility");
65
+ assert.equal(policy.profileName, "claude-standalone");
66
+ assert.equal(policy.externalReview.mode, "required");
67
+ assert.equal(policy.acceptanceRequired, true);
68
+ assert.equal(policy.requireObservedEffort, true);
69
+ assert.equal(policy.maxParallelWorkers, 4);
70
+ });
71
+
72
+ test("legacy YAML without model selection resolves preserved metadata before package defaults", () => {
73
+ const target = project();
74
+ fs.cpSync(path.join(packageRoot, ".claude/nccgs"), path.join(target, ".claude/nccgs"), { recursive: true });
75
+ const legacy = "schema_version: 1\nownership:\n protected_paths:\n - Assets/HandTuned/**\n";
76
+ write(target, ".nccgs/project.yaml", legacy);
77
+ write(target, ".nccgs/agent-activation.json", { schemaVersion: 1, profile: "quality" });
78
+ assert.equal(resolveExecution(target).profileName, "quality");
79
+ write(target, ".claude/nccgs/install-state.json", { schemaVersion: 3, modelProfile: "inherit" });
80
+ assert.equal(resolveExecution(target).profileName, "inherit");
81
+ assert.equal(resolveExecution(target).name, "compatibility");
82
+ assert.equal(fs.readFileSync(path.join(target, ".nccgs/project.yaml"), "utf8"), legacy);
83
+ write(target, ".nccgs/project.yaml", legacy + "models:\n profile: balanced\n");
84
+ assert.equal(resolveExecution(target).profileName, "balanced");
85
+ });
86
+
87
+ test("compiler separates standalone ownership from preserved Astra contract", () => {
88
+ const outputs = generated(packageRoot);
89
+ const catalog = JSON.parse(outputs.get(".claude/nccgs/workflow-catalog.json"));
90
+ assert.equal(catalog.executionContracts.standalone.planningOwner, "Claude Code");
91
+ assert.equal(catalog.executionContracts.compatibility.planningOwner, "Astra");
92
+ assert.equal(catalog.profileContracts["astra-claude"].reviewModel, "fable");
93
+ assert.equal(catalog.profileContracts["claude-standalone"].reviewModel, "fable");
94
+ for (const workflow of catalog.workflows) assert.equal(workflow.ownerByExecution.standalone, "Claude Code");
95
+ assert.match(outputs.get(".claude/agents/nccgs-adversarial-reviewer.md"), /^model: fable$/m);
96
+ assert.doesNotMatch(outputs.get(".claude/agents/nccgs-adversarial-reviewer.md"), /^model: opus$/m);
97
+ });
98
+
99
+ test("generated turn ceilings track the role template and stale documentation is detected", () => {
100
+ const target = temporary();
101
+ fs.cpSync(packageRoot, target, { recursive: true });
102
+ const role = ".claude/nccgs/agent-library/nccgs-adversarial-reviewer.md";
103
+ const template = fs.readFileSync(path.join(target, role), "utf8");
104
+ write(target, role, template.replace(/^maxTurns: \d+$/m, "maxTurns: 199"));
105
+ const outputs = generated(target), routing = outputs.get(".claude/nccgs/routing.generated.md");
106
+ assert.match(routing, /\| nccgs-adversarial-reviewer \| 199 \|/);
107
+ assert.match(outputs.get(".claude/agents/nccgs-adversarial-reviewer.md"), /^maxTurns: 199$/m);
108
+ assert.ok(compile(target).includes(".claude/nccgs/routing.generated.md"));
109
+ compile(target, true);
110
+ assert.deepEqual(compile(target), []);
111
+ });
112
+
113
+ test("new install defaults to standalone without erasing custom settings and hooks", () => {
114
+ const target = project();
115
+ const custom = { matcher: "Read", hooks: [{ type: "command", command: "echo project-owned" }] };
116
+ write(target, ".claude/settings.json", { env: { KEEP: "yes" }, hooks: { PreToolUse: [custom] }, permissions: { allow: ["Read(./docs/**)"] } });
117
+ succeeds(run(target, "install", "--dry-run"));
118
+ assert.equal(fs.existsSync(path.join(target, ".nccgs/execution.json")), false);
119
+ succeeds(run(target, "install"));
120
+ const policy = resolveExecution(target);
121
+ assert.equal(policy.name, "standalone");
122
+ assert.equal(policy.profileName, "claude-standalone");
123
+ assert.equal(policy.externalReview.mode, "off");
124
+ assert.deepEqual(policy.profile.models.standard, { model: "opus", effort: "high" });
125
+ const settings = read(target, ".claude/settings.json");
126
+ assert.equal(settings.env.KEEP, "yes");
127
+ assert.ok(settings.hooks.PreToolUse.some(item => JSON.stringify(item) === JSON.stringify(custom)));
128
+ assert.ok(settings.permissions.allow.includes("Read(./docs/**)"));
129
+ assert.equal(settings.model, "opus");
130
+ succeeds(run(target, "doctor"));
131
+ });
132
+
133
+ test("update preserves missing legacy execution policy and exact legacy model routes", () => {
134
+ const target = project();
135
+ succeeds(run(target, "install", "--model-profile", "astra-claude"));
136
+ fs.unlinkSync(path.join(target, ".nccgs/execution.json"));
137
+ const historical = { schemaVersion: 1, id: "pending-legacy", stages: { plan: { actor: "Astra" } }, reviewBudget: { used: 2, limit: 3 } };
138
+ write(target, ".nccgs/tasks/pending-legacy.json", historical);
139
+ succeeds(run(target, "update"));
140
+ assert.equal(fs.existsSync(path.join(target, ".nccgs/execution.json")), false);
141
+ assert.equal(resolveExecution(target).name, "compatibility");
142
+ assert.equal(resolveExecution(target).profileName, "astra-claude");
143
+ assert.equal(read(target, ".claude/settings.json").model, "claude-opus-5-5");
144
+ assert.deepEqual(read(target, ".nccgs/tasks/pending-legacy.json"), historical);
145
+ });
146
+
147
+ test("update preserves required external review and custom agent content", () => {
148
+ const target = project();
149
+ succeeds(run(target, "install"));
150
+ const execution = { schemaVersion: 1, name: "standalone", externalReview: { mode: "required" }, acceptanceRequired: true, maxParallelWorkers: 1 };
151
+ write(target, ".nccgs/execution.json", execution);
152
+ const agent = ".claude/agents/nccgs-gameplay-programmer.md";
153
+ const content = fs.readFileSync(path.join(target, agent), "utf8") + "\nProject-owned convention.\n";
154
+ write(target, agent, content);
155
+ succeeds(run(target, "update"));
156
+ assert.deepEqual(read(target, ".nccgs/execution.json"), execution);
157
+ assert.equal(fs.readFileSync(path.join(target, agent), "utf8"), content);
158
+ assert.equal(read(target, ".nccgs/framework-overrides.json").overrides.find(item => item.path === agent).status, "pending_review");
159
+ });
160
+
161
+ test("execution migration previews, preserves task bytes and rolls back configuration", () => {
162
+ const target = project();
163
+ succeeds(run(target, "install", "--model-profile", "astra-claude"));
164
+ fs.unlinkSync(path.join(target, ".nccgs/execution.json"));
165
+ const taskText = '{"schemaVersion":1,"id":"active","reviewBudget":{"used":2,"limit":3},"stages":{}}\n';
166
+ write(target, ".nccgs/tasks/active.json", taskText);
167
+ const oldPolicy = fs.readFileSync(path.join(target, ".nccgs/project.yaml"), "utf8");
168
+ const oldSettings = fs.readFileSync(path.join(target, ".claude/settings.json"), "utf8");
169
+ const preview = run(target, "migrate-execution", "--to", "standalone", "--model-profile", "claude-standalone", "--dry-run");
170
+ succeeds(preview);
171
+ assert.match(preview.stdout, /Active tasks \(1\): active/);
172
+ assert.equal(fs.existsSync(path.join(target, ".nccgs/backups/execution")), false);
173
+ const applied = run(target, "migrate-execution", "--to", "standalone", "--model-profile", "claude-standalone", "--external-review", "advisory");
174
+ succeeds(applied);
175
+ const id = applied.stdout.match(/Execution migration ([0-9a-f-]{36}) applied/)[1];
176
+ assert.equal(resolveExecution(target).externalReview.mode, "advisory");
177
+ assert.equal(resolveExecution(target).profileName, "claude-standalone");
178
+ assert.equal(fs.readFileSync(path.join(target, ".nccgs/tasks/active.json"), "utf8"), taskText);
179
+ succeeds(run(target, "doctor"));
180
+ succeeds(run(target, "migrate-execution", "--rollback", id, "--dry-run"));
181
+ assert.equal(resolveExecution(target).name, "standalone");
182
+ succeeds(run(target, "migrate-execution", "--rollback", id));
183
+ assert.equal(resolveExecution(target).name, "compatibility");
184
+ assert.equal(fs.readFileSync(path.join(target, ".nccgs/project.yaml"), "utf8"), oldPolicy);
185
+ assert.equal(fs.readFileSync(path.join(target, ".claude/settings.json"), "utf8"), oldSettings);
186
+ assert.equal(fs.readFileSync(path.join(target, ".nccgs/tasks/active.json"), "utf8"), taskText);
187
+ succeeds(run(target, "doctor"));
188
+ });
189
+
190
+ test("rollback refuses changed configuration and backup tampering before writes", () => {
191
+ const target = project();
192
+ succeeds(run(target, "install"));
193
+ const migration = run(target, "migrate-execution", "--to", "standalone", "--external-review", "required");
194
+ succeeds(migration);
195
+ assert.equal(resolveExecution(target).designReviewRequired, true, "changing external review must preserve the reviewed-design gate");
196
+ const id = migration.stdout.match(/Execution migration ([0-9a-f-]{36}) applied/)[1];
197
+ const settings = fs.readFileSync(path.join(target, ".claude/settings.json"), "utf8");
198
+ write(target, ".claude/settings.json", settings + "\n");
199
+ let result = run(target, "migrate-execution", "--rollback", id);
200
+ assert.notEqual(result.status, 0);
201
+ assert.match(result.stderr, /Configuration changed after migration/);
202
+ assert.equal(resolveExecution(target).externalReview.mode, "required");
203
+ write(target, ".claude/settings.json", settings);
204
+ write(target, `.nccgs/backups/execution/${id}/files/.nccgs/execution.json`, "tampered");
205
+ result = run(target, "migrate-execution", "--rollback", id);
206
+ assert.notEqual(result.status, 0);
207
+ assert.match(result.stderr, /backup missing or changed/);
208
+ assert.equal(resolveExecution(target).externalReview.mode, "required");
209
+ });
210
+
211
+ test("doctor rejects malformed execution configuration", () => {
212
+ const target = project();
213
+ succeeds(run(target, "install"));
214
+ write(target, ".nccgs/execution.json", { schemaVersion: 1, name: "standalone", externalReview: { mode: "skip" } });
215
+ const result = run(target, "doctor");
216
+ assert.notEqual(result.status, 0);
217
+ assert.match(result.stderr, /Invalid execution policy/);
218
+ });
219
+
220
+ test("failed model migration restores prior execution and settings without touching tasks", () => {
221
+ const target = project();
222
+ succeeds(run(target, "install", "--model-profile", "astra-claude"));
223
+ fs.unlinkSync(path.join(target, ".nccgs/execution.json"));
224
+ write(target, ".claude/settings.json", "malformed-settings");
225
+ const taskText = '{"id":"failed-migration","schemaVersion":1,"reviewBudget":{"used":2,"limit":3},"stages":{}}';
226
+ write(target, ".nccgs/tasks/failed-migration.json", taskText);
227
+ const result = run(target, "migrate-execution", "--to", "standalone", "--model-profile", "claude-standalone");
228
+ assert.notEqual(result.status, 0);
229
+ assert.equal(resolveExecution(target).name, "compatibility");
230
+ assert.equal(fs.readFileSync(path.join(target, ".claude/settings.json"), "utf8"), "malformed-settings");
231
+ assert.equal(fs.readFileSync(path.join(target, ".nccgs/tasks/failed-migration.json"), "utf8"), taskText);
232
+ const archive = fs.readdirSync(path.join(target, ".nccgs/backups/execution"))[0];
233
+ assert.equal(read(target, `.nccgs/backups/execution/${archive}/manifest.json`).status, "failed-restored");
234
+ });