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,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
+ });