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,261 +1,261 @@
1
- #!/usr/bin/env node
2
-
3
- import fs from "node:fs";
4
- import path from "node:path";
5
- import { matchesFingerprint, sameFingerprint } from "../.claude/nccgs/tools/integrity.mjs";
6
- import { check as checkTask, snapshot, stages } from "../.claude/nccgs/tools/gates.mjs";
7
- import { hash, json, profilePolicy, resolveExecution, verifyArtifact } from "../.claude/nccgs/tools/policy.mjs";
8
- import { assertProjectRoot, assertRelativePath, containedPath } from "../.claude/nccgs/tools/paths.mjs";
9
- import { inputPolicy } from "../.claude/nccgs/tools/inputs.mjs";
10
- import { autonomyPolicy } from "../.claude/nccgs/tools/autonomy.mjs";
11
-
12
- const args = process.argv.slice(2);
13
- const projectIndex = args.indexOf("--project");
14
- const allowMigrationRequired = args.includes("--allow-migration-required") || args.includes("--allow-policy-migration");
15
- const warnings = [];
16
- const errors = [];
17
- if (projectIndex >= 0 && !args[projectIndex + 1]) {
18
- console.error("--project requires a directory path.");
19
- process.exit(2);
20
- }
21
-
22
- const project = projectIndex >= 0 ? path.resolve(args[projectIndex + 1]) : process.cwd();
23
- try { assertProjectRoot(project); }
24
- catch (error) { console.error(`ERROR: ${error.message}`); process.exit(1); }
25
- for (const marker of ["Assets", "Packages", "ProjectSettings"]) {
26
- const file = safePath(marker);
27
- if (file && !fs.existsSync(file)) errors.push(`Missing Unity marker: ${marker}`);
28
- }
29
-
30
- const statePath = safePath(".claude/nccgs/install-state.json");
31
- const overridePath = safePath(".nccgs/framework-overrides.json");
32
- const state = readJson(statePath, "NCCGS install state", true);
33
- const overrideManifest = readJson(overridePath, "NCCGS framework override manifest", false);
34
- if (state && ![1, 2, 3].includes(state.schemaVersion)) errors.push(`Unsupported install-state schema: ${state.schemaVersion ?? "unknown"}.`);
35
- if (overrideManifest && overrideManifest.schemaVersion !== 1) errors.push(`Unsupported framework override schema: ${overrideManifest.schemaVersion ?? "unknown"}.`);
36
-
37
- const overrides = new Map();
38
- for (const item of overrideManifest?.overrides ?? []) {
39
- if (!isManagedRelative(item.path) || !new Set(["preserve", "absent"]).has(item.disposition)
40
- || !new Set(["approved", "pending_review"]).has(item.status)) {
41
- errors.push(`Invalid framework override record: ${item.path ?? "path missing"}`);
42
- continue;
43
- }
44
- if (overrides.has(item.path)) errors.push(`Duplicate framework override: ${item.path}`);
45
- overrides.set(item.path, item);
46
- }
47
-
48
- let verifiedManaged = 0;
49
- if (state) {
50
- for (const item of state.files ?? []) {
51
- if (!isManagedRelative(item.relative)) {
52
- errors.push(`Unsafe managed path in install state: ${item.relative ?? "missing"}`);
53
- continue;
54
- }
55
- const file = safePath(item.relative);
56
- if (!file) continue;
57
- const override = overrides.get(item.relative);
58
- if (override) {
59
- const recordedUpstream = {
60
- sha256: override.upstreamSha256,
61
- canonicalSha256: override.upstreamCanonicalSha256
62
- };
63
- if (!sameFingerprint(item, recordedUpstream)) errors.push(`Framework override has stale upstream hashes: ${item.relative}`);
64
- validateOverride(override, file);
65
- verifiedManaged++;
66
- continue;
67
- }
68
- if (!fs.existsSync(file)) errors.push(`Managed file is missing: ${item.relative}`);
69
- else if (!matchesFingerprint(file, item)) errors.push(`Managed file was modified: ${item.relative}`);
70
- else verifiedManaged++;
71
- }
72
- }
73
- for (const item of overrides.values()) {
74
- if ((state?.files ?? []).some((managed) => managed.relative === item.path)) continue;
75
- const file = safePath(item.path);
76
- if (file) validateOverride(item, file);
77
- }
78
-
79
- const claudePath = safePath("CLAUDE.md");
80
- if (claudePath && (!fs.existsSync(claudePath) || !fs.readFileSync(claudePath, "utf8").includes("@.claude/nccgs/constitution.md"))) {
81
- errors.push("CLAUDE.md does not import the NCCGS constitution.");
82
- }
83
-
84
- const policyPath = safePath(".nccgs/project.yaml");
85
- let policySchema = null;
86
- if (policyPath && !fs.existsSync(policyPath)) errors.push(".nccgs/project.yaml is missing.");
87
- else if (policyPath) {
88
- const policyText = fs.readFileSync(policyPath, "utf8");
89
- const match = policyText.match(/^schema_version:\s*(\d+)\s*$/m);
90
- policySchema = match ? Number(match[1]) : null;
91
- if (policySchema !== 2) migrationIssue(`Project policy schema must be 2; found ${policySchema ?? "unknown"}.`);
92
- if (/^profile_contracts:/m.test(policyText)) migrationIssue("Legacy embedded profile_contracts require Astra migration to pipeline.json plus project gate mapping.");
93
- }
94
-
95
- validateSettings();
96
- let execution = null;
97
- try {
98
- execution = resolveExecution(project);
99
- if (!fs.existsSync(containedPath(project, ".nccgs/execution.json"))) warnings.push("No execution.json: compatibility obligations remain active. Standalone requires explicit migrate-execution; updating does not waive pending external review.");
100
- if (!execution.requireObservedEffort) warnings.push("Execution permits unknown observed effort; configured effort is intent, not runtime proof. Report unavailable provider model/effort evidence as unknown.");
101
- } catch (error) { errors.push(`Invalid execution policy: ${error.message}`); }
102
- try { inputPolicy(project); }
103
- catch (error) { errors.push(`Invalid canonical input/output policy: ${error.message}`); }
104
-
105
- const activationPath = safePath(".nccgs/agent-activation.json");
106
- if (activationPath && fs.existsSync(activationPath)) {
107
- try {
108
- const activation = json(activationPath);
109
- for (const [relative, expected] of Object.entries(activation.generated)) {
110
- const bytes = fs.readFileSync(containedPath(project, relative));
111
- if (hash(bytes) !== expected && hash(bytes.toString("utf8").replace(/\r\n/g, "\n")) !== activation.generatedCanonical?.[relative]) errors.push(`Activated agent modified: ${relative}`);
112
- }
113
- for (const warning of activation.warnings ?? []) migrationIssue(warning);
114
- } catch (error) { errors.push(`Invalid agent activation state: ${error.message}`); }
115
- }
116
- const tasksDir = safePath(".nccgs/tasks");
117
- if (tasksDir && fs.existsSync(tasksDir)) for (const file of fs.readdirSync(tasksDir).filter(f => f.endsWith(".json"))) {
118
- try {
119
- const task = json(containedPath(project, `.nccgs/tasks/${file}`));
120
- if (task.schemaVersion !== 2 && execution?.name === "standalone") warnings.push(`Legacy task ${task.id} retains compatibility stages and budget; explicit continuation/re-pin is required to adopt standalone policy.`);
121
- const executionCurrent = task.schemaVersion !== 2 || hash(JSON.stringify(execution)) === task.executionHash;
122
- if (!executionCurrent) warnings.push(`Task ${task.id} is pinned to another execution policy; preserve its history and explicitly continue/re-pin before claiming completion.`);
123
- if (!task.canonicalInputs) warnings.push(`Task ${task.id} predates canonical input pins; it is historical only until refresh-inputs and fresh evidence are recorded.`);
124
- if (task.closed) {
125
- const required = task.schemaVersion === 2 ? [task.mode === "implement" ? "implementation" : "analysis", "review", "report"] : stages;
126
- const terminal = task.schemaVersion === 2 ? task.stages?.report : task.stages?.acceptance;
127
- if (!required.every(s => task.stages?.[s]) || task.closed.snapshot !== terminal?.snapshot) throw new Error("Closure lacks its required stage records");
128
- if (task.schemaVersion === 2 && (task.execution?.acceptanceRequired || task.contract?.acceptanceRequired) && !task.stages?.acceptance) throw new Error("Closure lacks required acceptance");
129
- verifyStoredEvidence(task);
130
- if (executionCurrent && task.canonicalInputs && task.closed.snapshot === snapshot(project, task).sha256) checkTask(project, task, "done");
131
- else warnings.push(`Historical closure ${task.id} is for a different snapshot; it cannot approve current work.`);
132
- }
133
- } catch (error) { errors.push(`Invalid closed task ${file}: ${error.message}`); }
134
- }
135
- if (errors.length) {
136
- for (const error of errors) console.error(`ERROR: ${error}`);
137
- for (const warning of warnings) console.warn(`WARN: ${warning}`);
138
- console.error(`NCCGS doctor failed with ${errors.length} error(s).`);
139
- process.exit(1);
140
- }
141
-
142
- for (const warning of warnings) console.warn(`WARN: ${warning}`);
143
- console.log("NCCGS doctor passed.");
144
- console.log(`Framework version: ${state.frameworkVersion}`);
145
- console.log(`Model profile: ${state.modelProfile}`);
146
- console.log(`Execution policy: ${execution?.name}; external review: ${execution?.externalReview.mode}; maximum parallel workers: ${execution?.maxParallelWorkers}`);
147
- console.log(`Reviewed design gate: ${execution?.name === "standalone" && execution?.designReviewRequired === true ? "required for feature implementation" : "legacy/compatibility policy; not proof of document readiness"}`);
148
- console.log(`Managed files verified: ${verifiedManaged}`);
149
- console.log(`Framework overrides: ${overrides.size} (${[...overrides.values()].filter((item) => item.status !== "approved").length} pending review)`);
150
- console.log(`Project policy schema: ${policySchema ?? "migration required"}`);
151
- console.log("Settings scope: project settings and observable local overrides only; CLI, user/managed settings, provider routing and active-session model/effort still require runtime verification.");
152
-
153
- function validateOverride(item, file) {
154
- if (item.status !== "approved") migrationIssue(`Framework override awaits /migrate-project review: ${item.path}`);
155
- if (item.disposition === "absent") {
156
- if (fs.existsSync(file)) errors.push(`Override expects a missing file, but it exists: ${item.path}`);
157
- return;
158
- }
159
- if (!fs.existsSync(file)) errors.push(`Preserved framework override is missing: ${item.path}`);
160
- else if (!matchesFingerprint(file, item)) errors.push(`Preserved framework override changed after it was recorded: ${item.path}`);
161
- }
162
-
163
- function migrationIssue(message) {
164
- if (allowMigrationRequired) warnings.push(message);
165
- else errors.push(message);
166
- }
167
-
168
- function readJson(file, label, required) {
169
- if (!file) return null;
170
- if (!fs.existsSync(file)) {
171
- if (required) errors.push(`${label} is missing.`);
172
- return null;
173
- }
174
- try { return JSON.parse(fs.readFileSync(file, "utf8")); }
175
- catch (error) {
176
- errors.push(`${label} is invalid JSON: ${error.message}`);
177
- return null;
178
- }
179
- }
180
-
181
- function isManagedRelative(value) {
182
- try { return assertRelativePath(value).startsWith(".claude/"); }
183
- catch { return false; }
184
- }
185
-
186
- function safePath(relative) {
187
- try { return containedPath(project, relative); }
188
- catch (error) { errors.push(`Unsafe NCCGS path: ${error.message}`); return null; }
189
- }
190
-
191
- function verifyStoredEvidence(task) {
192
- const verify = (ref, runtime = false) => {
193
- const file = verifyArtifact(project, ref);
194
- if (!ref.path.endsWith(".json")) return;
195
- const record = json(file);
196
- if (runtime) {
197
- verifyArtifact(project, record.observationSource);
198
- if (record.evidenceMigration?.archivedObservation) verifyArtifact(project, record.evidenceMigration.archivedObservation);
199
- } else if (record.origin === "verification-command") verifyArtifact(project, record.log);
200
- };
201
- for (const stage of Object.values(task.stages ?? {})) {
202
- if (stage.runtime) verify(stage.runtime, true);
203
- if (stage.evidence) verify(stage.evidence);
204
- for (const check of stage.checks ?? []) verify(check.evidence);
205
- for (const artifact of stage.artifacts ?? []) verify(artifact);
206
- }
207
- for (const receipt of task.externalReviews ?? []) verify(receipt.evidence);
208
- }
209
-
210
- function validateSettings() {
211
- const settings = readJson(safePath(".claude/settings.json"), "Claude project settings", true);
212
- const fragment = readJson(safePath(".claude/nccgs/settings.fragment.json"), "NCCGS settings fragment", true);
213
- const local = readJson(safePath(".claude/settings.local.json"), "Claude local settings", false);
214
- const object = value => value && typeof value === "object" && !Array.isArray(value);
215
- if (!object(settings) || !object(fragment)) {
216
- errors.push("Claude project settings and NCCGS settings fragment must be JSON objects.");
217
- return;
218
- }
219
- if (local !== null && !object(local)) errors.push("Claude local settings must be a JSON object.");
220
- for (const [label, value] of [["project", settings], ["local", local]]) {
221
- if (value?.disableAllHooks === true) errors.push(`Claude ${label} settings disable all hooks.`);
222
- }
223
- for (const [event, groups] of Object.entries(fragment.hooks ?? {})) {
224
- for (const expected of groups) for (const hook of expected.hooks ?? []) {
225
- const actualGroups = Array.isArray(settings.hooks?.[event]) ? settings.hooks[event] : [];
226
- const present = actualGroups.some(group => group && (group.matcher ?? "") === (expected.matcher ?? "")
227
- && Array.isArray(group.hooks) && group.hooks.some(actual => actual && actual.type === hook.type && actual.command === hook.command
228
- && actual.async !== true && (actual.timeout === undefined || actual.timeout >= hook.timeout)));
229
- if (!present) errors.push(`Required NCCGS hook missing or altered: ${event} [${expected.matcher ?? ""}] ${hook.command}`);
230
- }
231
- }
232
- let autonomy = "guarded";
233
- try { autonomy = autonomyPolicy(project).mode; }
234
- catch (error) { errors.push(error.message); }
235
- for (const [key, required] of Object.entries(fragment.permissions ?? {})) {
236
- for (const rule of required) if (!Array.isArray(settings.permissions?.[key]) || !settings.permissions[key].includes(rule)) {
237
- errors.push(`Required NCCGS permission ${key} rule missing: ${rule}`);
238
- }
239
- }
240
- if (autonomy === "full") {
241
- console.log("Task autonomy: full (native permission mode requires nccgs run; configuration is not runtime proof).");
242
- console.log("NCCGS integrity/destructive-action guards, explicit deny/ask rules and managed/OS restrictions remain effective.");
243
- }
244
- try {
245
- if (!safePath(".claude/nccgs/pipeline.json")) return;
246
- const { name, profile } = profilePolicy(project, state?.modelProfile);
247
- const declared = policyPath && fs.existsSync(policyPath)
248
- ? fs.readFileSync(policyPath, "utf8").match(/^models:[ \t]*\r?\n((?:[ \t]+[^\n]*\n?|\s*\n)*)/m)?.[1]?.match(/^\s+profile:\s*([^#\r\n]+)/m)?.[1]?.trim().replace(/^['"]|['"]$/g, "") : null;
249
- if (declared && declared !== name) errors.push(`Project model profile ${declared} differs from installed profile ${name}; reconfigure models.`);
250
- if (profile.session) {
251
- const route = profile.models[profile.session.class];
252
- if (settings.model !== route.model) errors.push(`Project session model must be ${route.model}; found ${settings.model ?? "missing"}.`);
253
- if (settings.effortLevel !== route.effort) errors.push(`Project session effort must be ${route.effort}; found ${settings.effortLevel ?? "missing"}.`);
254
- if (settings.modelSettings?.[route.model]?.effortLevel !== route.effort) errors.push(`Project per-model effort for ${route.model} must be ${route.effort}.`);
255
- if (local?.model !== undefined && local.model !== route.model) errors.push(`Local settings override the required session model ${route.model}.`);
256
- if (local?.effortLevel !== undefined && local.effortLevel !== route.effort) errors.push(`Local settings override the required session effort ${route.effort}.`);
257
- if (local?.modelSettings?.[route.model]?.effortLevel !== undefined && local.modelSettings[route.model].effortLevel !== route.effort) errors.push(`Local settings override the required per-model effort for ${route.model}.`);
258
- }
259
- } catch (error) { errors.push(`Cannot verify session model policy: ${error.message}`); }
260
- if (local) warnings.push("Local Claude settings are present; project requirements were checked without treating local hook additions as replacements. Runtime overrides are not fully observable by doctor.");
261
- }
1
+ #!/usr/bin/env node
2
+ import { requiredHookPresent } from "../.claude/nccgs/tools/hook-transport.mjs";
3
+
4
+ import fs from "node:fs";
5
+ import path from "node:path";
6
+ import { matchesFingerprint, sameFingerprint } from "../.claude/nccgs/tools/integrity.mjs";
7
+ import { check as checkTask, snapshot, stages } from "../.claude/nccgs/tools/gates.mjs";
8
+ import { hash, json, profilePolicy, resolveExecution, verifyArtifact } from "../.claude/nccgs/tools/policy.mjs";
9
+ import { assertProjectRoot, assertRelativePath, containedPath } from "../.claude/nccgs/tools/paths.mjs";
10
+ import { inputPolicy } from "../.claude/nccgs/tools/inputs.mjs";
11
+ import { autonomyPolicy } from "../.claude/nccgs/tools/autonomy.mjs";
12
+
13
+ const args = process.argv.slice(2);
14
+ const projectIndex = args.indexOf("--project");
15
+ const allowMigrationRequired = args.includes("--allow-migration-required") || args.includes("--allow-policy-migration");
16
+ const warnings = [];
17
+ const errors = [];
18
+ if (projectIndex >= 0 && !args[projectIndex + 1]) {
19
+ console.error("--project requires a directory path.");
20
+ process.exit(2);
21
+ }
22
+
23
+ const project = projectIndex >= 0 ? path.resolve(args[projectIndex + 1]) : process.cwd();
24
+ try { assertProjectRoot(project); }
25
+ catch (error) { console.error(`ERROR: ${error.message}`); process.exit(1); }
26
+ for (const marker of ["Assets", "Packages", "ProjectSettings"]) {
27
+ const file = safePath(marker);
28
+ if (file && !fs.existsSync(file)) errors.push(`Missing Unity marker: ${marker}`);
29
+ }
30
+
31
+ const statePath = safePath(".claude/nccgs/install-state.json");
32
+ const overridePath = safePath(".nccgs/framework-overrides.json");
33
+ const state = readJson(statePath, "NCCGS install state", true);
34
+ const overrideManifest = readJson(overridePath, "NCCGS framework override manifest", false);
35
+ if (state && ![1, 2, 3].includes(state.schemaVersion)) errors.push(`Unsupported install-state schema: ${state.schemaVersion ?? "unknown"}.`);
36
+ if (overrideManifest && overrideManifest.schemaVersion !== 1) errors.push(`Unsupported framework override schema: ${overrideManifest.schemaVersion ?? "unknown"}.`);
37
+
38
+ const overrides = new Map();
39
+ for (const item of overrideManifest?.overrides ?? []) {
40
+ if (!isManagedRelative(item.path) || !new Set(["preserve", "absent"]).has(item.disposition)
41
+ || !new Set(["approved", "pending_review"]).has(item.status)) {
42
+ errors.push(`Invalid framework override record: ${item.path ?? "path missing"}`);
43
+ continue;
44
+ }
45
+ if (overrides.has(item.path)) errors.push(`Duplicate framework override: ${item.path}`);
46
+ overrides.set(item.path, item);
47
+ }
48
+
49
+ let verifiedManaged = 0;
50
+ if (state) {
51
+ for (const item of state.files ?? []) {
52
+ if (!isManagedRelative(item.relative)) {
53
+ errors.push(`Unsafe managed path in install state: ${item.relative ?? "missing"}`);
54
+ continue;
55
+ }
56
+ const file = safePath(item.relative);
57
+ if (!file) continue;
58
+ const override = overrides.get(item.relative);
59
+ if (override) {
60
+ const recordedUpstream = {
61
+ sha256: override.upstreamSha256,
62
+ canonicalSha256: override.upstreamCanonicalSha256
63
+ };
64
+ if (!sameFingerprint(item, recordedUpstream)) errors.push(`Framework override has stale upstream hashes: ${item.relative}`);
65
+ validateOverride(override, file);
66
+ verifiedManaged++;
67
+ continue;
68
+ }
69
+ if (!fs.existsSync(file)) errors.push(`Managed file is missing: ${item.relative}`);
70
+ else if (!matchesFingerprint(file, item)) errors.push(`Managed file was modified: ${item.relative}`);
71
+ else verifiedManaged++;
72
+ }
73
+ }
74
+ for (const item of overrides.values()) {
75
+ if ((state?.files ?? []).some((managed) => managed.relative === item.path)) continue;
76
+ const file = safePath(item.path);
77
+ if (file) validateOverride(item, file);
78
+ }
79
+
80
+ const claudePath = safePath("CLAUDE.md");
81
+ if (claudePath && (!fs.existsSync(claudePath) || !fs.readFileSync(claudePath, "utf8").includes("@.claude/nccgs/constitution.md"))) {
82
+ errors.push("CLAUDE.md does not import the NCCGS constitution.");
83
+ }
84
+
85
+ const policyPath = safePath(".nccgs/project.yaml");
86
+ let policySchema = null;
87
+ if (policyPath && !fs.existsSync(policyPath)) errors.push(".nccgs/project.yaml is missing.");
88
+ else if (policyPath) {
89
+ const policyText = fs.readFileSync(policyPath, "utf8");
90
+ const match = policyText.match(/^schema_version:\s*(\d+)\s*$/m);
91
+ policySchema = match ? Number(match[1]) : null;
92
+ if (policySchema !== 2) migrationIssue(`Project policy schema must be 2; found ${policySchema ?? "unknown"}.`);
93
+ if (/^profile_contracts:/m.test(policyText)) migrationIssue("Legacy embedded profile_contracts require Astra migration to pipeline.json plus project gate mapping.");
94
+ }
95
+
96
+ validateSettings();
97
+ let execution = null;
98
+ try {
99
+ execution = resolveExecution(project);
100
+ if (!fs.existsSync(containedPath(project, ".nccgs/execution.json"))) warnings.push("No execution.json: compatibility obligations remain active. Standalone requires explicit migrate-execution; updating does not waive pending external review.");
101
+ if (!execution.requireObservedEffort) warnings.push("Execution permits unknown observed effort; configured effort is intent, not runtime proof. Report unavailable provider model/effort evidence as unknown.");
102
+ } catch (error) { errors.push(`Invalid execution policy: ${error.message}`); }
103
+ try { inputPolicy(project); }
104
+ catch (error) { errors.push(`Invalid canonical input/output policy: ${error.message}`); }
105
+
106
+ const activationPath = safePath(".nccgs/agent-activation.json");
107
+ if (activationPath && fs.existsSync(activationPath)) {
108
+ try {
109
+ const activation = json(activationPath);
110
+ for (const [relative, expected] of Object.entries(activation.generated)) {
111
+ const bytes = fs.readFileSync(containedPath(project, relative));
112
+ if (hash(bytes) !== expected && hash(bytes.toString("utf8").replace(/\r\n/g, "\n")) !== activation.generatedCanonical?.[relative]) errors.push(`Activated agent modified: ${relative}`);
113
+ }
114
+ for (const warning of activation.warnings ?? []) migrationIssue(warning);
115
+ } catch (error) { errors.push(`Invalid agent activation state: ${error.message}`); }
116
+ }
117
+ const tasksDir = safePath(".nccgs/tasks");
118
+ if (tasksDir && fs.existsSync(tasksDir)) for (const file of fs.readdirSync(tasksDir).filter(f => f.endsWith(".json"))) {
119
+ try {
120
+ const task = json(containedPath(project, `.nccgs/tasks/${file}`));
121
+ if (task.schemaVersion !== 2 && execution?.name === "standalone") warnings.push(`Legacy task ${task.id} retains compatibility stages and budget; explicit continuation/re-pin is required to adopt standalone policy.`);
122
+ const executionCurrent = task.schemaVersion !== 2 || hash(JSON.stringify(execution)) === task.executionHash;
123
+ if (!executionCurrent) warnings.push(`Task ${task.id} is pinned to another execution policy; preserve its history and explicitly continue/re-pin before claiming completion.`);
124
+ if (!task.canonicalInputs) warnings.push(`Task ${task.id} predates canonical input pins; it is historical only until refresh-inputs and fresh evidence are recorded.`);
125
+ if (task.closed) {
126
+ const required = task.schemaVersion === 2 ? [task.mode === "implement" ? "implementation" : "analysis", "review", "report"] : stages;
127
+ const terminal = task.schemaVersion === 2 ? task.stages?.report : task.stages?.acceptance;
128
+ if (!required.every(s => task.stages?.[s]) || task.closed.snapshot !== terminal?.snapshot) throw new Error("Closure lacks its required stage records");
129
+ if (task.schemaVersion === 2 && (task.execution?.acceptanceRequired || task.contract?.acceptanceRequired) && !task.stages?.acceptance) throw new Error("Closure lacks required acceptance");
130
+ verifyStoredEvidence(task);
131
+ if (executionCurrent && task.canonicalInputs && task.closed.snapshot === snapshot(project, task).sha256) checkTask(project, task, "done");
132
+ else warnings.push(`Historical closure ${task.id} is for a different snapshot; it cannot approve current work.`);
133
+ }
134
+ } catch (error) { errors.push(`Invalid closed task ${file}: ${error.message}`); }
135
+ }
136
+ if (errors.length) {
137
+ for (const error of errors) console.error(`ERROR: ${error}`);
138
+ for (const warning of warnings) console.warn(`WARN: ${warning}`);
139
+ console.error(`NCCGS doctor failed with ${errors.length} error(s).`);
140
+ process.exit(1);
141
+ }
142
+
143
+ for (const warning of warnings) console.warn(`WARN: ${warning}`);
144
+ console.log("NCCGS doctor passed.");
145
+ console.log(`Framework version: ${state.frameworkVersion}`);
146
+ console.log(`Model profile: ${state.modelProfile}`);
147
+ console.log(`Execution policy: ${execution?.name}; external review: ${execution?.externalReview.mode}; maximum parallel workers: ${execution?.maxParallelWorkers}`);
148
+ console.log(`Reviewed design gate: ${execution?.name === "standalone" && execution?.designReviewRequired === true ? "required for feature implementation" : "legacy/compatibility policy; not proof of document readiness"}`);
149
+ console.log(`Managed files verified: ${verifiedManaged}`);
150
+ console.log(`Framework overrides: ${overrides.size} (${[...overrides.values()].filter((item) => item.status !== "approved").length} pending review)`);
151
+ console.log(`Project policy schema: ${policySchema ?? "migration required"}`);
152
+ console.log("Settings scope: project settings and observable local overrides only; CLI, user/managed settings, provider routing and active-session model/effort still require runtime verification.");
153
+
154
+ function validateOverride(item, file) {
155
+ if (item.status !== "approved") migrationIssue(`Framework override awaits /migrate-project review: ${item.path}`);
156
+ if (item.disposition === "absent") {
157
+ if (fs.existsSync(file)) errors.push(`Override expects a missing file, but it exists: ${item.path}`);
158
+ return;
159
+ }
160
+ if (!fs.existsSync(file)) errors.push(`Preserved framework override is missing: ${item.path}`);
161
+ else if (!matchesFingerprint(file, item)) errors.push(`Preserved framework override changed after it was recorded: ${item.path}`);
162
+ }
163
+
164
+ function migrationIssue(message) {
165
+ if (allowMigrationRequired) warnings.push(message);
166
+ else errors.push(message);
167
+ }
168
+
169
+ function readJson(file, label, required) {
170
+ if (!file) return null;
171
+ if (!fs.existsSync(file)) {
172
+ if (required) errors.push(`${label} is missing.`);
173
+ return null;
174
+ }
175
+ try { return JSON.parse(fs.readFileSync(file, "utf8")); }
176
+ catch (error) {
177
+ errors.push(`${label} is invalid JSON: ${error.message}`);
178
+ return null;
179
+ }
180
+ }
181
+
182
+ function isManagedRelative(value) {
183
+ try { return assertRelativePath(value).startsWith(".claude/"); }
184
+ catch { return false; }
185
+ }
186
+
187
+ function safePath(relative) {
188
+ try { return containedPath(project, relative); }
189
+ catch (error) { errors.push(`Unsafe NCCGS path: ${error.message}`); return null; }
190
+ }
191
+
192
+ function verifyStoredEvidence(task) {
193
+ const verify = (ref, runtime = false) => {
194
+ const file = verifyArtifact(project, ref);
195
+ if (!ref.path.endsWith(".json")) return;
196
+ const record = json(file);
197
+ if (runtime) {
198
+ verifyArtifact(project, record.observationSource);
199
+ if (record.evidenceMigration?.archivedObservation) verifyArtifact(project, record.evidenceMigration.archivedObservation);
200
+ } else if (record.origin === "verification-command") verifyArtifact(project, record.log);
201
+ };
202
+ for (const stage of Object.values(task.stages ?? {})) {
203
+ if (stage.runtime) verify(stage.runtime, true);
204
+ if (stage.evidence) verify(stage.evidence);
205
+ for (const check of stage.checks ?? []) verify(check.evidence);
206
+ for (const artifact of stage.artifacts ?? []) verify(artifact);
207
+ }
208
+ for (const receipt of task.externalReviews ?? []) verify(receipt.evidence);
209
+ }
210
+
211
+ function validateSettings() {
212
+ const settings = readJson(safePath(".claude/settings.json"), "Claude project settings", true);
213
+ const fragment = readJson(safePath(".claude/nccgs/settings.fragment.json"), "NCCGS settings fragment", true);
214
+ const local = readJson(safePath(".claude/settings.local.json"), "Claude local settings", false);
215
+ const object = value => value && typeof value === "object" && !Array.isArray(value);
216
+ if (!object(settings) || !object(fragment)) {
217
+ errors.push("Claude project settings and NCCGS settings fragment must be JSON objects.");
218
+ return;
219
+ }
220
+ if (local !== null && !object(local)) errors.push("Claude local settings must be a JSON object.");
221
+ for (const [label, value] of [["project", settings], ["local", local]]) {
222
+ if (value?.disableAllHooks === true) errors.push(`Claude ${label} settings disable all hooks.`);
223
+ }
224
+ for (const [event, groups] of Object.entries(fragment.hooks ?? {})) {
225
+ for (const expected of groups) for (const hook of expected.hooks ?? []) {
226
+ const actualGroups = Array.isArray(settings.hooks?.[event]) ? settings.hooks[event] : [];
227
+ const present = actualGroups.some(group => group && (group.matcher ?? "") === (expected.matcher ?? "")
228
+ && Array.isArray(group.hooks) && group.hooks.some(actual => requiredHookPresent(actual, hook)));
229
+ if (!present) errors.push(`Required NCCGS hook missing or altered: ${event} [${expected.matcher ?? ""}] ${hook.command}`);
230
+ }
231
+ }
232
+ let autonomy = "guarded";
233
+ try { autonomy = autonomyPolicy(project).mode; }
234
+ catch (error) { errors.push(error.message); }
235
+ for (const [key, required] of Object.entries(fragment.permissions ?? {})) {
236
+ for (const rule of required) if (!Array.isArray(settings.permissions?.[key]) || !settings.permissions[key].includes(rule)) {
237
+ errors.push(`Required NCCGS permission ${key} rule missing: ${rule}`);
238
+ }
239
+ }
240
+ if (autonomy === "full") {
241
+ console.log("Task autonomy: full (native permission mode requires nccgs run; configuration is not runtime proof).");
242
+ console.log("NCCGS integrity/destructive-action guards, explicit deny/ask rules and managed/OS restrictions remain effective.");
243
+ }
244
+ try {
245
+ if (!safePath(".claude/nccgs/pipeline.json")) return;
246
+ const { name, profile } = profilePolicy(project, state?.modelProfile);
247
+ const declared = policyPath && fs.existsSync(policyPath)
248
+ ? fs.readFileSync(policyPath, "utf8").match(/^models:[ \t]*\r?\n((?:[ \t]+[^\n]*\n?|\s*\n)*)/m)?.[1]?.match(/^\s+profile:\s*([^#\r\n]+)/m)?.[1]?.trim().replace(/^['"]|['"]$/g, "") : null;
249
+ if (declared && declared !== name) errors.push(`Project model profile ${declared} differs from installed profile ${name}; reconfigure models.`);
250
+ if (profile.session) {
251
+ const route = profile.models[profile.session.class];
252
+ if (settings.model !== route.model) errors.push(`Project session model must be ${route.model}; found ${settings.model ?? "missing"}.`);
253
+ if (settings.effortLevel !== route.effort) errors.push(`Project session effort must be ${route.effort}; found ${settings.effortLevel ?? "missing"}.`);
254
+ if (settings.modelSettings?.[route.model]?.effortLevel !== route.effort) errors.push(`Project per-model effort for ${route.model} must be ${route.effort}.`);
255
+ if (local?.model !== undefined && local.model !== route.model) errors.push(`Local settings override the required session model ${route.model}.`);
256
+ if (local?.effortLevel !== undefined && local.effortLevel !== route.effort) errors.push(`Local settings override the required session effort ${route.effort}.`);
257
+ if (local?.modelSettings?.[route.model]?.effortLevel !== undefined && local.modelSettings[route.model].effortLevel !== route.effort) errors.push(`Local settings override the required per-model effort for ${route.model}.`);
258
+ }
259
+ } catch (error) { errors.push(`Cannot verify session model policy: ${error.message}`); }
260
+ if (local) warnings.push("Local Claude settings are present; project requirements were checked without treating local hook additions as replacements. Runtime overrides are not fully observable by doctor.");
261
+ }