nccgs 2.1.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 (76) 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 +3 -1
  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/hook-transport.mjs +34 -0
  31. package/.claude/nccgs/tools/native-completion.mjs +10 -1
  32. package/.claude/nccgs/tools/returned-handoff.mjs +11 -2
  33. package/.claude/nccgs/tools/shared-context-guard.mjs +60 -0
  34. package/.claude/nccgs/tools/shared-context.mjs +25 -0
  35. package/.claude/nccgs/tools/tool-guard.mjs +120 -131
  36. package/.claude/nccgs/tools/unity-transport.mjs +170 -0
  37. package/.claude/nccgs/workflow-catalog.json +4 -4
  38. package/CHANGELOG.md +235 -200
  39. package/CLAUDE.md +10 -8
  40. package/MEMORY.md +109 -60
  41. package/README.md +206 -191
  42. package/UPGRADING.md +130 -118
  43. package/VERSION +1 -1
  44. package/docs/ARCHITECTURE.md +131 -131
  45. package/docs/ASSET-PRODUCTION-GRAPHIFY-PLAN.md +230 -0
  46. package/docs/HUONG-DAN-MIGRATE-VA-SU-DUNG.md +117 -116
  47. package/docs/NCCGS-1.5.md +237 -237
  48. package/docs/PROJECT-POLICY.md +94 -94
  49. package/docs/RELEASE-STATUS.md +17 -9
  50. package/docs/UNINSTALL.md +3 -3
  51. package/docs/WFK17-HOOK-REPAIR.md +71 -0
  52. package/docs/WORKFLOWS.md +85 -85
  53. package/package.json +1 -1
  54. package/scaffold/.nccgs/project.yaml +109 -108
  55. package/scripts/benchmark-v15.mjs +173 -173
  56. package/scripts/doctor.mjs +261 -261
  57. package/scripts/install.mjs +530 -529
  58. package/scripts/run.mjs +5 -4
  59. package/scripts/supervise.mjs +4 -0
  60. package/scripts/uninstall.mjs +292 -292
  61. package/scripts/validate.mjs +285 -280
  62. package/tests/autonomy.test.mjs +126 -113
  63. package/tests/design-readiness.test.mjs +199 -199
  64. package/tests/fixtures/compiler-shutdown.cs +67 -64
  65. package/tests/fixtures/native-provider.mjs +61 -0
  66. package/tests/framework.test.mjs +740 -740
  67. package/tests/hook-repair.test.mjs +106 -0
  68. package/tests/lifecycle-v15.test.mjs +346 -300
  69. package/tests/native-completion-notices.test.mjs +181 -0
  70. package/tests/phase1.test.mjs +427 -416
  71. package/tests/phase2.test.mjs +250 -250
  72. package/tests/policy-v15.test.mjs +234 -234
  73. package/tests/returned-handoff-notices.test.mjs +170 -0
  74. package/tests/runtime-v15.test.mjs +1197 -1191
  75. package/tests/task-slices.test.mjs +285 -285
  76. 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
+ });