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,113 +1,126 @@
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 { launchPlan } from "../scripts/run.mjs";
9
- import { autonomyPolicy, fullTaskAutonomy } from "../.claude/nccgs/tools/autonomy.mjs";
10
- import { safeTaskCommand } from "../.claude/nccgs/tools/tool-guard.mjs";
11
- import { canonicalInputs } from "../.claude/nccgs/tools/inputs.mjs";
12
-
13
- const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
14
- function fixture() {
15
- const base = fs.realpathSync(os.tmpdir());
16
- const directory = fs.mkdtempSync(path.join(base, "nccgs-autonomy-test-"));
17
- const project = path.join(directory, "Project with spaces");
18
- fs.mkdirSync(project);
19
- for (const name of ["Assets", "Packages", "ProjectSettings"]) fs.mkdirSync(path.join(project, name));
20
- const env = { ...process.env };
21
- for (const name of ["CLAUDECODE", "CLAUDE_CODE_ENTRYPOINT", "NCCGS_PROJECT_ROOT", "NCCGS_TASK_AUTONOMY", "CLAUDE_PROJECT_DIR"]) delete env[name];
22
- const run = (...args) => spawnSync(process.execPath, [path.join(root, "scripts/cli.mjs"), ...args, "--project", project], { cwd: directory, env, encoding: "utf8" });
23
- const write = (relative, value) => { const file = path.join(project, relative); fs.mkdirSync(path.dirname(file), { recursive: true }); fs.writeFileSync(file, JSON.stringify(value)); };
24
- return { project, directory, env, run, write, dispose() {
25
- assert.equal(path.dirname(fs.realpathSync(directory)).toLowerCase(), base.toLowerCase());
26
- assert.ok(path.basename(directory).startsWith("nccgs-autonomy-test-"));
27
- assert.equal(fs.lstatSync(directory).isSymbolicLink(), false);
28
- fs.rmSync(directory, { recursive: true, force: true });
29
- } };
30
- }
31
-
32
- test("fresh installs select full task sessions while native flags and all guards remain explicit", () => {
33
- const f = fixture();
34
- try {
35
- const installed = f.run("install"); assert.equal(installed.status, 0, installed.stderr + installed.stdout);
36
- assert.equal(autonomyPolicy(f.project).mode, "full");
37
- const plan = launchPlan(["--project", f.project, "--claude", "C:/Program Files/Claude/claude.exe", "--", "-p", "literal ; task $()", "--model", "sonnet"], f.env);
38
- assert.deepEqual(plan.args, ["--permission-mode", "bypassPermissions", "-p", "literal ; task $()", "--model", "sonnet"]);
39
- assert.equal(plan.project, fs.realpathSync(f.project));
40
- assert.equal(plan.env.CLAUDE_CODE_DISABLE_BACKGROUND_TASKS, "1");
41
- assert.equal(launchPlan(["--project", f.project], { ...f.env, CLAUDE_CODE_DISABLE_BACKGROUND_TASKS: "0", CLAUDE_AUTO_BACKGROUND_TASKS: "1" }).env.CLAUDE_CODE_DISABLE_BACKGROUND_TASKS, "1");
42
- assert.equal(fullTaskAutonomy(f.project, plan.env), true);
43
- assert.equal(fullTaskAutonomy(f.project, f.env), false);
44
- const settings = JSON.parse(fs.readFileSync(path.join(f.project, ".claude/settings.json")));
45
- assert.ok(settings.permissions.deny.includes("Bash(git reset --hard*)"));
46
- assert.match(JSON.stringify(settings.hooks), /CLAUDE_PROJECT_DIR/);
47
- assert.equal(f.run("doctor").status, 0);
48
- const dry = f.run("run", "--dry-run"); assert.equal(dry.status, 0, dry.stderr);
49
- assert.equal(JSON.parse(dry.stdout).mode, "full");
50
- assert.equal(JSON.parse(dry.stdout).nativeBackgroundTasks, false);
51
- assert.throws(() => launchPlan(["--project", f.project, "--", "--permission-mode=default"], f.env), /conflicting/);
52
- assert.throws(() => launchPlan(["--project", f.project], { ...f.env, CLAUDECODE: "1" }), /external/);
53
- assert.throws(() => launchPlan(["--project", f.project, "--", "--setting-sources", "user"], f.env), /conflicting/);
54
- } finally { f.dispose(); }
55
- });
56
-
57
- test("updates preserve legacy permissions and migrate hook paths without duplicating custom hooks", () => {
58
- const f = fixture();
59
- try {
60
- assert.equal(f.run("install", "--autonomy", "guarded").status, 0);
61
- fs.unlinkSync(path.join(f.project, ".nccgs/autonomy.json")); // legacy absence
62
- const file = path.join(f.project, ".claude/settings.json");
63
- const settings = JSON.parse(fs.readFileSync(file));
64
- settings.permissions.ask = ["Bash(custom-policy-command *)"];
65
- settings.hooks.SessionStart[0].hooks[0].command = "node .claude/nccgs/hooks/session-start.mjs";
66
- settings.hooks.SessionStart[0].hooks.push({ type: "command", command: "custom-start" });
67
- fs.writeFileSync(file, JSON.stringify(settings));
68
- const dry = f.run("update", "--autonomy", "full", "--dry-run");
69
- assert.equal(dry.status, 0, dry.stderr); assert.equal(autonomyPolicy(f.project).mode, "guarded");
70
- const unchanged = f.run("update"); assert.equal(unchanged.status, 0, unchanged.stderr);
71
- assert.equal(autonomyPolicy(f.project).mode, "guarded");
72
- assert.equal(launchPlan(["--project", f.project], f.env).args[1], "default");
73
- const enabled = f.run("update", "--autonomy", "full"); assert.equal(enabled.status, 0, enabled.stderr);
74
- const actual = JSON.parse(fs.readFileSync(file));
75
- assert.deepEqual(actual.permissions.ask, ["Bash(custom-policy-command *)"]);
76
- const commands = actual.hooks.SessionStart.flatMap(group => group.hooks.map(h => h.command));
77
- assert.equal(commands.filter(command => command.includes("session-start.mjs")).length, 1);
78
- assert.ok(commands.includes("custom-start"));
79
- assert.ok(actual.permissions.deny.includes("Bash(git reset --hard*)"));
80
- const before = canonicalInputs(f.project).sha256;
81
- f.write(".nccgs/autonomy.json", { schemaVersion: 1, mode: "guarded" });
82
- assert.notEqual(canonicalInputs(f.project).sha256, before);
83
- } finally { f.dispose(); }
84
- });
85
-
86
- test("report punctuation is data while shell composition and foreign task roots remain blocked", () => {
87
- const f = fixture();
88
- try {
89
- const prefix = "node .claude/nccgs/tools/task.mjs report --id task --outcome partial --summary ";
90
- for (const summary of ['"build passed; stress pending"', "'literal $(not executed); > output | text'", '"line one\nline two; done"']) assert.equal(safeTaskCommand(prefix + summary, f.project), "report");
91
- for (const summary of ['"$(touch bad)"', '"`touch bad`"', '"unfinished', '"ok"; echo bad', '"ok" > bad', '"ok" && echo bad']) assert.equal(safeTaskCommand(prefix + summary, f.project), null);
92
- const absolute = `node "${f.project.replaceAll("\\", "/")}/.claude/nccgs/tools/task.mjs" report --project "${f.project}" --summary "yes; report"`;
93
- assert.equal(safeTaskCommand(absolute, f.project), "report");
94
- assert.equal(safeTaskCommand(absolute.replace(`--project "${f.project}"`, `--project "${f.directory}"`), f.project), null);
95
- } finally { f.dispose(); }
96
- });
97
-
98
- test("hook execution from outside the project still uses its policy and rejects forged DONE", () => {
99
- const f = fixture();
100
- try {
101
- assert.equal(f.run("install").status, 0);
102
- const env = { ...f.env, NCCGS_PROJECT_ROOT: f.project, NCCGS_TASK_AUTONOMY: "full", CLAUDE_PROJECT_DIR: f.project };
103
- const hook = (file, mode, input) => spawnSync(process.execPath, [path.join(f.project, ".claude/nccgs/hooks", file), mode], { cwd: f.directory, env, input: JSON.stringify(input), encoding: "utf8" });
104
- const start = hook("session-start.mjs", "", { session_id: "fixture-session" });
105
- assert.equal(start.status, 0, start.stderr); assert.match(start.stdout, /TASK AUTONOMY/);
106
- const report = hook("closure-guard.mjs", "pre", { tool_name: "Bash", tool_input: { command: `node "${f.project.replaceAll("\\", "/")}/.claude/nccgs/tools/task.mjs" report --project "${f.project}" --id task --summary "test; report"` } });
107
- assert.equal(report.status, 0, report.stderr);
108
- const destructive = hook("protect-git.mjs", "", { tool_input: { command: "git reset --hard HEAD" } });
109
- assert.equal(destructive.status, 2);
110
- const done = hook("closure-guard.mjs", "stop", { session_id: "fixture-session", last_assistant_message: "Status: DONE" });
111
- assert.equal(JSON.parse(done.stdout).decision, "block");
112
- } finally { f.dispose(); }
113
- });
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 { launchPlan } from "../scripts/run.mjs";
9
+ import { autonomyPolicy, conflictingLaunchOption, fullTaskAutonomy } from "../.claude/nccgs/tools/autonomy.mjs";
10
+ import { safeTaskCommand } from "../.claude/nccgs/tools/tool-guard.mjs";
11
+ import { canonicalInputs } from "../.claude/nccgs/tools/inputs.mjs";
12
+
13
+ const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
14
+ function fixture() {
15
+ const base = fs.realpathSync(os.tmpdir());
16
+ const directory = fs.mkdtempSync(path.join(base, "nccgs-autonomy-test-"));
17
+ const project = path.join(directory, "Project with spaces");
18
+ fs.mkdirSync(project);
19
+ for (const name of ["Assets", "Packages", "ProjectSettings"]) fs.mkdirSync(path.join(project, name));
20
+ const env = { ...process.env };
21
+ for (const name of ["CLAUDECODE", "CLAUDE_CODE_ENTRYPOINT", "NCCGS_PROJECT_ROOT", "NCCGS_TASK_AUTONOMY", "CLAUDE_PROJECT_DIR"]) delete env[name];
22
+ const run = (...args) => spawnSync(process.execPath, [path.join(root, "scripts/cli.mjs"), ...args, "--project", project], { cwd: directory, env, encoding: "utf8" });
23
+ const write = (relative, value) => { const file = path.join(project, relative); fs.mkdirSync(path.dirname(file), { recursive: true }); fs.writeFileSync(file, JSON.stringify(value)); };
24
+ return { project, directory, env, run, write, dispose() {
25
+ assert.equal(path.dirname(fs.realpathSync(directory)).toLowerCase(), base.toLowerCase());
26
+ assert.ok(path.basename(directory).startsWith("nccgs-autonomy-test-"));
27
+ assert.equal(fs.lstatSync(directory).isSymbolicLink(), false);
28
+ fs.rmSync(directory, { recursive: true, force: true });
29
+ } };
30
+ }
31
+
32
+ test("fresh installs select full task sessions while native flags and all guards remain explicit", () => {
33
+ const f = fixture();
34
+ try {
35
+ const installed = f.run("install"); assert.equal(installed.status, 0, installed.stderr + installed.stdout);
36
+ assert.equal(autonomyPolicy(f.project).mode, "full");
37
+ const plan = launchPlan(["--project", f.project, "--claude", "C:/Program Files/Claude/claude.exe", "--", "-p", "literal ; task $()", "--model", "sonnet"], f.env);
38
+ // R08: strict isolation directly follows the owned permission prefix, before every caller argument.
39
+ assert.deepEqual(plan.args, ["--permission-mode", "bypassPermissions", "--strict-mcp-config", "-p", "literal ; task $()", "--model", "sonnet", "--mcp-config", '{"mcpServers":{}}']);
40
+ assert.equal(plan.project, fs.realpathSync(f.project));
41
+ assert.equal(plan.env.CLAUDE_CODE_DISABLE_BACKGROUND_TASKS, "1");
42
+ assert.equal(launchPlan(["--project", f.project], { ...f.env, CLAUDE_CODE_DISABLE_BACKGROUND_TASKS: "0", CLAUDE_AUTO_BACKGROUND_TASKS: "1" }).env.CLAUDE_CODE_DISABLE_BACKGROUND_TASKS, "1");
43
+ assert.equal(fullTaskAutonomy(f.project, plan.env), true);
44
+ assert.equal(fullTaskAutonomy(f.project, f.env), false);
45
+ const settings = JSON.parse(fs.readFileSync(path.join(f.project, ".claude/settings.json")));
46
+ assert.ok(settings.permissions.deny.includes("Bash(git reset --hard*)"));
47
+ assert.match(JSON.stringify(settings.hooks), /CLAUDE_PROJECT_DIR/);
48
+ assert.equal(f.run("doctor").status, 0);
49
+ const dry = f.run("run", "--dry-run"); assert.equal(dry.status, 0, dry.stderr);
50
+ assert.equal(JSON.parse(dry.stdout).mode, "full");
51
+ assert.equal(JSON.parse(dry.stdout).nativeBackgroundTasks, false);
52
+ assert.throws(() => launchPlan(["--project", f.project, "--", "--permission-mode=default"], f.env), /conflicting/);
53
+ assert.throws(() => launchPlan(["--project", f.project], { ...f.env, CLAUDECODE: "1" }), /external/);
54
+ assert.throws(() => launchPlan(["--project", f.project, "--", "--setting-sources", "user"], f.env), /conflicting/);
55
+ for (const form of ["--restricted", "--restricted=true", "--restricted=false", "--restricted="])
56
+ assert.throws(() => launchPlan(["--project", f.project, "--", "-p", "x", form], f.env), /conflicting/, form);
57
+ assert.deepEqual(launchPlan(["--project", f.project, "--", "-p", "restricted", "--restricted-mode"], f.env).args.slice(0, 5),
58
+ ["--permission-mode", "bypassPermissions", "--strict-mcp-config", "-p", "restricted"]);
59
+ } finally { f.dispose(); }
60
+ });
61
+
62
+ test("R09 shared launch-ownership policy covers --restricted and keeps R08 forms", () => {
63
+ for (const form of ["--restricted", "--restricted=true", "--restricted=false", "--restricted=", "--bare", "--bare=", "--safe-mode", "--safe-mode=false", "--permission-mode", "--settings=x", "-w"])
64
+ assert.equal(conflictingLaunchOption.test(form), true, form);
65
+ for (const value of ["restricted", "--restricted-mode", "--restrictedx", "--unrestricted", "-restricted", "--print", "bare", "--bare-metal", "--safe-modes"])
66
+ assert.equal(conflictingLaunchOption.test(value), false, value);
67
+ });
68
+
69
+ test("updates preserve legacy permissions and migrate hook paths without duplicating custom hooks", () => {
70
+ const f = fixture();
71
+ try {
72
+ assert.equal(f.run("install", "--autonomy", "guarded").status, 0);
73
+ fs.unlinkSync(path.join(f.project, ".nccgs/autonomy.json")); // legacy absence
74
+ const file = path.join(f.project, ".claude/settings.json");
75
+ const settings = JSON.parse(fs.readFileSync(file));
76
+ settings.permissions.ask = ["Bash(custom-policy-command *)"];
77
+ delete settings.hooks.SessionStart[0].hooks[0].args;
78
+ settings.hooks.SessionStart[0].hooks[0].command = "node .claude/nccgs/hooks/session-start.mjs";
79
+ settings.hooks.SessionStart[0].hooks.push({ type: "command", command: "custom-start" });
80
+ fs.writeFileSync(file, JSON.stringify(settings));
81
+ const dry = f.run("update", "--autonomy", "full", "--dry-run");
82
+ assert.equal(dry.status, 0, dry.stderr); assert.equal(autonomyPolicy(f.project).mode, "guarded");
83
+ const unchanged = f.run("update"); assert.equal(unchanged.status, 0, unchanged.stderr);
84
+ assert.equal(autonomyPolicy(f.project).mode, "guarded");
85
+ assert.equal(launchPlan(["--project", f.project], f.env).args[1], "default");
86
+ const enabled = f.run("update", "--autonomy", "full"); assert.equal(enabled.status, 0, enabled.stderr);
87
+ const actual = JSON.parse(fs.readFileSync(file));
88
+ assert.deepEqual(actual.permissions.ask, ["Bash(custom-policy-command *)"]);
89
+ const commands = actual.hooks.SessionStart.flatMap(group => group.hooks.map(h => [h.command, ...(h.args ?? [])].join(" ")));
90
+ assert.equal(commands.filter(command => command.includes("session-start.mjs")).length, 1);
91
+ assert.ok(commands.includes("custom-start"));
92
+ assert.ok(actual.permissions.deny.includes("Bash(git reset --hard*)"));
93
+ const before = canonicalInputs(f.project).sha256;
94
+ f.write(".nccgs/autonomy.json", { schemaVersion: 1, mode: "guarded" });
95
+ assert.notEqual(canonicalInputs(f.project).sha256, before);
96
+ } finally { f.dispose(); }
97
+ });
98
+
99
+ test("report punctuation is data while shell composition and foreign task roots remain blocked", () => {
100
+ const f = fixture();
101
+ try {
102
+ const prefix = "node .claude/nccgs/tools/task.mjs report --id task --outcome partial --summary ";
103
+ for (const summary of ['"build passed; stress pending"', "'literal $(not executed); > output | text'", '"line one\nline two; done"']) assert.equal(safeTaskCommand(prefix + summary, f.project), "report");
104
+ for (const summary of ['"$(touch bad)"', '"`touch bad`"', '"unfinished', '"ok"; echo bad', '"ok" > bad', '"ok" && echo bad']) assert.equal(safeTaskCommand(prefix + summary, f.project), null);
105
+ const absolute = `node "${f.project.replaceAll("\\", "/")}/.claude/nccgs/tools/task.mjs" report --project "${f.project}" --summary "yes; report"`;
106
+ assert.equal(safeTaskCommand(absolute, f.project), "report");
107
+ assert.equal(safeTaskCommand(absolute.replace(`--project "${f.project}"`, `--project "${f.directory}"`), f.project), null);
108
+ } finally { f.dispose(); }
109
+ });
110
+
111
+ test("hook execution from outside the project still uses its policy and rejects forged DONE", () => {
112
+ const f = fixture();
113
+ try {
114
+ assert.equal(f.run("install").status, 0);
115
+ const env = { ...f.env, NCCGS_PROJECT_ROOT: f.project, NCCGS_TASK_AUTONOMY: "full", CLAUDE_PROJECT_DIR: f.project };
116
+ const hook = (file, mode, input) => spawnSync(process.execPath, [path.join(f.project, ".claude/nccgs/hooks", file), mode], { cwd: f.directory, env, input: JSON.stringify(input), encoding: "utf8" });
117
+ const start = hook("session-start.mjs", "", { session_id: "fixture-session" });
118
+ assert.equal(start.status, 0, start.stderr); assert.match(start.stdout, /TASK AUTONOMY/);
119
+ const report = hook("closure-guard.mjs", "pre", { tool_name: "Bash", tool_input: { command: `node "${f.project.replaceAll("\\", "/")}/.claude/nccgs/tools/task.mjs" report --project "${f.project}" --id task --summary "test; report"` } });
120
+ assert.equal(report.status, 0, report.stderr);
121
+ const destructive = hook("protect-git.mjs", "", { tool_input: { command: "git reset --hard HEAD" } });
122
+ assert.equal(destructive.status, 2);
123
+ const done = hook("closure-guard.mjs", "stop", { session_id: "fixture-session", last_assistant_message: "Status: DONE" });
124
+ assert.equal(JSON.parse(done.stdout).decision, "block");
125
+ } finally { f.dispose(); }
126
+ });