nccgs 1.1.0 → 1.3.0
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 +7 -1
- package/.claude/agents/nccgs-engine-programmer.md +2 -1
- package/.claude/agents/nccgs-gameplay-programmer.md +2 -1
- package/.claude/agents/nccgs-qa-engineer.md +2 -1
- package/.claude/agents/nccgs-ui-programmer.md +2 -1
- package/.claude/agents/nccgs-unity-build-specialist.md +2 -1
- package/.claude/agents/nccgs-unity-implementer.md +2 -1
- package/.claude/agents/nccgs-unity-systems-specialist.md +2 -1
- package/.claude/agents/nccgs-unity-ui-specialist.md +2 -1
- package/.claude/agents/nccgs-verification-engineer.md +2 -1
- package/.claude/nccgs/VERSION +1 -1
- package/.claude/{agents → nccgs/agent-library}/nccgs-accessibility-specialist.md +0 -1
- package/.claude/nccgs/agent-library/nccgs-adversarial-reviewer.md +30 -0
- package/.claude/{agents → nccgs/agent-library}/nccgs-ai-programmer.md +0 -1
- package/.claude/{agents → nccgs/agent-library}/nccgs-analytics-engineer.md +0 -1
- package/.claude/{agents → nccgs/agent-library}/nccgs-art-direction-lead.md +0 -1
- package/.claude/{agents → nccgs/agent-library}/nccgs-audio-direction-lead.md +0 -1
- package/.claude/{agents → nccgs/agent-library}/nccgs-creative-director.md +0 -1
- package/.claude/{agents → nccgs/agent-library}/nccgs-documentation-manager.md +0 -1
- package/.claude/{agents → nccgs/agent-library}/nccgs-economy-designer.md +0 -1
- package/.claude/nccgs/agent-library/nccgs-engine-programmer.md +25 -0
- package/.claude/{agents → nccgs/agent-library}/nccgs-game-design-lead.md +0 -1
- package/.claude/{agents → nccgs/agent-library}/nccgs-game-designer.md +0 -1
- package/.claude/nccgs/agent-library/nccgs-gameplay-programmer.md +25 -0
- package/.claude/{agents → nccgs/agent-library}/nccgs-level-designer.md +0 -1
- package/.claude/{agents → nccgs/agent-library}/nccgs-live-ops-designer.md +0 -1
- package/.claude/{agents → nccgs/agent-library}/nccgs-localization-lead.md +0 -1
- package/.claude/{agents → nccgs/agent-library}/nccgs-narrative-lead.md +0 -1
- package/.claude/{agents → nccgs/agent-library}/nccgs-network-programmer.md +0 -1
- package/.claude/{agents → nccgs/agent-library}/nccgs-performance-analyst.md +0 -1
- package/.claude/{agents → nccgs/agent-library}/nccgs-producer.md +0 -1
- package/.claude/{agents → nccgs/agent-library}/nccgs-production-coordinator.md +0 -1
- package/.claude/{agents → nccgs/agent-library}/nccgs-programming-lead.md +0 -1
- package/.claude/{agents → nccgs/agent-library}/nccgs-prototyper.md +0 -1
- package/.claude/nccgs/agent-library/nccgs-qa-engineer.md +25 -0
- package/.claude/{agents → nccgs/agent-library}/nccgs-qa-lead.md +0 -1
- package/.claude/{agents → nccgs/agent-library}/nccgs-release-engineer.md +0 -1
- package/.claude/{agents → nccgs/agent-library}/nccgs-release-lead.md +0 -1
- package/.claude/{agents → nccgs/agent-library}/nccgs-security-engineer.md +0 -1
- package/.claude/{agents → nccgs/agent-library}/nccgs-sound-designer.md +0 -1
- package/.claude/{agents → nccgs/agent-library}/nccgs-systems-designer.md +0 -1
- package/.claude/{agents → nccgs/agent-library}/nccgs-technical-architect.md +0 -1
- package/.claude/{agents → nccgs/agent-library}/nccgs-technical-artist.md +0 -1
- package/.claude/{agents → nccgs/agent-library}/nccgs-technical-director.md +0 -1
- package/.claude/{agents → nccgs/agent-library}/nccgs-tools-programmer.md +0 -1
- package/.claude/nccgs/agent-library/nccgs-ui-programmer.md +25 -0
- package/.claude/nccgs/agent-library/nccgs-unity-build-specialist.md +25 -0
- package/.claude/{agents → nccgs/agent-library}/nccgs-unity-content-specialist.md +0 -1
- package/.claude/nccgs/agent-library/nccgs-unity-implementer.md +25 -0
- package/.claude/{agents → nccgs/agent-library}/nccgs-unity-rendering-specialist.md +0 -1
- package/.claude/nccgs/agent-library/nccgs-unity-systems-specialist.md +25 -0
- package/.claude/nccgs/agent-library/nccgs-unity-ui-specialist.md +25 -0
- package/.claude/{agents → nccgs/agent-library}/nccgs-ux-designer.md +0 -1
- package/.claude/nccgs/agent-library/nccgs-verification-engineer.md +25 -0
- package/.claude/{agents → nccgs/agent-library}/nccgs-world-builder.md +0 -1
- package/.claude/{agents → nccgs/agent-library}/nccgs-writer.md +0 -1
- package/.claude/nccgs/constitution.md +13 -0
- package/.claude/nccgs/hooks/agent-audit.mjs +6 -0
- package/.claude/nccgs/hooks/closure-guard.mjs +34 -0
- package/.claude/nccgs/hooks/runtime-observation.mjs +11 -0
- package/.claude/nccgs/hooks/session-start.mjs +8 -1
- package/.claude/nccgs/pipeline.json +176 -0
- package/.claude/nccgs/protocols/agent-contract.md +5 -0
- package/.claude/nccgs/protocols/astra-claude.md +77 -0
- package/.claude/nccgs/protocols/evidence.md +6 -0
- package/.claude/nccgs/protocols/model-routing.md +27 -5
- package/.claude/nccgs/protocols/orchestration.md +5 -0
- package/.claude/nccgs/protocols/task-gates.md +66 -0
- package/.claude/nccgs/routing.generated.md +11 -0
- package/.claude/nccgs/settings.fragment.json +73 -9
- package/.claude/nccgs/studio.json +187 -96
- package/.claude/nccgs/tools/approve-overrides.mjs +81 -0
- package/.claude/nccgs/tools/compile-policy.mjs +83 -0
- package/.claude/nccgs/tools/configure-models.mjs +76 -31
- package/.claude/nccgs/tools/gates.mjs +167 -0
- package/.claude/nccgs/tools/integrity.mjs +69 -0
- package/.claude/nccgs/tools/policy.mjs +62 -0
- package/.claude/nccgs/tools/runtime.mjs +40 -0
- package/.claude/nccgs/tools/task.mjs +77 -0
- package/.claude/nccgs/workflow-catalog.json +91 -14
- package/.claude/skills/accessibility-review/SKILL.md +6 -5
- package/.claude/skills/accessibility-review/references/astra.md +17 -0
- package/.claude/skills/accessibility-review/references/legacy.md +7 -0
- package/.claude/skills/architecture-decision/SKILL.md +6 -5
- package/.claude/skills/architecture-decision/references/astra.md +12 -0
- package/.claude/skills/architecture-decision/references/legacy.md +7 -0
- package/.claude/skills/asset-audit/SKILL.md +6 -5
- package/.claude/skills/asset-audit/references/astra.md +17 -0
- package/.claude/skills/asset-audit/references/legacy.md +7 -0
- package/.claude/skills/audit/SKILL.md +6 -7
- package/.claude/skills/audit/references/astra.md +17 -0
- package/.claude/skills/audit/references/legacy.md +9 -0
- package/.claude/skills/balance-review/SKILL.md +6 -5
- package/.claude/skills/balance-review/references/astra.md +17 -0
- package/.claude/skills/balance-review/references/legacy.md +7 -0
- package/.claude/skills/bug-triage/SKILL.md +6 -5
- package/.claude/skills/bug-triage/references/astra.md +20 -0
- package/.claude/skills/bug-triage/references/legacy.md +7 -0
- package/.claude/skills/closure/SKILL.md +5 -4
- package/.claude/skills/closure/references/astra.md +15 -0
- package/.claude/skills/closure/references/legacy.md +7 -0
- package/.claude/skills/compatibility-review/SKILL.md +6 -5
- package/.claude/skills/compatibility-review/references/astra.md +17 -0
- package/.claude/skills/compatibility-review/references/legacy.md +7 -0
- package/.claude/skills/context-pack/SKILL.md +5 -4
- package/.claude/skills/context-pack/references/astra.md +12 -0
- package/.claude/skills/context-pack/references/legacy.md +7 -0
- package/.claude/skills/dependency-review/SKILL.md +6 -5
- package/.claude/skills/dependency-review/references/astra.md +17 -0
- package/.claude/skills/dependency-review/references/legacy.md +7 -0
- package/.claude/skills/design/SKILL.md +6 -11
- package/.claude/skills/design/references/astra.md +12 -0
- package/.claude/skills/design/references/legacy.md +13 -0
- package/.claude/skills/design-review/SKILL.md +6 -5
- package/.claude/skills/design-review/references/astra.md +17 -0
- package/.claude/skills/design-review/references/legacy.md +7 -0
- package/.claude/skills/evidence-review/SKILL.md +6 -5
- package/.claude/skills/evidence-review/references/astra.md +17 -0
- package/.claude/skills/evidence-review/references/legacy.md +7 -0
- package/.claude/skills/hotfix/SKILL.md +6 -5
- package/.claude/skills/hotfix/references/astra.md +23 -0
- package/.claude/skills/hotfix/references/legacy.md +7 -0
- package/.claude/skills/incident-recovery/SKILL.md +6 -5
- package/.claude/skills/incident-recovery/references/astra.md +20 -0
- package/.claude/skills/incident-recovery/references/legacy.md +7 -0
- package/.claude/skills/localize-game/SKILL.md +6 -5
- package/.claude/skills/localize-game/references/astra.md +20 -0
- package/.claude/skills/localize-game/references/legacy.md +7 -0
- package/.claude/skills/migrate-project/SKILL.md +6 -14
- package/.claude/skills/migrate-project/references/astra.md +23 -0
- package/.claude/skills/migrate-project/references/legacy.md +19 -0
- package/.claude/skills/migrate-project/references/procedure.md +21 -1
- package/.claude/skills/milestone-review/SKILL.md +6 -5
- package/.claude/skills/milestone-review/references/astra.md +12 -0
- package/.claude/skills/milestone-review/references/legacy.md +7 -0
- package/.claude/skills/nccgs-code-review/SKILL.md +15 -0
- package/.claude/skills/nccgs-code-review/references/astra.md +15 -0
- package/.claude/skills/{code-review/SKILL.md → nccgs-code-review/references/legacy.md} +0 -7
- package/.claude/skills/performance-audit/SKILL.md +6 -5
- package/.claude/skills/performance-audit/references/astra.md +17 -0
- package/.claude/skills/performance-audit/references/legacy.md +7 -0
- package/.claude/skills/plan-feature/SKILL.md +6 -5
- package/.claude/skills/plan-feature/references/astra.md +12 -0
- package/.claude/skills/plan-feature/references/legacy.md +7 -0
- package/.claude/skills/playtest/SKILL.md +5 -4
- package/.claude/skills/playtest/references/astra.md +20 -0
- package/.claude/skills/playtest/references/legacy.md +7 -0
- package/.claude/skills/project-stage/SKILL.md +5 -4
- package/.claude/skills/project-stage/references/astra.md +12 -0
- package/.claude/skills/project-stage/references/legacy.md +7 -0
- package/.claude/skills/prototype-feature/SKILL.md +6 -5
- package/.claude/skills/prototype-feature/references/astra.md +20 -0
- package/.claude/skills/prototype-feature/references/legacy.md +7 -0
- package/.claude/skills/qa-plan/SKILL.md +6 -5
- package/.claude/skills/qa-plan/references/astra.md +12 -0
- package/.claude/skills/qa-plan/references/legacy.md +7 -0
- package/.claude/skills/release/SKILL.md +6 -9
- package/.claude/skills/release/references/astra.md +23 -0
- package/.claude/skills/release/references/legacy.md +11 -0
- package/.claude/skills/release-readiness/SKILL.md +6 -5
- package/.claude/skills/release-readiness/references/astra.md +17 -0
- package/.claude/skills/release-readiness/references/legacy.md +7 -0
- package/.claude/skills/retrospective/SKILL.md +6 -5
- package/.claude/skills/retrospective/references/astra.md +12 -0
- package/.claude/skills/retrospective/references/legacy.md +7 -0
- package/.claude/skills/review/SKILL.md +6 -7
- package/.claude/skills/review/references/astra.md +15 -0
- package/.claude/skills/review/references/legacy.md +9 -0
- package/.claude/skills/security-audit/SKILL.md +6 -5
- package/.claude/skills/security-audit/references/astra.md +17 -0
- package/.claude/skills/security-audit/references/legacy.md +7 -0
- package/.claude/skills/sprint-plan/SKILL.md +6 -5
- package/.claude/skills/sprint-plan/references/astra.md +12 -0
- package/.claude/skills/sprint-plan/references/legacy.md +7 -0
- package/.claude/skills/status/SKILL.md +5 -6
- package/.claude/skills/status/references/astra.md +13 -0
- package/.claude/skills/status/references/legacy.md +9 -0
- package/.claude/skills/story-readiness/SKILL.md +5 -4
- package/.claude/skills/story-readiness/references/astra.md +17 -0
- package/.claude/skills/story-readiness/references/legacy.md +7 -0
- package/.claude/skills/test/SKILL.md +6 -7
- package/.claude/skills/test/references/astra.md +20 -0
- package/.claude/skills/test/references/legacy.md +9 -0
- package/.claude/skills/ui-review/SKILL.md +6 -5
- package/.claude/skills/ui-review/references/astra.md +17 -0
- package/.claude/skills/ui-review/references/legacy.md +7 -0
- package/.claude/skills/work/SKILL.md +6 -27
- package/.claude/skills/work/references/astra.md +20 -0
- package/.claude/skills/work/references/closure.md +10 -0
- package/.claude/skills/work/references/feature-contracts.md +4 -0
- package/.claude/skills/work/references/legacy.md +29 -0
- package/README.md +67 -29
- package/UPGRADING.md +54 -4
- package/VERSION +1 -1
- package/docs/ARCHITECTURE.md +30 -13
- package/docs/HUONG-DAN-MIGRATE-VA-SU-DUNG.md +34 -322
- package/docs/MIGRATION-MATRIX.md +1 -1
- package/docs/NCCGS-1.3.md +125 -0
- package/docs/PROJECT-POLICY.md +18 -3
- package/docs/WORKFLOWS.md +20 -14
- package/package-assets/nccgs-project.gitignore +2 -0
- package/package.json +5 -3
- package/scaffold/.nccgs/gates.json +7 -0
- package/scaffold/.nccgs/project.yaml +6 -3
- package/scaffold/.nccgs/state.md +1 -1
- package/scaffold/.nccgs/templates/agent-handoff.md +10 -0
- package/scaffold/.nccgs/templates/closure-record.md +11 -0
- package/scaffold/.nccgs/templates/feature-contract.md +9 -0
- package/scaffold/.nccgs/templates/implementation-report.md +49 -0
- package/scripts/cli.mjs +10 -5
- package/scripts/doctor.mjs +102 -18
- package/scripts/install.mjs +221 -88
- package/scripts/validate.mjs +34 -6
- package/tests/framework.test.mjs +213 -8
- package/tests/gates.test.mjs +170 -0
package/scripts/install.mjs
CHANGED
|
@@ -1,8 +1,13 @@
|
|
|
1
1
|
import { spawnSync } from "node:child_process";
|
|
2
|
-
import crypto from "node:crypto";
|
|
3
2
|
import fs from "node:fs";
|
|
4
3
|
import path from "node:path";
|
|
5
4
|
import { fileURLToPath } from "node:url";
|
|
5
|
+
import {
|
|
6
|
+
equivalentFiles,
|
|
7
|
+
fingerprintFile,
|
|
8
|
+
matchesFingerprint,
|
|
9
|
+
sameFingerprint
|
|
10
|
+
} from "../.claude/nccgs/tools/integrity.mjs";
|
|
6
11
|
|
|
7
12
|
const scriptDir = path.dirname(fileURLToPath(import.meta.url));
|
|
8
13
|
const root = path.resolve(scriptDir, "..");
|
|
@@ -52,92 +57,131 @@ if (validation.status !== 0) {
|
|
|
52
57
|
const frameworkSource = path.join(root, ".claude");
|
|
53
58
|
const frameworkFiles = walk(frameworkSource).map((file) => ({
|
|
54
59
|
source: file,
|
|
55
|
-
relative: path.relative(root, file),
|
|
60
|
+
relative: posix(path.relative(root, file)),
|
|
56
61
|
destination: path.join(project, path.relative(root, file)),
|
|
57
62
|
}));
|
|
58
63
|
const npmExcludedUnityFile = ".claude/skills/setup-vivox-voice-chat/evals/.gitignore";
|
|
59
|
-
if (!frameworkFiles.some((item) =>
|
|
64
|
+
if (!frameworkFiles.some((item) => item.relative === npmExcludedUnityFile)) {
|
|
60
65
|
const fallback = path.join(root, "package-assets", "setup-vivox-voice-chat-evals.gitignore");
|
|
61
66
|
if (fs.existsSync(fallback)) {
|
|
62
67
|
frameworkFiles.push({
|
|
63
68
|
source: fallback,
|
|
64
|
-
relative: npmExcludedUnityFile
|
|
69
|
+
relative: npmExcludedUnityFile,
|
|
65
70
|
destination: path.join(project, ...npmExcludedUnityFile.split("/"))
|
|
66
71
|
});
|
|
67
72
|
frameworkFiles.sort((a, b) => a.relative.localeCompare(b.relative));
|
|
68
73
|
}
|
|
69
74
|
}
|
|
75
|
+
|
|
70
76
|
const installStatePath = path.join(project, ".claude", "nccgs", "install-state.json");
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
catch (error) {
|
|
75
|
-
console.error(`Invalid NCCGS install state at ${installStatePath}: ${error.message}`);
|
|
76
|
-
process.exit(2);
|
|
77
|
-
}
|
|
78
|
-
}
|
|
77
|
+
const overrideManifestPath = path.join(project, ".nccgs", "framework-overrides.json");
|
|
78
|
+
const previousInstallState = readJsonIfPresent(installStatePath, "NCCGS install state");
|
|
79
|
+
const previousOverrideManifest = readJsonIfPresent(overrideManifestPath, "NCCGS framework override manifest");
|
|
79
80
|
if (updateOnly && !previousInstallState) {
|
|
80
81
|
console.error("NCCGS is not installed in this project. Run `npx nccgs@latest install` first.");
|
|
81
82
|
process.exit(2);
|
|
82
83
|
}
|
|
84
|
+
if (previousOverrideManifest && previousOverrideManifest.schemaVersion !== 1) {
|
|
85
|
+
console.error(`Unsupported framework override schema: ${previousOverrideManifest.schemaVersion ?? "unknown"}`);
|
|
86
|
+
process.exit(2);
|
|
87
|
+
}
|
|
88
|
+
for (const item of previousInstallState?.files ?? []) {
|
|
89
|
+
if (!isManagedRelative(item.relative)) {
|
|
90
|
+
console.error(`Unsafe managed path in install state: ${item.relative ?? "missing"}`);
|
|
91
|
+
process.exit(2);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
for (const item of previousOverrideManifest?.overrides ?? []) {
|
|
95
|
+
if (!isManagedRelative(item.path)) {
|
|
96
|
+
console.error(`Unsafe path in framework override manifest: ${item.path ?? "missing"}`);
|
|
97
|
+
process.exit(2);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
83
101
|
const previousFiles = new Map((previousInstallState?.files ?? []).map((item) => [item.relative, item]));
|
|
84
|
-
const
|
|
85
|
-
const
|
|
102
|
+
const previousOverrides = new Map((previousOverrideManifest?.overrides ?? []).map((item) => [item.path, item]));
|
|
103
|
+
const currentFrameworkNames = new Set(frameworkFiles.map((item) => item.relative));
|
|
104
|
+
const currentVersion = fs.readFileSync(path.join(root, "VERSION"), "utf8").trim();
|
|
105
|
+
|
|
106
|
+
const filePlans = frameworkFiles.map((item) => planFrameworkFile(item));
|
|
107
|
+
const stalePlans = (previousInstallState?.files ?? [])
|
|
86
108
|
.filter((item) => !currentFrameworkNames.has(item.relative))
|
|
87
|
-
.map((item) => (
|
|
109
|
+
.map((item) => planStaleFile(item))
|
|
110
|
+
.filter(Boolean);
|
|
111
|
+
|
|
112
|
+
const activeOverridePaths = new Set([
|
|
113
|
+
...filePlans.filter((item) => item.override).map((item) => item.relative),
|
|
114
|
+
...stalePlans.filter((item) => item.override).map((item) => item.relative)
|
|
115
|
+
]);
|
|
116
|
+
const carriedOverrides = [];
|
|
117
|
+
for (const item of previousOverrides.values()) {
|
|
118
|
+
if (activeOverridePaths.has(item.path) || currentFrameworkNames.has(item.path) || previousFiles.has(item.path)) continue;
|
|
119
|
+
const destination = path.join(project, ...item.path.split("/"));
|
|
120
|
+
const absent = item.disposition === "absent";
|
|
121
|
+
const stillMatches = absent ? !fs.existsSync(destination) : matchesFingerprint(destination, item);
|
|
122
|
+
carriedOverrides.push({
|
|
88
123
|
...item,
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
})
|
|
92
|
-
.filter((item) => fs.existsSync(item.destination));
|
|
93
|
-
|
|
94
|
-
const differentFiles = frameworkFiles.filter((item) => {
|
|
95
|
-
if (!fs.existsSync(item.destination)) return false;
|
|
96
|
-
return !sameFile(item.source, item.destination);
|
|
97
|
-
});
|
|
98
|
-
const automaticReplacements = differentFiles.filter((item) => {
|
|
99
|
-
const previous = previousFiles.get(posix(item.relative));
|
|
100
|
-
return previous && hashFile(item.destination) === previous.sha256;
|
|
101
|
-
});
|
|
102
|
-
const conflicts = differentFiles.filter((item) => !automaticReplacements.includes(item));
|
|
103
|
-
const automaticStale = staleFrameworkFiles.filter((item) => item.unmodified);
|
|
104
|
-
const staleConflicts = staleFrameworkFiles.filter((item) => !item.unmodified);
|
|
105
|
-
|
|
106
|
-
if ((conflicts.length || staleConflicts.length) && !force) {
|
|
107
|
-
console.error(`NCCGS found ${conflicts.length} modified and ${staleConflicts.length} modified-stale framework file(s):`);
|
|
108
|
-
for (const item of conflicts.slice(0, 30)) console.error(`- ${posix(item.relative)}`);
|
|
109
|
-
for (const item of staleConflicts.slice(0, 30)) console.error(`- stale: ${item.relative}`);
|
|
110
|
-
if (conflicts.length > 30) console.error(`- ... ${conflicts.length - 30} more`);
|
|
111
|
-
console.error("These files differ from the hashes recorded at the previous install.");
|
|
112
|
-
console.error("Review them, then re-run with --force to back up and replace intentional overrides.");
|
|
113
|
-
process.exit(2);
|
|
124
|
+
status: item.status === "approved" && stillMatches ? "approved" : "pending_review",
|
|
125
|
+
reason: stillMatches ? item.reason : "Previously approved override changed and requires migration review."
|
|
126
|
+
});
|
|
114
127
|
}
|
|
115
128
|
|
|
116
|
-
const
|
|
117
|
-
|
|
129
|
+
const overrideRecords = [
|
|
130
|
+
...filePlans.filter((item) => item.override).map((item) => item.override),
|
|
131
|
+
...stalePlans.filter((item) => item.override).map((item) => item.override),
|
|
132
|
+
...carriedOverrides
|
|
133
|
+
].sort((a, b) => a.path.localeCompare(b.path));
|
|
134
|
+
const pendingOverrides = overrideRecords.filter((item) => item.status !== "approved");
|
|
135
|
+
|
|
136
|
+
const newFiles = filePlans.filter((item) => item.action === "install");
|
|
137
|
+
const currentFiles = filePlans.filter((item) => item.action === "current");
|
|
138
|
+
const automaticReplacements = filePlans.filter((item) => item.action === "replace");
|
|
139
|
+
const forcedReplacements = filePlans.filter((item) => item.action === "force-replace");
|
|
140
|
+
const preservedFiles = filePlans.filter((item) => item.action.startsWith("preserve"));
|
|
141
|
+
const automaticStale = stalePlans.filter((item) => item.action === "remove");
|
|
142
|
+
const forcedStale = stalePlans.filter((item) => item.action === "force-remove");
|
|
143
|
+
const preservedStale = stalePlans.filter((item) => item.action === "preserve-stale");
|
|
144
|
+
|
|
118
145
|
const scaffoldFiles = walk(path.join(root, "scaffold")).map((file) => ({
|
|
119
146
|
source: file,
|
|
120
147
|
relative: path.relative(path.join(root, "scaffold"), file),
|
|
121
148
|
destination: path.join(project, path.relative(path.join(root, "scaffold"), file)),
|
|
122
149
|
}));
|
|
150
|
+
const npmExcludedScaffoldFile = ".nccgs/.gitignore";
|
|
151
|
+
if (!scaffoldFiles.some((item) => posix(item.relative) === npmExcludedScaffoldFile)) {
|
|
152
|
+
const fallback = path.join(root, "package-assets", "nccgs-project.gitignore");
|
|
153
|
+
if (fs.existsSync(fallback)) {
|
|
154
|
+
scaffoldFiles.push({
|
|
155
|
+
source: fallback,
|
|
156
|
+
relative: npmExcludedScaffoldFile.split("/").join(path.sep),
|
|
157
|
+
destination: path.join(project, ...npmExcludedScaffoldFile.split("/"))
|
|
158
|
+
});
|
|
159
|
+
scaffoldFiles.sort((a, b) => a.relative.localeCompare(b.relative));
|
|
160
|
+
}
|
|
161
|
+
}
|
|
123
162
|
const newScaffold = scaffoldFiles.filter((item) => !fs.existsSync(item.destination));
|
|
124
163
|
const settingsPlan = planManagedSettings(project);
|
|
125
164
|
const targetPolicyPath = path.join(project, ".nccgs", "project.yaml");
|
|
126
165
|
const targetPolicy = fs.existsSync(targetPolicyPath) ? fs.readFileSync(targetPolicyPath, "utf8") : "";
|
|
127
166
|
const policyModelProfile = targetPolicy.match(/^models:\s*$[\s\S]*?^\s+profile:\s*([^#\r\n]+)/m)?.[1]?.trim();
|
|
128
|
-
const
|
|
129
|
-
|
|
167
|
+
const studio = JSON.parse(fs.readFileSync(path.join(root, ".claude/nccgs/studio.json"), "utf8"));
|
|
168
|
+
const modelProfile = requestedModelProfile || policyModelProfile || studio.defaultProfile;
|
|
169
|
+
if (!Object.hasOwn(studio.modelProfiles, modelProfile)) {
|
|
130
170
|
console.error(`Unknown model profile: ${modelProfile}`);
|
|
131
171
|
process.exit(2);
|
|
132
172
|
}
|
|
133
173
|
|
|
134
174
|
console.log(`NCCGS installation target: ${project}`);
|
|
135
|
-
console.log(`Framework version: ${previousInstallState?.frameworkVersion ?? "not installed"} -> ${
|
|
136
|
-
console.log(`Framework files: ${frameworkFiles.length} (${newFiles.length} new, ${
|
|
137
|
-
console.log(`Stale framework files: ${automaticStale.length} automatic removal, ${
|
|
175
|
+
console.log(`Framework version: ${previousInstallState?.frameworkVersion ?? "not installed"} -> ${currentVersion}`);
|
|
176
|
+
console.log(`Framework files: ${frameworkFiles.length} (${newFiles.length} new, ${currentFiles.length} current, ${automaticReplacements.length} automatic update, ${preservedFiles.length} preserved override, ${forcedReplacements.length} forced override)`);
|
|
177
|
+
console.log(`Stale framework files: ${automaticStale.length} automatic removal, ${preservedStale.length} preserved override, ${forcedStale.length} forced removal`);
|
|
138
178
|
console.log(`Project scaffold files to initialize: ${newScaffold.length}`);
|
|
139
179
|
console.log(`Managed Claude settings: ${settingsPlan.changed ? "update" : "already current"}`);
|
|
140
180
|
console.log(`Agent model profile: ${modelProfile}`);
|
|
181
|
+
if (pendingOverrides.length) {
|
|
182
|
+
console.log(`${pendingOverrides.length} project override(s) will be preserved for /migrate-project review:`);
|
|
183
|
+
for (const item of pendingOverrides.slice(0, 30)) console.log(`- ${item.path} (${item.disposition})`);
|
|
184
|
+
}
|
|
141
185
|
if (dryRun) {
|
|
142
186
|
console.log("Dry run complete; no files changed.");
|
|
143
187
|
process.exit(0);
|
|
@@ -145,23 +189,21 @@ if (dryRun) {
|
|
|
145
189
|
|
|
146
190
|
const stamp = new Date().toISOString().replace(/[:.]/g, "-");
|
|
147
191
|
const backupRoot = path.join(project, ".nccgs", "backups", stamp);
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
for (const item of staleFrameworkFiles) {
|
|
154
|
-
const backup = path.join(backupRoot, ...item.relative.split("/"));
|
|
155
|
-
fs.mkdirSync(path.dirname(backup), { recursive: true });
|
|
156
|
-
fs.copyFileSync(item.destination, backup);
|
|
192
|
+
const replacedFiles = [...automaticReplacements, ...forcedReplacements];
|
|
193
|
+
const removedFiles = [...automaticStale, ...forcedStale];
|
|
194
|
+
for (const item of replacedFiles) backupFile(item.destination, path.join(backupRoot, ...item.relative.split("/")));
|
|
195
|
+
for (const item of removedFiles) {
|
|
196
|
+
backupFile(item.destination, path.join(backupRoot, ...item.relative.split("/")));
|
|
157
197
|
fs.unlinkSync(item.destination);
|
|
158
198
|
}
|
|
199
|
+
if (previousOverrideManifest && JSON.stringify(previousOverrideManifest.overrides ?? []) !== JSON.stringify(overrideRecords)) {
|
|
200
|
+
backupFile(overrideManifestPath, path.join(backupRoot, ".nccgs", "framework-overrides.json"));
|
|
201
|
+
}
|
|
159
202
|
|
|
160
|
-
for (const item of
|
|
203
|
+
for (const item of filePlans.filter((entry) => new Set(["install", "replace", "force-replace"]).has(entry.action))) {
|
|
161
204
|
fs.mkdirSync(path.dirname(item.destination), { recursive: true });
|
|
162
205
|
fs.copyFileSync(item.source, item.destination);
|
|
163
206
|
}
|
|
164
|
-
|
|
165
207
|
for (const item of newScaffold) {
|
|
166
208
|
fs.mkdirSync(path.dirname(item.destination), { recursive: true });
|
|
167
209
|
fs.copyFileSync(item.source, item.destination);
|
|
@@ -170,10 +212,14 @@ for (const item of newScaffold) {
|
|
|
170
212
|
installManagedSettings(settingsPlan, backupRoot);
|
|
171
213
|
installClaudeImport(project, backupRoot);
|
|
172
214
|
|
|
215
|
+
const skipAgentPaths = preservedFiles
|
|
216
|
+
.filter((item) => item.relative.startsWith(".claude/agents/"))
|
|
217
|
+
.flatMap((item) => ["--skip-path", item.relative]);
|
|
173
218
|
const modelConfiguration = spawnSync(process.execPath, [
|
|
174
|
-
path.join(
|
|
219
|
+
path.join(root, ".claude", "nccgs", "tools", "configure-models.mjs"),
|
|
175
220
|
"--project", project,
|
|
176
221
|
"--profile", modelProfile,
|
|
222
|
+
...skipAgentPaths
|
|
177
223
|
], { cwd: project, encoding: "utf8" });
|
|
178
224
|
if (modelConfiguration.status !== 0) {
|
|
179
225
|
process.stderr.write(modelConfiguration.stdout ?? "");
|
|
@@ -182,12 +228,13 @@ if (modelConfiguration.status !== 0) {
|
|
|
182
228
|
process.exit(1);
|
|
183
229
|
}
|
|
184
230
|
|
|
185
|
-
|
|
231
|
+
writeOverrideManifest(overrideManifestPath, overrideRecords, previousOverrideManifest, currentVersion);
|
|
232
|
+
writeInstallState(project, modelProfile, frameworkFiles, installStatePath, previousInstallState, new Set(overrideRecords.map((item) => item.path)));
|
|
186
233
|
|
|
187
234
|
const doctor = spawnSync(process.execPath, [
|
|
188
235
|
path.join(scriptDir, "doctor.mjs"),
|
|
189
236
|
"--project", project,
|
|
190
|
-
"--allow-
|
|
237
|
+
"--allow-migration-required"
|
|
191
238
|
], { cwd: project, encoding: "utf8" });
|
|
192
239
|
process.stdout.write(doctor.stdout ?? "");
|
|
193
240
|
process.stderr.write(doctor.stderr ?? "");
|
|
@@ -197,10 +244,12 @@ if (doctor.status !== 0) {
|
|
|
197
244
|
}
|
|
198
245
|
|
|
199
246
|
console.log("NCCGS installation complete.");
|
|
200
|
-
if (
|
|
247
|
+
if (fs.existsSync(backupRoot)) {
|
|
248
|
+
console.log(`Replaced shared files were backed up under: ${backupRoot}`);
|
|
249
|
+
}
|
|
201
250
|
const installedPolicy = fs.readFileSync(targetPolicyPath, "utf8");
|
|
202
251
|
const installedPolicySchema = Number(installedPolicy.match(/^schema_version:\s*(\d+)\s*$/m)?.[1] ?? 0);
|
|
203
|
-
if (installedPolicySchema !== 2) {
|
|
252
|
+
if (installedPolicySchema !== 2 || pendingOverrides.length || /^profile_contracts:/m.test(installedPolicy)) {
|
|
204
253
|
console.log("Project migration required: open a new Claude Code session and run `/migrate-project` once.");
|
|
205
254
|
} else if (!previousInstallState) {
|
|
206
255
|
console.log("First NCCGS adoption: run `/migrate-project` once for an existing project; a new project can start with `/status`.");
|
|
@@ -208,7 +257,75 @@ if (installedPolicySchema !== 2) {
|
|
|
208
257
|
console.log("No project migration required for this NCCGS update.");
|
|
209
258
|
}
|
|
210
259
|
|
|
260
|
+
function planFrameworkFile(item) {
|
|
261
|
+
const previous = previousFiles.get(item.relative);
|
|
262
|
+
const existingOverride = previousOverrides.get(item.relative);
|
|
263
|
+
const upstream = fingerprintFile(item.source);
|
|
264
|
+
if (!fs.existsSync(item.destination)) {
|
|
265
|
+
if (!force && (previous || existingOverride?.disposition === "absent")) {
|
|
266
|
+
return { ...item, action: "preserve-absent", override: buildOverride(item, existingOverride, upstream, "absent") };
|
|
267
|
+
}
|
|
268
|
+
return { ...item, action: "install", override: null };
|
|
269
|
+
}
|
|
270
|
+
if (equivalentFiles(item.source, item.destination)) return { ...item, action: "current", override: null };
|
|
271
|
+
if (!force && previous && matchesFingerprint(item.destination, previous)) return { ...item, action: "replace", override: null };
|
|
272
|
+
if (force) return { ...item, action: "force-replace", override: null };
|
|
273
|
+
return { ...item, action: "preserve", override: buildOverride(item, existingOverride, upstream, "preserve") };
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
function planStaleFile(previous) {
|
|
277
|
+
const destination = path.join(project, ...previous.relative.split("/"));
|
|
278
|
+
if (!fs.existsSync(destination)) return null;
|
|
279
|
+
if (force) return { ...previous, relative: previous.relative, destination, action: "force-remove", override: null };
|
|
280
|
+
if (matchesFingerprint(destination, previous)) return { ...previous, relative: previous.relative, destination, action: "remove", override: null };
|
|
281
|
+
const existingOverride = previousOverrides.get(previous.relative);
|
|
282
|
+
const current = fingerprintFile(destination);
|
|
283
|
+
const stillApproved = existingOverride?.status === "approved" && matchesFingerprint(destination, existingOverride)
|
|
284
|
+
&& existingOverride.upstreamSha256 === null;
|
|
285
|
+
return {
|
|
286
|
+
...previous,
|
|
287
|
+
relative: previous.relative,
|
|
288
|
+
destination,
|
|
289
|
+
action: "preserve-stale",
|
|
290
|
+
override: {
|
|
291
|
+
path: previous.relative,
|
|
292
|
+
disposition: "preserve",
|
|
293
|
+
status: stillApproved ? "approved" : "pending_review",
|
|
294
|
+
...current,
|
|
295
|
+
upstreamSha256: null,
|
|
296
|
+
upstreamCanonicalSha256: null,
|
|
297
|
+
retiredFromFramework: true,
|
|
298
|
+
reason: "Locally modified file was preserved after its framework path retired; review ownership during migration."
|
|
299
|
+
}
|
|
300
|
+
};
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
function buildOverride(item, existing, upstream, disposition) {
|
|
304
|
+
const current = disposition === "absent" ? { sha256: null, canonicalSha256: null } : fingerprintFile(item.destination);
|
|
305
|
+
const currentMatches = disposition === "absent"
|
|
306
|
+
? existing?.disposition === "absent" && !fs.existsSync(item.destination)
|
|
307
|
+
: existing?.disposition === "preserve" && sameFingerprint(current, existing);
|
|
308
|
+
const previousUpstream = existing ? {
|
|
309
|
+
sha256: existing.upstreamSha256,
|
|
310
|
+
canonicalSha256: existing.upstreamCanonicalSha256
|
|
311
|
+
} : null;
|
|
312
|
+
const upstreamMatches = previousUpstream && sameFingerprint(upstream, previousUpstream);
|
|
313
|
+
const approved = existing?.status === "approved" && currentMatches && upstreamMatches;
|
|
314
|
+
return {
|
|
315
|
+
path: item.relative,
|
|
316
|
+
disposition,
|
|
317
|
+
status: approved ? "approved" : "pending_review",
|
|
318
|
+
...current,
|
|
319
|
+
upstreamSha256: upstream.sha256,
|
|
320
|
+
upstreamCanonicalSha256: upstream.canonicalSha256 ?? null,
|
|
321
|
+
reason: approved
|
|
322
|
+
? existing.reason
|
|
323
|
+
: "Project-specific framework change was preserved automatically; review and rebase it with /migrate-project."
|
|
324
|
+
};
|
|
325
|
+
}
|
|
326
|
+
|
|
211
327
|
function walk(directory) {
|
|
328
|
+
if (!fs.existsSync(directory)) return [];
|
|
212
329
|
const output = [];
|
|
213
330
|
for (const entry of fs.readdirSync(directory, { withFileTypes: true })) {
|
|
214
331
|
const full = path.join(directory, entry.name);
|
|
@@ -218,27 +335,36 @@ function walk(directory) {
|
|
|
218
335
|
return output.sort((a, b) => a.localeCompare(b));
|
|
219
336
|
}
|
|
220
337
|
|
|
221
|
-
function
|
|
222
|
-
|
|
223
|
-
const b = fs.readFileSync(right);
|
|
224
|
-
return a.length === b.length && a.equals(b);
|
|
338
|
+
function posix(value) {
|
|
339
|
+
return value.split(path.sep).join("/");
|
|
225
340
|
}
|
|
226
341
|
|
|
227
|
-
function
|
|
228
|
-
|
|
342
|
+
function isManagedRelative(value) {
|
|
343
|
+
if (typeof value !== "string" || !value.startsWith(".claude/")) return false;
|
|
344
|
+
const parts = value.split("/");
|
|
345
|
+
return !parts.includes("") && !parts.includes(".") && !parts.includes("..");
|
|
229
346
|
}
|
|
230
347
|
|
|
231
|
-
function
|
|
232
|
-
|
|
348
|
+
function readJsonIfPresent(file, label) {
|
|
349
|
+
if (!fs.existsSync(file)) return null;
|
|
350
|
+
try { return JSON.parse(fs.readFileSync(file, "utf8")); }
|
|
351
|
+
catch (error) {
|
|
352
|
+
console.error(`Invalid ${label} at ${file}: ${error.message}`);
|
|
353
|
+
process.exit(2);
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
function backupFile(source, destination) {
|
|
358
|
+
fs.mkdirSync(path.dirname(destination), { recursive: true });
|
|
359
|
+
fs.copyFileSync(source, destination);
|
|
233
360
|
}
|
|
234
361
|
|
|
235
362
|
function planManagedSettings(projectRoot) {
|
|
236
363
|
const settingsPath = path.join(projectRoot, ".claude", "settings.json");
|
|
237
364
|
let current = {};
|
|
238
365
|
if (fs.existsSync(settingsPath)) {
|
|
239
|
-
try {
|
|
240
|
-
|
|
241
|
-
} catch (error) {
|
|
366
|
+
try { current = JSON.parse(fs.readFileSync(settingsPath, "utf8")); }
|
|
367
|
+
catch (error) {
|
|
242
368
|
console.error(`Cannot merge invalid JSON settings at ${settingsPath}: ${error.message}`);
|
|
243
369
|
process.exit(2);
|
|
244
370
|
}
|
|
@@ -264,27 +390,37 @@ function planManagedSettings(projectRoot) {
|
|
|
264
390
|
|
|
265
391
|
function installManagedSettings(plan, backupDirectory) {
|
|
266
392
|
if (!plan.changed) return;
|
|
267
|
-
if (plan.existed)
|
|
268
|
-
const backup = path.join(backupDirectory, ".claude", "settings.json");
|
|
269
|
-
fs.mkdirSync(path.dirname(backup), { recursive: true });
|
|
270
|
-
fs.copyFileSync(plan.settingsPath, backup);
|
|
271
|
-
}
|
|
393
|
+
if (plan.existed) backupFile(plan.settingsPath, path.join(backupDirectory, ".claude", "settings.json"));
|
|
272
394
|
fs.mkdirSync(path.dirname(plan.settingsPath), { recursive: true });
|
|
273
395
|
fs.writeFileSync(plan.settingsPath, plan.nextText, "utf8");
|
|
274
396
|
}
|
|
275
397
|
|
|
276
|
-
function
|
|
398
|
+
function writeOverrideManifest(file, overrides, previous, frameworkVersion) {
|
|
399
|
+
if (!overrides.length && !previous) return;
|
|
400
|
+
const now = new Date().toISOString();
|
|
401
|
+
const manifest = {
|
|
402
|
+
schemaVersion: 1,
|
|
403
|
+
frameworkVersion,
|
|
404
|
+
createdAt: previous?.createdAt ?? now,
|
|
405
|
+
updatedAt: now,
|
|
406
|
+
overrides
|
|
407
|
+
};
|
|
408
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
409
|
+
fs.writeFileSync(file, `${JSON.stringify(manifest, null, 2)}\n`, "utf8");
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
function writeInstallState(projectRoot, profile, files, statePath, previousState, overridePaths) {
|
|
277
413
|
const now = new Date().toISOString();
|
|
278
414
|
const state = {
|
|
279
|
-
schemaVersion:
|
|
280
|
-
frameworkVersion:
|
|
415
|
+
schemaVersion: 3,
|
|
416
|
+
frameworkVersion: currentVersion,
|
|
281
417
|
previousFrameworkVersion: previousState?.frameworkVersion ?? null,
|
|
282
418
|
modelProfile: profile,
|
|
283
419
|
installedAt: previousState?.installedAt ?? now,
|
|
284
420
|
updatedAt: now,
|
|
285
421
|
files: files.map((item) => ({
|
|
286
|
-
relative:
|
|
287
|
-
|
|
422
|
+
relative: item.relative,
|
|
423
|
+
...(overridePaths.has(item.relative) ? fingerprintFile(item.source) : fingerprintFile(path.join(projectRoot, ...item.relative.split("/"))))
|
|
288
424
|
}))
|
|
289
425
|
};
|
|
290
426
|
fs.mkdirSync(path.dirname(statePath), { recursive: true });
|
|
@@ -299,12 +435,9 @@ function installClaudeImport(projectRoot, backupDirectory) {
|
|
|
299
435
|
fs.copyFileSync(path.join(root, "CLAUDE.md"), claudePath);
|
|
300
436
|
return;
|
|
301
437
|
}
|
|
302
|
-
|
|
303
438
|
const current = fs.readFileSync(claudePath, "utf8");
|
|
304
439
|
if (current.includes(begin) || current.includes("@.claude/nccgs/constitution.md")) return;
|
|
305
|
-
|
|
306
|
-
fs.mkdirSync(backupDirectory, { recursive: true });
|
|
307
|
-
fs.copyFileSync(claudePath, path.join(backupDirectory, "CLAUDE.md"));
|
|
440
|
+
backupFile(claudePath, path.join(backupDirectory, "CLAUDE.md"));
|
|
308
441
|
const separator = current.endsWith("\n") ? "\n" : "\n\n";
|
|
309
442
|
fs.writeFileSync(claudePath, `${current}${separator}${block}\n`, "utf8");
|
|
310
443
|
}
|
package/scripts/validate.mjs
CHANGED
|
@@ -3,6 +3,7 @@ import fs from "node:fs";
|
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import { spawnSync } from "node:child_process";
|
|
5
5
|
import { fileURLToPath } from "node:url";
|
|
6
|
+
import { compile } from "../.claude/nccgs/tools/compile-policy.mjs";
|
|
6
7
|
|
|
7
8
|
const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
|
8
9
|
const errors = [];
|
|
@@ -44,17 +45,29 @@ for (const relative of [
|
|
|
44
45
|
"VERSION", "README.md", "CLAUDE.md", "LICENSE", "package.json",
|
|
45
46
|
".claude/nccgs/VERSION", ".claude/nccgs/constitution.md",
|
|
46
47
|
".claude/nccgs/studio.json", ".claude/nccgs/workflow-catalog.json",
|
|
48
|
+
".claude/nccgs/pipeline.json", ".claude/nccgs/protocols/task-gates.md",
|
|
49
|
+
".claude/nccgs/tools/task.mjs", ".claude/nccgs/tools/gates.mjs", ".claude/nccgs/tools/runtime.mjs",
|
|
50
|
+
"scaffold/.nccgs/gates.json", "tests/gates.test.mjs",
|
|
47
51
|
".claude/nccgs/settings.fragment.json", ".claude/nccgs/unity-skills-manifest.json",
|
|
48
52
|
".claude/nccgs/protocols/agent-contract.md", ".claude/nccgs/protocols/orchestration.md",
|
|
49
53
|
".claude/nccgs/protocols/model-routing.md", ".claude/nccgs/protocols/context-packets.md",
|
|
54
|
+
".claude/nccgs/protocols/astra-claude.md", "scaffold/.nccgs/templates/implementation-report.md",
|
|
50
55
|
".claude/nccgs/protocols/evidence.md", ".claude/nccgs/protocols/unity-boundary.md",
|
|
51
56
|
"scaffold/.nccgs/project.yaml", "scaffold/.nccgs/state.md",
|
|
52
57
|
"scaffold/.nccgs/requirements.yaml", "scaffold/.nccgs/templates/context-packet.yaml",
|
|
53
58
|
"scaffold/.nccgs/templates/closure-record.md", "package-assets/setup-vivox-voice-chat-evals.gitignore",
|
|
59
|
+
"package-assets/nccgs-project.gitignore",
|
|
54
60
|
"scripts/cli.mjs", "scripts/doctor.mjs", "scripts/install.mjs",
|
|
55
|
-
"scripts/sync-unity-skills.mjs", "tests/framework.test.mjs"
|
|
61
|
+
"scripts/sync-unity-skills.mjs", "tests/framework.test.mjs",
|
|
62
|
+
".claude/nccgs/tools/integrity.mjs", ".claude/nccgs/tools/approve-overrides.mjs"
|
|
56
63
|
]) requireFile(relative);
|
|
57
64
|
|
|
65
|
+
const scaffoldIgnore = path.join(root, "scaffold/.nccgs/.gitignore");
|
|
66
|
+
const scaffoldIgnoreFallback = path.join(root, "package-assets/nccgs-project.gitignore");
|
|
67
|
+
if (fs.existsSync(scaffoldIgnore) && fs.readFileSync(scaffoldIgnore, "utf8") !== fs.readFileSync(scaffoldIgnoreFallback, "utf8")) {
|
|
68
|
+
errors.push("Scaffold .nccgs/.gitignore differs from its npm fallback asset.");
|
|
69
|
+
}
|
|
70
|
+
|
|
58
71
|
const versions = [
|
|
59
72
|
fs.readFileSync(path.join(root, "VERSION"), "utf8").trim(),
|
|
60
73
|
JSON.parse(fs.readFileSync(path.join(root, "package.json"), "utf8")).version,
|
|
@@ -70,17 +83,22 @@ const rootClaude = fs.readFileSync(path.join(root, "CLAUDE.md"), "utf8");
|
|
|
70
83
|
if (!rootClaude.includes("@.claude/nccgs/constitution.md")) errors.push("CLAUDE.md does not import the NCCGS constitution.");
|
|
71
84
|
|
|
72
85
|
const studio = JSON.parse(fs.readFileSync(path.join(root, ".claude/nccgs/studio.json"), "utf8"));
|
|
86
|
+
const pipeline = JSON.parse(fs.readFileSync(path.join(root, ".claude/nccgs/pipeline.json"), "utf8"));
|
|
87
|
+
for (const stale of compile(root)) errors.push(`Stale generated policy: ${stale}; run npm run policy:build`);
|
|
73
88
|
const agentNames = new Set();
|
|
74
89
|
for (const agent of studio.agents ?? []) {
|
|
75
90
|
if (agentNames.has(agent.name)) errors.push(`Duplicate catalog agent: ${agent.name}`);
|
|
76
91
|
agentNames.add(agent.name);
|
|
77
|
-
const
|
|
92
|
+
const active = pipeline.profiles[pipeline.defaultProfile].activeAgents.includes(agent.name);
|
|
93
|
+
const file = path.join(root, active ? ".claude/agents" : ".claude/nccgs/agent-library", `${agent.name}.md`);
|
|
78
94
|
if (!fs.existsSync(file)) { errors.push(`Catalog agent file missing: ${agent.name}`); continue; }
|
|
79
95
|
const metadata = frontmatter(file);
|
|
80
96
|
if (metadata?.name !== agent.name || !metadata?.description) errors.push(`Invalid agent frontmatter: ${agent.name}`);
|
|
81
97
|
const expected = studio.modelProfiles[studio.defaultProfile]?.[agent.modelClass];
|
|
82
|
-
if (metadata?.model !== expected) errors.push(`Agent model mismatch: ${agent.name} expected ${expected}, found ${metadata?.model}`);
|
|
83
|
-
|
|
98
|
+
if (active && metadata?.model !== expected) errors.push(`Agent model mismatch: ${agent.name} expected ${expected}, found ${metadata?.model}`);
|
|
99
|
+
const expectedEffort = studio.effortProfiles?.[studio.defaultProfile]?.[agent.modelClass];
|
|
100
|
+
if (active && metadata?.effort !== expectedEffort) errors.push(`Agent effort mismatch: ${agent.name} expected ${expectedEffort}, found ${metadata?.effort}`);
|
|
101
|
+
if ((metadata?.tools ?? "").split(/,\s*/).some(tool => ["Task", "Agent"].includes(tool))) errors.push(`Agent may recursively delegate: ${agent.name}`);
|
|
84
102
|
if (!fs.readFileSync(file, "utf8").includes("protocols/agent-contract.md")) errors.push(`Agent does not load common contract: ${agent.name}`);
|
|
85
103
|
if (fs.readFileSync(file, "utf8").split(/\r?\n/).length > 80) errors.push(`Agent entrypoint exceeds 80 lines: ${agent.name}`);
|
|
86
104
|
}
|
|
@@ -88,6 +106,7 @@ for (const agent of studio.agents ?? []) {
|
|
|
88
106
|
const actualAgents = walk(path.join(root, ".claude/agents")).filter((file) => file.endsWith(".md"));
|
|
89
107
|
for (const file of actualAgents) {
|
|
90
108
|
if (!agentNames.has(path.basename(file, ".md"))) errors.push(`Uncataloged NCCGS agent: ${posix(path.relative(root, file))}`);
|
|
109
|
+
if (!pipeline.coreAgents.includes(path.basename(file, ".md"))) errors.push(`Non-core agent active in source package: ${posix(path.relative(root, file))}`);
|
|
91
110
|
}
|
|
92
111
|
|
|
93
112
|
const workflows = JSON.parse(fs.readFileSync(path.join(root, ".claude/nccgs/workflow-catalog.json"), "utf8"));
|
|
@@ -103,6 +122,10 @@ for (const workflow of workflows.workflows ?? []) {
|
|
|
103
122
|
if (lines > 120) errors.push(`Workflow entrypoint exceeds 120 lines: ${workflow.name}`);
|
|
104
123
|
if (fs.existsSync(path.join(path.dirname(file), "README.md"))) errors.push(`NCCGS-owned skill contains redundant README: ${workflow.name}`);
|
|
105
124
|
for (const role of workflow.primaryRoles ?? []) if (!agentNames.has(role)) errors.push(`Workflow ${workflow.name} references unknown role ${role}`);
|
|
125
|
+
for (const [profile, roles] of Object.entries(workflow.primaryRolesByProfile ?? {})) {
|
|
126
|
+
if (!studio.modelProfiles[profile]) errors.push(`Workflow ${workflow.name} references unknown profile ${profile}`);
|
|
127
|
+
for (const role of roles) if (!agentNames.has(role)) errors.push(`Workflow ${workflow.name} references unknown role ${role}`);
|
|
128
|
+
}
|
|
106
129
|
}
|
|
107
130
|
for (const entry of workflows.entrypoints ?? []) if (!workflowNames.has(entry)) errors.push(`Missing entrypoint workflow: ${entry}`);
|
|
108
131
|
|
|
@@ -128,7 +151,12 @@ for (const command of hookCommands) {
|
|
|
128
151
|
const check = spawnSync(process.execPath, ["--check", file], { encoding: "utf8" });
|
|
129
152
|
if (check.status !== 0) errors.push(`Hook syntax failure ${match[1]}: ${check.stderr.trim()}`);
|
|
130
153
|
}
|
|
131
|
-
for (const file of [
|
|
154
|
+
for (const file of [
|
|
155
|
+
...walk(path.join(root, ".claude/nccgs/tools")).filter(file => file.endsWith(".mjs")),
|
|
156
|
+
path.join(root, "scripts/cli.mjs"),
|
|
157
|
+
path.join(root, "scripts/doctor.mjs"),
|
|
158
|
+
path.join(root, "scripts/install.mjs")
|
|
159
|
+
]) {
|
|
132
160
|
const check = spawnSync(process.execPath, ["--check", file], { encoding: "utf8" });
|
|
133
161
|
if (check.status !== 0) errors.push(`Script syntax failure ${posix(path.relative(root, file))}: ${check.stderr.trim()}`);
|
|
134
162
|
}
|
|
@@ -198,7 +226,7 @@ if (errors.length) {
|
|
|
198
226
|
|
|
199
227
|
console.log("NCCGS validation passed.");
|
|
200
228
|
console.log(`NCCGS workflows: ${workflowNames.size} (${workflows.entrypoints.length} entrypoints)`);
|
|
201
|
-
console.log(`Studio agents: ${agentNames.size}`);
|
|
229
|
+
console.log(`Studio agents: ${agentNames.size} in library; ${pipeline.coreAgents.length} active by default`);
|
|
202
230
|
console.log(`Path-scoped rules: ${rules.length}`);
|
|
203
231
|
console.log(`Managed hooks: ${hookCommands.length}`);
|
|
204
232
|
console.log(`Vendored Unity Skills: ${manifest?.skills?.length ?? 0}`);
|