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.
- package/.claude/agents/nccgs-adversarial-reviewer.md +1 -1
- package/.claude/agents/nccgs-engine-programmer.md +1 -1
- package/.claude/agents/nccgs-fast-implementer.md +1 -1
- package/.claude/agents/nccgs-game-designer.md +1 -1
- package/.claude/agents/nccgs-gameplay-programmer.md +1 -1
- package/.claude/agents/nccgs-qa-engineer.md +1 -1
- package/.claude/agents/nccgs-task-scout.md +1 -1
- package/.claude/agents/nccgs-ui-programmer.md +1 -1
- package/.claude/agents/nccgs-unity-build-specialist.md +1 -1
- package/.claude/agents/nccgs-unity-implementer.md +1 -1
- package/.claude/agents/nccgs-unity-systems-specialist.md +1 -1
- package/.claude/agents/nccgs-unity-ui-specialist.md +1 -1
- package/.claude/agents/nccgs-verification-engineer.md +1 -1
- package/.claude/nccgs/MEMORY.md +3 -1
- package/.claude/nccgs/VERSION +1 -1
- package/.claude/nccgs/constitution.md +187 -176
- package/.claude/nccgs/hooks/closure-guard.mjs +2 -0
- package/.claude/nccgs/hooks/unity-transport-guard.mjs +11 -0
- package/.claude/nccgs/pipeline.json +260 -260
- package/.claude/nccgs/protocols/context-packets.md +5 -0
- package/.claude/nccgs/protocols/model-routing.md +46 -46
- package/.claude/nccgs/protocols/review-handoff.md +21 -1
- package/.claude/nccgs/protocols/shared-workflow-context.md +33 -0
- package/.claude/nccgs/protocols/unity-boundary.md +20 -0
- package/.claude/nccgs/routing.generated.md +4 -4
- package/.claude/nccgs/settings.fragment.json +84 -24
- package/.claude/nccgs/studio.json +53 -53
- package/.claude/nccgs/tools/autonomy.mjs +13 -0
- package/.claude/nccgs/tools/command-tokens.mjs +22 -0
- package/.claude/nccgs/tools/hook-transport.mjs +34 -0
- package/.claude/nccgs/tools/native-completion.mjs +10 -1
- package/.claude/nccgs/tools/returned-handoff.mjs +11 -2
- package/.claude/nccgs/tools/shared-context-guard.mjs +60 -0
- package/.claude/nccgs/tools/shared-context.mjs +25 -0
- package/.claude/nccgs/tools/tool-guard.mjs +120 -131
- package/.claude/nccgs/tools/unity-transport.mjs +170 -0
- package/.claude/nccgs/workflow-catalog.json +4 -4
- package/CHANGELOG.md +235 -200
- package/CLAUDE.md +10 -8
- package/MEMORY.md +109 -60
- package/README.md +206 -191
- package/UPGRADING.md +130 -118
- package/VERSION +1 -1
- package/docs/ARCHITECTURE.md +131 -131
- package/docs/ASSET-PRODUCTION-GRAPHIFY-PLAN.md +230 -0
- package/docs/HUONG-DAN-MIGRATE-VA-SU-DUNG.md +117 -116
- package/docs/NCCGS-1.5.md +237 -237
- package/docs/PROJECT-POLICY.md +94 -94
- package/docs/RELEASE-STATUS.md +17 -9
- package/docs/UNINSTALL.md +3 -3
- package/docs/WFK17-HOOK-REPAIR.md +71 -0
- package/docs/WORKFLOWS.md +85 -85
- package/package.json +1 -1
- package/scaffold/.nccgs/project.yaml +109 -108
- package/scripts/benchmark-v15.mjs +173 -173
- package/scripts/doctor.mjs +261 -261
- package/scripts/install.mjs +530 -529
- package/scripts/run.mjs +5 -4
- package/scripts/supervise.mjs +4 -0
- package/scripts/uninstall.mjs +292 -292
- package/scripts/validate.mjs +285 -280
- package/tests/autonomy.test.mjs +126 -113
- package/tests/design-readiness.test.mjs +199 -199
- package/tests/fixtures/compiler-shutdown.cs +67 -64
- package/tests/fixtures/native-provider.mjs +61 -0
- package/tests/framework.test.mjs +740 -740
- package/tests/hook-repair.test.mjs +106 -0
- package/tests/lifecycle-v15.test.mjs +346 -300
- package/tests/native-completion-notices.test.mjs +181 -0
- package/tests/phase1.test.mjs +427 -416
- package/tests/phase2.test.mjs +250 -250
- package/tests/policy-v15.test.mjs +234 -234
- package/tests/returned-handoff-notices.test.mjs +170 -0
- package/tests/runtime-v15.test.mjs +1197 -1191
- package/tests/task-slices.test.mjs +285 -285
- package/tests/unity-transport.test.mjs +348 -0
package/tests/autonomy.test.mjs
CHANGED
|
@@ -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
|
-
|
|
39
|
-
assert.
|
|
40
|
-
assert.equal(plan.
|
|
41
|
-
assert.equal(
|
|
42
|
-
assert.equal(
|
|
43
|
-
assert.equal(fullTaskAutonomy(f.project,
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
assert.
|
|
47
|
-
assert.
|
|
48
|
-
|
|
49
|
-
assert.equal(
|
|
50
|
-
assert.equal(JSON.parse(dry.stdout).
|
|
51
|
-
assert.
|
|
52
|
-
assert.throws(() => launchPlan(["--project", f.project
|
|
53
|
-
assert.throws(() => launchPlan(["--project", f.project,
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
assert.equal(
|
|
73
|
-
|
|
74
|
-
const
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
f.
|
|
82
|
-
assert.
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
const
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
});
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
const
|
|
103
|
-
const
|
|
104
|
-
const
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
assert.equal(
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
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
|
+
});
|