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