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
@@ -0,0 +1,170 @@
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 test from "node:test";
6
+ import { captureReturnedHandoff } from "../.claude/nccgs/tools/returned-handoff.mjs";
7
+ import { nativeCompletion } from "../.claude/nccgs/tools/native-completion.mjs";
8
+
9
+ const REL = ".nccgs/evidence/analysis/handoff.md";
10
+ const GREP = "<tools-used-before-summary>Grep</tools-used-before-summary>";
11
+ const text = value => ({ type: "text", text: value });
12
+ const BODY = "NCCGS-HANDOFF: COMPLETE\n\n## Outcome\nDone.\n";
13
+
14
+ function fixture() {
15
+ const project = fs.mkdtempSync(path.join(os.tmpdir(), "nccgs-returned-handoff-"));
16
+ const dispatch = { handoffTransport: "native-result", status: "running", kind: "analysis", sessionId: "S1",
17
+ toolUseId: "T1", agentId: "A1", agent: "nccgs-engine-programmer", snapshot: "snap1", handoffPaths: [REL] };
18
+ const input = response => ({ hook_event_name: "PostToolUse", tool_name: "Agent", session_id: "S1", tool_use_id: "T1",
19
+ tool_input: { subagent_type: "nccgs-engine-programmer" }, tool_response: response });
20
+ const file = path.join(project, REL);
21
+ // Real hook path: completion comes from the authoritative classifier.
22
+ const capture = (response, { completion, d = dispatch, snapshot = "snap1", inp } = {}) =>
23
+ captureReturnedHandoff(project, d, inp ?? input(response), completion ?? nativeCompletion(response), snapshot);
24
+ const resp = (content, extra = {}) => ({ agentId: "A1", status: "completed", content, ...extra });
25
+ return { project, dispatch, input, file, capture, resp, dispose: () => fs.rmSync(project, { recursive: true, force: true }) };
26
+ }
27
+ function withFixture(fn) { const f = fixture(); try { fn(f); } finally { f.dispose(); } }
28
+ const rejected = (f, result) => { assert.equal(result?.status, "rejected", JSON.stringify(result)); assert.ok(!fs.existsSync(f.file)); };
29
+ const captured = (f, result, expected) => {
30
+ assert.equal(result?.status, "captured", JSON.stringify(result));
31
+ assert.equal(fs.readFileSync(f.file, "utf8"), expected);
32
+ };
33
+
34
+ test("no-note response is captured verbatim", () => withFixture(f => {
35
+ captured(f, f.capture(f.resp([text(BODY)])), BODY);
36
+ }));
37
+
38
+ test("observed Grep leading header plus one handoff", () => withFixture(f => {
39
+ captured(f, f.capture(f.resp([text(GREP), text(BODY)], { harnessNoteCount: 1 })), BODY);
40
+ }));
41
+
42
+ test("designated tail block is stripped", () => withFixture(f => {
43
+ captured(f, f.capture(f.resp([text(BODY), text(GREP)], { harnessTailCount: 1 })), BODY);
44
+ }));
45
+
46
+ test("leading plus tail blocks are stripped", () => withFixture(f => {
47
+ captured(f, f.capture(f.resp([text(GREP), text(BODY), text("<tools-used-before-summary>Read,Edit</tools-used-before-summary>")],
48
+ { harnessNoteCount: 1, harnessTailCount: 1 })), BODY);
49
+ }));
50
+
51
+ test("multiple recognized notes are stripped", () => withFixture(f => {
52
+ const notes = [text(GREP), text("<tools-used-before-summary>Read</tools-used-before-summary>")];
53
+ captured(f, f.capture(f.resp([...notes, text(BODY), text(GREP)], { harnessNoteCount: 2, harnessTailCount: 1 })), BODY);
54
+ }));
55
+
56
+ test("model text is preserved byte for byte", () => {
57
+ const bodies = [
58
+ ` \n${BODY} \n`,
59
+ "NCCGS-HANDOFF: COMPLETE\r\nline\r\n\r\n",
60
+ "NCCGS-HANDOFF: COMPLETE\nUnicode: é中文 \u{1F600}\n",
61
+ `${BODY}\n\`\`\`nccgs-review\n{"schemaVersion":1,"verdict":"PASS","criteriaMet":true,"findings":[{"id":"F-1"}],"conditions":[{"id":"C-1"}]}\n\`\`\`\n`,
62
+ ];
63
+ for (const body of bodies) withFixture(f => {
64
+ captured(f, f.capture(f.resp([text(GREP), text(body)], { harnessNoteCount: 1 })), body);
65
+ });
66
+ });
67
+
68
+ test("marker-like text inside the model body is preserved verbatim", () => withFixture(f => {
69
+ const body = `${BODY}\nQuote: ${GREP}\n<system-reminder>kept</system-reminder>\n`;
70
+ captured(f, f.capture(f.resp([text(GREP), text(body)], { harnessNoteCount: 1 })), body);
71
+ }));
72
+
73
+ test("undesignated lookalike block counts as a model block and is ambiguous", () => withFixture(f => {
74
+ rejected(f, f.capture(f.resp([text(GREP), text(BODY)])));
75
+ rejected(f, f.capture(f.resp([text(BODY), text(GREP)], { harnessNoteCount: 0 })));
76
+ }));
77
+
78
+ test("unknown, mixed, turn-limit and non-completed responses reject with a COMPLETE body", () => {
79
+ const cases = [
80
+ f => f.resp([text("<harness-advisory>something new</harness-advisory>"), text(BODY)], { harnessNoteCount: 1 }),
81
+ f => f.resp([text(`${GREP}\nextra prose`), text(BODY)], { harnessNoteCount: 1 }),
82
+ f => f.resp([text("Note: this agent stopped at its 30-turn limit before finishing"), text(BODY)], { harnessNoteCount: 1 }),
83
+ f => f.resp([text(BODY), text("this agent stopped at its 12-turn limit before finishing")], { harnessTailCount: 1 }),
84
+ f => f.resp([text(BODY)], { status: "failed" }),
85
+ f => f.resp([text(BODY)], { status: "async_launched" }),
86
+ f => ({ agentId: "A1", content: [text(BODY)] }),
87
+ ];
88
+ for (const make of cases) withFixture(f => rejected(f, f.capture(make(f))));
89
+ });
90
+
91
+ test("invalid, negative, non-integer, overlapping and too-large counts reject", () => {
92
+ const counts = [
93
+ { harnessNoteCount: -1 }, { harnessTailCount: -1 }, { harnessNoteCount: 1.5 }, { harnessTailCount: "1" },
94
+ { harnessNoteCount: 2, harnessTailCount: 1 }, { harnessNoteCount: 3 }, { harnessTailCount: 5 },
95
+ { harnessNoteCount: Number.NaN }, { harnessNoteCount: Number.MAX_SAFE_INTEGER + 2 },
96
+ ];
97
+ for (const extra of counts) withFixture(f => {
98
+ rejected(f, f.capture(f.resp([text(GREP), text(BODY)], extra)));
99
+ // Even with a forged supplied completion the raw response is re-evaluated.
100
+ rejected(f, f.capture(f.resp([text(GREP), text(BODY)], extra), { completion: { status: "completed" } }));
101
+ });
102
+ });
103
+
104
+ test("count equal to all blocks leaves no model block and rejects", () => withFixture(f => {
105
+ rejected(f, f.capture(f.resp([text(GREP)], { harnessNoteCount: 1 })));
106
+ }));
107
+
108
+ test("absent, non-text, empty, whitespace-only and oversized model content reject", () => {
109
+ const big = "x".repeat(262145), exact = "y".repeat(262144);
110
+ const cases = [
111
+ undefined, "str", [], [{ type: "image", text: BODY }], [{ type: "text" }], [{ type: "text", text: 5 }],
112
+ [text("")], [text(" \r\n\t ")], [text(big)], [text("é".repeat(131073))],
113
+ ];
114
+ for (const content of cases) withFixture(f => rejected(f, f.capture(f.resp(content))));
115
+ for (const content of cases.filter(Array.isArray)) withFixture(f =>
116
+ rejected(f, f.capture(f.resp([text(GREP), ...content], { harnessNoteCount: 1 }))));
117
+ withFixture(f => captured(f, f.capture(f.resp([text(exact)])), exact));
118
+ });
119
+
120
+ test("multiple model blocks are ambiguous", () => withFixture(f => {
121
+ rejected(f, f.capture(f.resp([text(BODY), text("second")])));
122
+ rejected(f, f.capture(f.resp([text(GREP), text(BODY), text("second")], { harnessNoteCount: 1 })));
123
+ rejected(f, f.capture(f.resp([text(GREP), text(BODY), text("second"), text(GREP)], { harnessNoteCount: 1, harnessTailCount: 1 })));
124
+ }));
125
+
126
+ test("forged supplied completion with invalid or turn-limited raw response rejects", () => withFixture(f => {
127
+ const forged = { status: "completed" };
128
+ rejected(f, f.capture(f.resp([text("<unknown>x</unknown>"), text(BODY)], { harnessNoteCount: 1 }), { completion: forged }));
129
+ rejected(f, f.capture(f.resp([text("this agent stopped at its 5-turn limit before finishing"), text(BODY)], { harnessNoteCount: 1 }), { completion: forged }));
130
+ rejected(f, f.capture(f.resp([text(BODY)], { status: "failed" }), { completion: forged }));
131
+ rejected(f, f.capture(f.resp([text(BODY)], { harnessNoteCount: 7 }), { completion: forged }));
132
+ }));
133
+
134
+ test("failures indicated by the supplied completion are preserved", () => withFixture(f => {
135
+ const response = f.resp([text(BODY)]);
136
+ rejected(f, f.capture(response, { completion: { status: "failed" } }));
137
+ rejected(f, f.capture(response, { completion: { status: "completed", reason: "turn-limit" } }));
138
+ rejected(f, f.capture(response, { completion: { reason: "x" } }));
139
+ }));
140
+
141
+ test("identity and snapshot mismatches reject", () => withFixture(f => {
142
+ const response = f.resp([text(GREP), text(BODY)], { harnessNoteCount: 1 });
143
+ const base = f.input(response);
144
+ for (const field of ["session_id", "tool_use_id", "hook_event_name", "tool_name"])
145
+ rejected(f, f.capture(response, { inp: { ...base, [field]: "incorrect" } }));
146
+ rejected(f, f.capture(response, { inp: { ...base, tool_input: { subagent_type: "wrong" } } }));
147
+ rejected(f, f.capture({ ...response, agentId: "wrong" }));
148
+ rejected(f, f.capture(response, { snapshot: "other" }));
149
+ rejected(f, f.capture(response, { d: { ...f.dispatch, status: "completed" } }));
150
+ rejected(f, f.capture(response, { d: { ...f.dispatch, kind: "implementation" } }));
151
+ rejected(f, f.capture(response, { d: { ...f.dispatch, handoffPaths: [REL, "other.md"] } }));
152
+ assert.equal(f.capture(response, { d: { ...f.dispatch, handoffTransport: undefined } }), null);
153
+ assert.ok(!fs.existsSync(f.file));
154
+ }));
155
+
156
+ test("an existing handoff is never overwritten", () => withFixture(f => {
157
+ fs.mkdirSync(path.dirname(f.file), { recursive: true });
158
+ fs.writeFileSync(f.file, "ORIGINAL DRAFT");
159
+ const result = f.capture(f.resp([text(GREP), text(BODY)], { harnessNoteCount: 1 }));
160
+ assert.equal(result.status, "rejected");
161
+ assert.equal(result.reason, "handoff-already-exists");
162
+ assert.equal(fs.readFileSync(f.file, "utf8"), "ORIGINAL DRAFT");
163
+ }));
164
+
165
+ test("paths escaping the project reject without writing", () => withFixture(f => {
166
+ const response = f.resp([text(BODY)]);
167
+ const result = f.capture(response, { d: { ...f.dispatch, handoffPaths: ["../escape.md"] } });
168
+ assert.equal(result.status, "rejected");
169
+ assert.ok(!fs.existsSync(path.join(f.project, "..", "escape.md")));
170
+ }));