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/scripts/validate.mjs
CHANGED
|
@@ -1,280 +1,285 @@
|
|
|
1
|
-
import
|
|
2
|
-
import
|
|
3
|
-
import
|
|
4
|
-
import
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
const
|
|
11
|
-
const
|
|
12
|
-
const
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
const
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
"
|
|
48
|
-
".
|
|
49
|
-
".claude/nccgs/
|
|
50
|
-
".claude/nccgs/
|
|
51
|
-
".claude/nccgs/
|
|
52
|
-
".claude/nccgs/
|
|
53
|
-
".claude/nccgs/tools/
|
|
54
|
-
".claude/nccgs/tools/
|
|
55
|
-
"
|
|
56
|
-
"
|
|
57
|
-
".claude/nccgs/tools/
|
|
58
|
-
"
|
|
59
|
-
"
|
|
60
|
-
".claude/nccgs/
|
|
61
|
-
".claude/nccgs/protocols/
|
|
62
|
-
".claude/nccgs/protocols/
|
|
63
|
-
".claude/nccgs/protocols/
|
|
64
|
-
"
|
|
65
|
-
"scaffold/.nccgs/
|
|
66
|
-
"scaffold/.nccgs/
|
|
67
|
-
"package-assets/
|
|
68
|
-
"
|
|
69
|
-
"scripts/
|
|
70
|
-
"scripts/
|
|
71
|
-
"scripts/
|
|
72
|
-
"
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
const
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
fs.readFileSync(path.join(root, ".
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
if (packageJson.
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
//
|
|
94
|
-
|
|
95
|
-
const
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|| metadata.
|
|
102
|
-
||
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
||
|
|
106
|
-
|
|
107
|
-
const
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
if (
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
const
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
if (execution.
|
|
127
|
-
if (execution.
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
if (pipeline.
|
|
131
|
-
|
|
132
|
-
const
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
agentNames.
|
|
136
|
-
|
|
137
|
-
const
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
if (
|
|
146
|
-
if (
|
|
147
|
-
if (fs.readFileSync(file, "utf8").
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
if (!
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
const
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
workflowNames.
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
if (
|
|
170
|
-
|
|
171
|
-
for (const [
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
/^
|
|
182
|
-
|
|
183
|
-
/
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
const
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
if (
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
if (
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
if (!
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
}
|
|
273
|
-
|
|
274
|
-
console.
|
|
275
|
-
console.
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
console.log(
|
|
280
|
-
console.log(`
|
|
1
|
+
import { managedHookKey } from "../.claude/nccgs/tools/hook-transport.mjs";
|
|
2
|
+
import crypto from "node:crypto";
|
|
3
|
+
import fs from "node:fs";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import { spawnSync } from "node:child_process";
|
|
6
|
+
import { fileURLToPath } from "node:url";
|
|
7
|
+
import { compile } from "../.claude/nccgs/tools/compile-policy.mjs";
|
|
8
|
+
import { executionPolicy } from "../.claude/nccgs/tools/policy.mjs";
|
|
9
|
+
|
|
10
|
+
const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
|
11
|
+
const errors = [];
|
|
12
|
+
const warnings = [];
|
|
13
|
+
const posix = (value) => value.split(path.sep).join("/");
|
|
14
|
+
|
|
15
|
+
function walk(directory) {
|
|
16
|
+
if (!fs.existsSync(directory)) return [];
|
|
17
|
+
const output = [];
|
|
18
|
+
for (const entry of fs.readdirSync(directory, { withFileTypes: true })) {
|
|
19
|
+
const full = path.join(directory, entry.name);
|
|
20
|
+
if (entry.isDirectory()) output.push(...walk(full));
|
|
21
|
+
else if (entry.isFile()) output.push(full);
|
|
22
|
+
}
|
|
23
|
+
return output.sort((a, b) => a.localeCompare(b));
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function sha256(file) {
|
|
27
|
+
return crypto.createHash("sha256").update(fs.readFileSync(file)).digest("hex");
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function frontmatter(file) {
|
|
31
|
+
const text = fs.readFileSync(file, "utf8");
|
|
32
|
+
const match = text.match(/^---\r?\n([\s\S]*?)\r?\n---(?:\r?\n|$)/);
|
|
33
|
+
if (!match) return null;
|
|
34
|
+
const result = {};
|
|
35
|
+
for (const line of match[1].split(/\r?\n/)) {
|
|
36
|
+
const item = line.match(/^([A-Za-z0-9_-]+):\s*(.*)$/);
|
|
37
|
+
if (item) result[item[1]] = item[2].replace(/^['"]|['"]$/g, "").trim();
|
|
38
|
+
}
|
|
39
|
+
return result;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function requireFile(relative) {
|
|
43
|
+
if (!fs.existsSync(path.join(root, relative))) errors.push(`Missing required file: ${relative}`);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
for (const relative of [
|
|
47
|
+
"VERSION", "README.md", "CLAUDE.md", "MEMORY.md", "LICENSE", "package.json",
|
|
48
|
+
"THIRD_PARTY_NOTICES.md", ".claude/nccgs/THIRD_PARTY_NOTICES.md",
|
|
49
|
+
".claude/nccgs/VERSION", ".claude/nccgs/constitution.md", ".claude/nccgs/MEMORY.md",
|
|
50
|
+
".claude/nccgs/studio.json", ".claude/nccgs/workflow-catalog.json",
|
|
51
|
+
".claude/nccgs/pipeline.json", ".claude/nccgs/protocols/task-gates.md",
|
|
52
|
+
".claude/nccgs/protocols/design-lifecycle.md", ".claude/nccgs/tools/design-readiness.mjs", "scaffold/.nccgs/templates/design-contract.json",
|
|
53
|
+
".claude/nccgs/tools/task.mjs", ".claude/nccgs/tools/gates.mjs", ".claude/nccgs/tools/runtime.mjs",
|
|
54
|
+
".claude/nccgs/tools/handoff.mjs", "docs/HANDOFF-AUTOMATION.md",
|
|
55
|
+
".claude/nccgs/tools/task-slices.mjs", ".claude/nccgs/protocols/task-slices.md",
|
|
56
|
+
"scaffold/.nccgs/templates/task-slices.json", "docs/TASK-SLICING.md", "tests/task-slices.test.mjs",
|
|
57
|
+
".claude/nccgs/tools/repair-packet.mjs", ".claude/nccgs/protocols/bounded-repair.md", "scaffold/.nccgs/templates/repair-packet.json", "docs/BOUNDED-REPAIR.md",
|
|
58
|
+
".claude/nccgs/tools/unity-repair.mjs", "scaffold/.nccgs/templates/repair-unity-code.json", "scaffold/.nccgs/templates/repair-unity-editor.json",
|
|
59
|
+
"scaffold/.nccgs/gates.json", "scaffold/.nccgs/execution.json", "tests/gates.test.mjs",
|
|
60
|
+
".claude/nccgs/settings.fragment.json", ".claude/nccgs/unity-skills-manifest.json",
|
|
61
|
+
".claude/nccgs/protocols/agent-contract.md", ".claude/nccgs/protocols/orchestration.md",
|
|
62
|
+
".claude/nccgs/protocols/model-routing.md", ".claude/nccgs/protocols/context-packets.md",
|
|
63
|
+
".claude/nccgs/protocols/astra-claude.md", "scaffold/.nccgs/templates/implementation-report.md",
|
|
64
|
+
".claude/nccgs/protocols/evidence.md", ".claude/nccgs/protocols/unity-boundary.md",
|
|
65
|
+
"scaffold/.nccgs/project.yaml", "scaffold/.nccgs/state.md",
|
|
66
|
+
"scaffold/.nccgs/requirements.yaml", "scaffold/.nccgs/templates/context-packet.yaml",
|
|
67
|
+
"scaffold/.nccgs/templates/closure-record.md", "package-assets/setup-vivox-voice-chat-evals.gitignore",
|
|
68
|
+
"package-assets/nccgs-project.gitignore",
|
|
69
|
+
"scripts/cli.mjs", "scripts/doctor.mjs", "scripts/install.mjs",
|
|
70
|
+
"scripts/uninstall.mjs", ".claude/nccgs/tools/installation-guard.mjs", "tests/uninstall.test.mjs", "docs/UNINSTALL.md",
|
|
71
|
+
"scripts/recover-session.mjs",
|
|
72
|
+
"scripts/sync-unity-skills.mjs", "tests/framework.test.mjs",
|
|
73
|
+
".claude/nccgs/tools/integrity.mjs", ".claude/nccgs/tools/approve-overrides.mjs"
|
|
74
|
+
]) requireFile(relative);
|
|
75
|
+
|
|
76
|
+
const scaffoldIgnore = path.join(root, "scaffold/.nccgs/.gitignore");
|
|
77
|
+
const scaffoldIgnoreFallback = path.join(root, "package-assets/nccgs-project.gitignore");
|
|
78
|
+
if (fs.existsSync(scaffoldIgnore) && fs.readFileSync(scaffoldIgnore, "utf8") !== fs.readFileSync(scaffoldIgnoreFallback, "utf8")) {
|
|
79
|
+
errors.push("Scaffold .nccgs/.gitignore differs from its npm fallback asset.");
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const versions = [
|
|
83
|
+
fs.readFileSync(path.join(root, "VERSION"), "utf8").trim(),
|
|
84
|
+
JSON.parse(fs.readFileSync(path.join(root, "package.json"), "utf8")).version,
|
|
85
|
+
fs.readFileSync(path.join(root, ".claude/nccgs/VERSION"), "utf8").trim()
|
|
86
|
+
];
|
|
87
|
+
if (new Set(versions).size !== 1) errors.push(`Version mismatch: ${versions.join(" / ")}`);
|
|
88
|
+
|
|
89
|
+
const packageJson = JSON.parse(fs.readFileSync(path.join(root, "package.json"), "utf8"));
|
|
90
|
+
if (packageJson.private === true) errors.push("package.json is marked private and cannot be published.");
|
|
91
|
+
if (packageJson.bin?.nccgs !== "scripts/cli.mjs") errors.push("package.json does not expose the nccgs CLI.");
|
|
92
|
+
|
|
93
|
+
// This adaptation must remain attributable in the npm archive and installed tree.
|
|
94
|
+
// Canonical LF comparison accepts a Windows checkout without changing license text.
|
|
95
|
+
const ponytailDirectory = ".claude/nccgs/packages/ponytail";
|
|
96
|
+
const ponytailFiles = ["manifest.json", "README.md", "engineering.md", "complexity-review.md", "NOTICE.md", "LICENSE"];
|
|
97
|
+
for (const file of ponytailFiles) requireFile(`${ponytailDirectory}/${file}`);
|
|
98
|
+
try {
|
|
99
|
+
const metadata = JSON.parse(fs.readFileSync(path.join(root, ponytailDirectory, "manifest.json"), "utf8"));
|
|
100
|
+
if (metadata.schemaVersion !== 1 || metadata.name !== "nccgs-ponytail" || metadata.kind !== "shared-guidance"
|
|
101
|
+
|| metadata.entrypoint !== "README.md" || metadata.license !== "MIT"
|
|
102
|
+
|| metadata.licenseFile !== "LICENSE" || metadata.noticeFile !== "NOTICE.md"
|
|
103
|
+
|| JSON.stringify(metadata.files) !== JSON.stringify(ponytailFiles)) throw new Error("invalid package metadata or file inventory");
|
|
104
|
+
if (metadata.upstream?.repository !== "https://github.com/DietrichGebert/ponytail"
|
|
105
|
+
|| !/^[a-f0-9]{40}$/.test(metadata.upstream?.commit ?? "")
|
|
106
|
+
|| !metadata.upstream?.version || !metadata.upstream?.copyright) throw new Error("missing pinned upstream attribution");
|
|
107
|
+
const license = fs.readFileSync(path.join(root, ponytailDirectory, "LICENSE"), "utf8").replace(/\r\n/g, "\n");
|
|
108
|
+
const expected = metadata.upstream.sources.find(item => item.path === "LICENSE")?.sha256LF;
|
|
109
|
+
if (crypto.createHash("sha256").update(license).digest("hex") !== expected
|
|
110
|
+
|| !license.includes(metadata.upstream.copyright)) throw new Error("upstream MIT license text differs from its source fingerprint");
|
|
111
|
+
for (const relative of ["THIRD_PARTY_NOTICES.md", ".claude/nccgs/THIRD_PARTY_NOTICES.md", `${ponytailDirectory}/NOTICE.md`]) {
|
|
112
|
+
const notice = fs.readFileSync(path.join(root, relative), "utf8");
|
|
113
|
+
if (!notice.includes("Ponytail") || !notice.includes(metadata.upstream.copyright)
|
|
114
|
+
|| !notice.includes(metadata.upstream.commit)) throw new Error(`missing attribution in ${relative}`);
|
|
115
|
+
}
|
|
116
|
+
} catch (error) { errors.push(`Invalid Ponytail shared package: ${error.message}`); }
|
|
117
|
+
|
|
118
|
+
const rootClaude = fs.readFileSync(path.join(root, "CLAUDE.md"), "utf8");
|
|
119
|
+
if (!rootClaude.includes("@.claude/nccgs/constitution.md")) errors.push("CLAUDE.md does not import the NCCGS constitution.");
|
|
120
|
+
if (!/^@MEMORY\.md$/m.test(fs.readFileSync(path.join(root, ".claude/nccgs/constitution.md"), "utf8"))) errors.push("Constitution must import adjacent MEMORY.md using a relative path.");
|
|
121
|
+
|
|
122
|
+
const studio = JSON.parse(fs.readFileSync(path.join(root, ".claude/nccgs/studio.json"), "utf8"));
|
|
123
|
+
const pipeline = JSON.parse(fs.readFileSync(path.join(root, ".claude/nccgs/pipeline.json"), "utf8"));
|
|
124
|
+
try {
|
|
125
|
+
const execution = executionPolicy(JSON.parse(fs.readFileSync(path.join(root, "scaffold/.nccgs/execution.json"), "utf8")));
|
|
126
|
+
if (execution.name !== "standalone" || execution.externalReview.mode !== "off") errors.push("New-install execution must default to standalone with external review off.");
|
|
127
|
+
if (execution.designReviewRequired !== true) errors.push("New installs must require reviewed design handoffs.");
|
|
128
|
+
if (execution.reviewContractRequired !== true) errors.push("New installs must require reviewer-owned verdicts and conditions.");
|
|
129
|
+
} catch (error) { errors.push(`Invalid scaffold execution policy: ${error.message}`); }
|
|
130
|
+
if (pipeline.defaultProfile !== "claude-standalone") errors.push("New-install default model profile must be claude-standalone.");
|
|
131
|
+
if (pipeline.executionPolicies?.standalone?.runtime !== "claude-code" || pipeline.executionPolicies?.standalone?.planningOwner !== "Claude Code" || pipeline.executionPolicies?.standalone?.documentsOwner !== "Claude Code") errors.push("Standalone core must be owned by Claude Code.");
|
|
132
|
+
for (const stale of compile(root)) errors.push(`Stale generated policy: ${stale}; run npm run policy:build`);
|
|
133
|
+
const agentNames = new Set();
|
|
134
|
+
for (const agent of studio.agents ?? []) {
|
|
135
|
+
if (agentNames.has(agent.name)) errors.push(`Duplicate catalog agent: ${agent.name}`);
|
|
136
|
+
agentNames.add(agent.name);
|
|
137
|
+
const active = pipeline.profiles[pipeline.defaultProfile].activeAgents.includes(agent.name);
|
|
138
|
+
const file = path.join(root, active ? ".claude/agents" : ".claude/nccgs/agent-library", `${agent.name}.md`);
|
|
139
|
+
if (!fs.existsSync(file)) { errors.push(`Catalog agent file missing: ${agent.name}`); continue; }
|
|
140
|
+
const metadata = frontmatter(file);
|
|
141
|
+
if (metadata?.name !== agent.name || !metadata?.description) errors.push(`Invalid agent frontmatter: ${agent.name}`);
|
|
142
|
+
const expected = studio.modelProfiles[studio.defaultProfile]?.[agent.modelClass];
|
|
143
|
+
if (active && metadata?.model !== expected) errors.push(`Agent model mismatch: ${agent.name} expected ${expected}, found ${metadata?.model}`);
|
|
144
|
+
const expectedEffort = studio.effortProfiles?.[studio.defaultProfile]?.[agent.modelClass];
|
|
145
|
+
if (active && metadata?.effort !== expectedEffort) errors.push(`Agent effort mismatch: ${agent.name} expected ${expectedEffort}, found ${metadata?.effort}`);
|
|
146
|
+
if ((metadata?.tools ?? "").split(/,\s*/).some(tool => ["Task", "Agent"].includes(tool))) errors.push(`Agent may recursively delegate: ${agent.name}`);
|
|
147
|
+
if (!fs.readFileSync(file, "utf8").includes("protocols/agent-contract.md")) errors.push(`Agent does not load common contract: ${agent.name}`);
|
|
148
|
+
if (fs.readFileSync(file, "utf8").split(/\r?\n/).length > 80) errors.push(`Agent entrypoint exceeds 80 lines: ${agent.name}`);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
const actualAgents = walk(path.join(root, ".claude/agents")).filter((file) => file.endsWith(".md"));
|
|
152
|
+
for (const file of actualAgents) {
|
|
153
|
+
if (!agentNames.has(path.basename(file, ".md"))) errors.push(`Uncataloged NCCGS agent: ${posix(path.relative(root, file))}`);
|
|
154
|
+
if (!pipeline.coreAgents.includes(path.basename(file, ".md"))) errors.push(`Non-core agent active in source package: ${posix(path.relative(root, file))}`);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
const workflows = JSON.parse(fs.readFileSync(path.join(root, ".claude/nccgs/workflow-catalog.json"), "utf8"));
|
|
158
|
+
const workflowNames = new Set();
|
|
159
|
+
for (const workflow of workflows.workflows ?? []) {
|
|
160
|
+
if (workflowNames.has(workflow.name)) errors.push(`Duplicate workflow: ${workflow.name}`);
|
|
161
|
+
workflowNames.add(workflow.name);
|
|
162
|
+
const file = path.join(root, ".claude", "skills", workflow.name, "SKILL.md");
|
|
163
|
+
if (!fs.existsSync(file)) { errors.push(`Workflow skill missing: ${workflow.name}`); continue; }
|
|
164
|
+
const metadata = frontmatter(file);
|
|
165
|
+
if (metadata?.name !== workflow.name || !metadata?.description) errors.push(`Invalid workflow frontmatter: ${workflow.name}`);
|
|
166
|
+
const wrapper = fs.readFileSync(file, "utf8");
|
|
167
|
+
if (!wrapper.includes(".nccgs/execution.json") || !wrapper.includes("../../nccgs/protocols/standalone.md")) errors.push(`Workflow lacks execution-first standalone routing: ${workflow.name}`);
|
|
168
|
+
const lines = fs.readFileSync(file, "utf8").split(/\r?\n/).length;
|
|
169
|
+
if (lines > 120) errors.push(`Workflow entrypoint exceeds 120 lines: ${workflow.name}`);
|
|
170
|
+
if (fs.existsSync(path.join(path.dirname(file), "README.md"))) errors.push(`NCCGS-owned skill contains redundant README: ${workflow.name}`);
|
|
171
|
+
for (const role of workflow.primaryRoles ?? []) if (!agentNames.has(role)) errors.push(`Workflow ${workflow.name} references unknown role ${role}`);
|
|
172
|
+
for (const [profile, roles] of Object.entries(workflow.primaryRolesByProfile ?? {})) {
|
|
173
|
+
if (!studio.modelProfiles[profile]) errors.push(`Workflow ${workflow.name} references unknown profile ${profile}`);
|
|
174
|
+
for (const role of roles) if (!agentNames.has(role)) errors.push(`Workflow ${workflow.name} references unknown role ${role}`);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
for (const entry of workflows.entrypoints ?? []) if (!workflowNames.has(entry)) errors.push(`Missing entrypoint workflow: ${entry}`);
|
|
178
|
+
|
|
179
|
+
const projectScaffold = fs.readFileSync(path.join(root, "scaffold/.nccgs/project.yaml"), "utf8");
|
|
180
|
+
for (const pattern of [
|
|
181
|
+
/^schema_version:\s*2\s*$/m, /^studio:\s*$/m, /^models:\s*$/m,
|
|
182
|
+
/^context:\s*$/m, /^closure:\s*$/m, /max_agent_depth:\s*1/m,
|
|
183
|
+
/single_writer_per_path:\s*true/m, /independent_verification:\s*true/m,
|
|
184
|
+
/canonical_record:\s*"\.nccgs\/closures\/\{feature_id\}\.md"/m
|
|
185
|
+
]) if (!pattern.test(projectScaffold)) errors.push(`Project scaffold lacks schema invariant: ${pattern}`);
|
|
186
|
+
|
|
187
|
+
const settingsFragment = JSON.parse(fs.readFileSync(path.join(root, ".claude/nccgs/settings.fragment.json"), "utf8"));
|
|
188
|
+
const hookCommands = [];
|
|
189
|
+
for (const [event, groups] of Object.entries(settingsFragment.hooks ?? {})) {
|
|
190
|
+
for (const group of groups) for (const hook of group.hooks ?? []) {
|
|
191
|
+
hookCommands.push(hook);
|
|
192
|
+
if (["PreToolUse", "SubagentStart", "SubagentStop"].includes(event) && hook.onFailure !== "block") errors.push(`Integrity hook must block on runtime failure: ${event}`);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
const hookKeys = hookCommands.map(managedHookKey);
|
|
196
|
+
if (hookKeys.length !== new Set(hookKeys).size) errors.push("Managed hook command is duplicated.");
|
|
197
|
+
for (const hook of hookCommands) {
|
|
198
|
+
const key = managedHookKey(hook);
|
|
199
|
+
if (!key || hook.command !== "node" || !Array.isArray(hook.args)) { errors.push("Managed hook must use direct Node argv"); continue; }
|
|
200
|
+
const relative = JSON.parse(key)[0], file = path.join(root, ...relative.split("/"));
|
|
201
|
+
if (!fs.existsSync(file)) { errors.push(`Managed hook script missing: ${relative}`); continue; }
|
|
202
|
+
const check = spawnSync(process.execPath, ["--check", file], { encoding: "utf8" });
|
|
203
|
+
if (check.status !== 0) errors.push(`Hook syntax failure ${relative}: ${check.stderr.trim()}`);
|
|
204
|
+
}
|
|
205
|
+
for (const file of [
|
|
206
|
+
...walk(path.join(root, ".claude/nccgs/tools")).filter(file => file.endsWith(".mjs")),
|
|
207
|
+
path.join(root, "scripts/cli.mjs"),
|
|
208
|
+
path.join(root, "scripts/run.mjs"),
|
|
209
|
+
path.join(root, "scripts/doctor.mjs"),
|
|
210
|
+
path.join(root, "scripts/install.mjs")
|
|
211
|
+
]) {
|
|
212
|
+
const check = spawnSync(process.execPath, ["--check", file], { encoding: "utf8" });
|
|
213
|
+
if (check.status !== 0) errors.push(`Script syntax failure ${posix(path.relative(root, file))}: ${check.stderr.trim()}`);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
const rules = walk(path.join(root, ".claude/rules")).filter((file) => file.endsWith(".md"));
|
|
217
|
+
if (rules.length < 11) errors.push(`Expected at least 11 path-scoped rules, found ${rules.length}.`);
|
|
218
|
+
|
|
219
|
+
const manifestPath = path.join(root, ".claude/nccgs/unity-skills-manifest.json");
|
|
220
|
+
const npmFallbackFiles = new Map([
|
|
221
|
+
["setup-vivox-voice-chat/evals/.gitignore", path.join(root, "package-assets/setup-vivox-voice-chat-evals.gitignore")]
|
|
222
|
+
]);
|
|
223
|
+
let manifest;
|
|
224
|
+
try { manifest = JSON.parse(fs.readFileSync(manifestPath, "utf8")); }
|
|
225
|
+
catch (error) { errors.push(`Unity Skills manifest is invalid JSON: ${error.message}`); }
|
|
226
|
+
const allSkillNames = new Set();
|
|
227
|
+
for (const directory of fs.readdirSync(path.join(root, ".claude/skills"), { withFileTypes: true })) {
|
|
228
|
+
if (!directory.isDirectory()) continue;
|
|
229
|
+
const skillFile = path.join(root, ".claude/skills", directory.name, "SKILL.md");
|
|
230
|
+
if (!fs.existsSync(skillFile)) { warnings.push(`Skill directory has no SKILL.md: ${directory.name}`); continue; }
|
|
231
|
+
const metadata = frontmatter(skillFile);
|
|
232
|
+
if (!metadata?.name || !metadata?.description) errors.push(`Skill lacks name/description: ${directory.name}`);
|
|
233
|
+
if (metadata?.name !== directory.name) errors.push(`Skill folder/name mismatch: ${directory.name} != ${metadata?.name}`);
|
|
234
|
+
if (allSkillNames.has(metadata?.name)) errors.push(`Duplicate skill name: ${metadata?.name}`);
|
|
235
|
+
allSkillNames.add(metadata?.name);
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
if (manifest) {
|
|
239
|
+
for (const skill of manifest.skills ?? []) {
|
|
240
|
+
const skillDir = path.join(root, ".claude/skills", skill.name);
|
|
241
|
+
if (!fs.existsSync(skillDir)) { errors.push(`Vendored Unity Skill missing: ${skill.name}`); continue; }
|
|
242
|
+
const expectedFiles = new Set();
|
|
243
|
+
for (const item of skill.files ?? []) {
|
|
244
|
+
expectedFiles.add(item.path);
|
|
245
|
+
let file = path.join(skillDir, ...item.path.split("/"));
|
|
246
|
+
if (!fs.existsSync(file)) file = npmFallbackFiles.get(`${skill.name}/${item.path}`) ?? file;
|
|
247
|
+
if (!fs.existsSync(file)) { errors.push(`Vendored Unity Skill file missing: ${skill.name}/${item.path}`); continue; }
|
|
248
|
+
if (sha256(file) !== item.sha256) errors.push(`Vendored Unity Skill modified: ${skill.name}/${item.path}`);
|
|
249
|
+
}
|
|
250
|
+
for (const file of walk(skillDir)) {
|
|
251
|
+
const relative = posix(path.relative(skillDir, file));
|
|
252
|
+
if (!expectedFiles.has(relative)) errors.push(`Untracked file in vendored Unity Skill: ${skill.name}/${relative}`);
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
const markdownRoots = [path.join(root, "README.md"), path.join(root, "docs"), path.join(root, ".claude/nccgs"), ...[...workflowNames].map((name) => path.join(root, ".claude/skills", name))];
|
|
258
|
+
for (const item of markdownRoots) {
|
|
259
|
+
if (!fs.existsSync(item)) continue;
|
|
260
|
+
const files = fs.statSync(item).isDirectory() ? walk(item).filter((file) => file.endsWith(".md")) : [item];
|
|
261
|
+
for (const file of files) {
|
|
262
|
+
const text = fs.readFileSync(file, "utf8");
|
|
263
|
+
for (const match of text.matchAll(/\[[^\]]+\]\(([^)]+)\)/g)) {
|
|
264
|
+
const link = match[1].trim();
|
|
265
|
+
if (!link || link.startsWith("#") || /^[a-z]+:/i.test(link)) continue;
|
|
266
|
+
const target = path.resolve(path.dirname(file), link.split("#")[0]);
|
|
267
|
+
if (!fs.existsSync(target)) errors.push(`Broken relative link in ${posix(path.relative(root, file))}: ${link}`);
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
for (const warning of warnings) console.warn(`WARN: ${warning}`);
|
|
273
|
+
if (errors.length) {
|
|
274
|
+
for (const error of errors) console.error(`ERROR: ${error}`);
|
|
275
|
+
console.error(`\nNCCGS validation failed with ${errors.length} error(s).`);
|
|
276
|
+
process.exit(1);
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
console.log("NCCGS validation passed.");
|
|
280
|
+
console.log(`NCCGS workflows: ${workflowNames.size} (${workflows.entrypoints.length} entrypoints)`);
|
|
281
|
+
console.log(`Studio agents: ${agentNames.size} in library; ${pipeline.coreAgents.length} active by default`);
|
|
282
|
+
console.log(`Path-scoped rules: ${rules.length}`);
|
|
283
|
+
console.log(`Managed hooks: ${hookCommands.length}`);
|
|
284
|
+
console.log(`Vendored Unity Skills: ${manifest?.skills?.length ?? 0}`);
|
|
285
|
+
console.log(`Discoverable skills total: ${allSkillNames.size}`);
|