nccgs 1.0.0 → 1.1.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/nccgs/VERSION +1 -1
- package/.claude/skills/migrate-project/SKILL.md +4 -2
- package/.claude/skills/migrate-project/references/procedure.md +12 -0
- package/README.md +16 -8
- package/UPGRADING.md +14 -10
- package/VERSION +1 -1
- package/docs/HUONG-DAN-MIGRATE-VA-SU-DUNG.md +18 -8
- package/docs/MIGRATION-MATRIX.md +2 -2
- package/package.json +1 -1
- package/scripts/cli.mjs +6 -2
- package/scripts/doctor.mjs +71 -0
- package/scripts/install.mjs +58 -15
- package/scripts/validate.mjs +2 -2
- package/tests/framework.test.mjs +60 -1
package/.claude/nccgs/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
1.
|
|
1
|
+
1.1.0
|
|
@@ -6,10 +6,11 @@ allowed-tools: Read, Glob, Grep, Write, Edit, Bash, Task, AskUserQuestion
|
|
|
6
6
|
|
|
7
7
|
# Migrate project
|
|
8
8
|
|
|
9
|
-
Treat brownfield adoption and
|
|
9
|
+
Treat brownfield adoption and migrations explicitly requested by the installer as CONTROLLED work. Read the complete [migration procedure](references/procedure.md). Default to `full` when no mode is given. Do not require migration merely because NCCGS was updated when the installer reported `No project migration required`.
|
|
10
10
|
|
|
11
11
|
## Modes
|
|
12
12
|
|
|
13
|
+
- `full` (default): run review, plan, apply, and verify in one invocation. Continue across phase boundaries without asking for ceremonial approval. Pause only for an unresolved Product Owner decision, destructive action, external side effect, or authority outside the current request.
|
|
13
14
|
- `review`: read-only inventory and KEEP/ADAPT/MIGRATE/DEPRECATE/REMOVE/MISSING classification.
|
|
14
15
|
- `plan`: write a dated reversible plan and proposed schema-2 policy without overwriting project truth.
|
|
15
16
|
- `apply`: execute an approved plan in validated batches with backups.
|
|
@@ -17,5 +18,6 @@ Treat brownfield adoption and NCCGS upgrades as CONTROLLED work. Read the comple
|
|
|
17
18
|
|
|
18
19
|
Migration decisions must cover canonical sources, ownership/protected paths, architecture, Unity tooling, model profile/fallback, enabled departments, context packets, verification baselines, closure safeguards, Git tracking, local settings, obsolete global skills, and rollback.
|
|
19
20
|
|
|
20
|
-
|
|
21
|
+
In `full` mode, write the plan before applying it, preserve the same evidence and rollback guarantees as the individual modes, and stop safely if review finds a decision that would materially change project truth. A reversible plan created within the same invocation is approved for in-scope application unless project policy explicitly requires a separate approval.
|
|
21
22
|
|
|
23
|
+
Never remove reports, registers, checklists, user settings, source assets, or legacy instructions until their live purpose is mapped and the Product Owner approves the replacement. Do not infer canon from code. Return batches, commits, preserved evidence, unresolved decisions, rollback, and final migration state.
|
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
# Brownfield migration procedure
|
|
2
2
|
|
|
3
|
+
## Full run
|
|
4
|
+
|
|
5
|
+
`/migrate-project` without a mode executes Review -> Plan -> Apply -> Verify as one
|
|
6
|
+
CONTROLLED operation. Keep the phase boundaries in the migration record, but do not
|
|
7
|
+
make the user invoke each phase separately. Continue automatically when changes are
|
|
8
|
+
reversible, backed up, within the requested adoption/migration scope, and do not
|
|
9
|
+
alter unresolved project truth. Stop before apply only when review exposes a genuine
|
|
10
|
+
Product Owner decision, destructive action, external side effect, or authority gap.
|
|
11
|
+
|
|
12
|
+
If the NCCGS installer and current schema indicate that an ordinary framework update
|
|
13
|
+
requires no project migration, report that fact and do not manufacture migration work.
|
|
14
|
+
|
|
3
15
|
## Review
|
|
4
16
|
|
|
5
17
|
1. Inspect repository status and project root without modifying it.
|
package/README.md
CHANGED
|
@@ -5,7 +5,7 @@ specialization of a full game-development team with project-aware governance,
|
|
|
5
5
|
bounded orchestration, configurable model routing, official Unity Skills, objective
|
|
6
6
|
evidence, and strict closure.
|
|
7
7
|
|
|
8
|
-
Version 1.
|
|
8
|
+
Version 1.1 contains:
|
|
9
9
|
|
|
10
10
|
- 45 studio agents across leadership, design, programming, Unity, content, quality,
|
|
11
11
|
and release;
|
|
@@ -48,13 +48,18 @@ npx nccgs@latest install
|
|
|
48
48
|
For an existing NCCGS installation:
|
|
49
49
|
|
|
50
50
|
```powershell
|
|
51
|
-
npx nccgs@latest
|
|
52
|
-
npx nccgs@latest
|
|
51
|
+
npx nccgs@latest update --dry-run
|
|
52
|
+
npx nccgs@latest update
|
|
53
|
+
npx nccgs@latest doctor
|
|
53
54
|
```
|
|
54
55
|
|
|
55
56
|
The current directory is the installation target. `--project <directory>` remains
|
|
56
57
|
available for automation, but is not needed for ordinary use.
|
|
57
58
|
|
|
59
|
+
Updates are hash-aware. Files that still match the previous install state are backed
|
|
60
|
+
up and replaced automatically; project-modified files block the update until they
|
|
61
|
+
are reviewed. Use `--force` only to back up and replace intentional overrides.
|
|
62
|
+
|
|
58
63
|
The installer validates the package, backs up replaced framework/settings files to
|
|
59
64
|
`.nccgs/backups/<timestamp>/`, copies `.claude/`, initializes missing `.nccgs/`
|
|
60
65
|
scaffold files without overwriting project truth, merges managed hooks into shared
|
|
@@ -64,13 +69,16 @@ import to `CLAUDE.md`.
|
|
|
64
69
|
Select a model profile during installation when needed:
|
|
65
70
|
|
|
66
71
|
```powershell
|
|
67
|
-
npx nccgs@latest
|
|
68
|
-
npx nccgs@latest
|
|
69
|
-
npx nccgs@latest
|
|
72
|
+
npx nccgs@latest update --model-profile quality
|
|
73
|
+
npx nccgs@latest update --model-profile balanced
|
|
74
|
+
npx nccgs@latest update --model-profile inherit
|
|
70
75
|
```
|
|
71
76
|
|
|
72
|
-
Open a new Claude Code session after
|
|
73
|
-
|
|
77
|
+
Open a new Claude Code session after first-time adoption of an existing project and
|
|
78
|
+
run `/migrate-project` once. It performs review, plan, apply, and verify as one
|
|
79
|
+
CONTROLLED operation, pausing only for a real Product Owner decision or unsafe side
|
|
80
|
+
effect. Ordinary framework updates report `No project migration required` and need
|
|
81
|
+
no Claude migration command.
|
|
74
82
|
|
|
75
83
|
## Model routing
|
|
76
84
|
|
package/UPGRADING.md
CHANGED
|
@@ -4,23 +4,27 @@ Always inspect a dry run and preserve project policy.
|
|
|
4
4
|
|
|
5
5
|
```powershell
|
|
6
6
|
cd "D:\path\to\UnityProject"
|
|
7
|
-
npx nccgs@latest
|
|
8
|
-
npx nccgs@latest
|
|
7
|
+
npx nccgs@latest update --dry-run
|
|
8
|
+
npx nccgs@latest update
|
|
9
|
+
npx nccgs@latest doctor
|
|
9
10
|
```
|
|
10
11
|
|
|
11
|
-
Version 1.
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
and
|
|
12
|
+
Version 1.1 adds hash-aware, idempotent updates. Managed files that still match the
|
|
13
|
+
previous install state update automatically. Project-modified framework files stop
|
|
14
|
+
the update and require review; `--force` backs them up before replacement. Existing
|
|
15
|
+
`.nccgs/project.yaml` remains project-owned and is never blindly overwritten.
|
|
16
|
+
|
|
17
|
+
When the installer reports that project migration is required, open a new Claude
|
|
18
|
+
Code session and run one command:
|
|
15
19
|
|
|
16
20
|
```text
|
|
17
|
-
/migrate-project
|
|
18
|
-
/migrate-project plan
|
|
19
|
-
/migrate-project apply
|
|
20
|
-
/migrate-project verify
|
|
21
|
+
/migrate-project
|
|
21
22
|
/status full
|
|
22
23
|
```
|
|
23
24
|
|
|
25
|
+
The workflow retains review, plan, apply, and verify internally. Do not run it after
|
|
26
|
+
an ordinary update when the installer reports `No project migration required`.
|
|
27
|
+
|
|
24
28
|
The migration plan must move schema 1 policy to schema 2, choose a model profile,
|
|
25
29
|
enable departments, configure context packets and orchestration, preserve closure
|
|
26
30
|
artifacts, verify hooks, and resolve obsolete global skills. Keep
|
package/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
1.
|
|
1
|
+
1.1.0
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
# Hướng dẫn migrate và sử dụng NCCGS 1.
|
|
1
|
+
# Hướng dẫn migrate và sử dụng NCCGS 1.1
|
|
2
2
|
|
|
3
|
-
NCCGS 1.
|
|
3
|
+
NCCGS 1.1 là studio operating system dành cho Unity project trong Claude Code. Nó
|
|
4
4
|
không thay thế cách chat trực tiếp; nó tự phân loại rủi ro, chọn workflow, agent và
|
|
5
5
|
model phù hợp, sử dụng official Unity Skills, thu thập bằng chứng và áp dụng closure.
|
|
6
6
|
|
|
@@ -38,13 +38,18 @@ npx nccgs@latest install
|
|
|
38
38
|
Nếu project đã có NCCGS:
|
|
39
39
|
|
|
40
40
|
```powershell
|
|
41
|
-
npx nccgs@latest
|
|
42
|
-
npx nccgs@latest
|
|
41
|
+
npx nccgs@latest update --dry-run
|
|
42
|
+
npx nccgs@latest update
|
|
43
|
+
npx nccgs@latest doctor
|
|
43
44
|
```
|
|
44
45
|
|
|
45
46
|
NCCGS mặc định cài vào directory hiện tại. Bạn không cần truyền đường dẫn project;
|
|
46
47
|
hãy mở PowerShell tại Unity project root hoặc dùng `cd` đến đó trước.
|
|
47
48
|
|
|
49
|
+
Từ 1.1, update kiểm tra hash của lần cài trước. File NCCGS chưa bị project sửa sẽ tự
|
|
50
|
+
được backup và cập nhật. File đã sửa tay sẽ chặn update; chỉ dùng `--force` sau khi
|
|
51
|
+
đã review và chấp nhận thay thế bản override.
|
|
52
|
+
|
|
48
53
|
Installer sẽ:
|
|
49
54
|
|
|
50
55
|
1. validate NCCGS;
|
|
@@ -68,7 +73,7 @@ Sau khi cài, đóng phiên Claude Code cũ và mở phiên mới tại project
|
|
|
68
73
|
Cài với profile cụ thể:
|
|
69
74
|
|
|
70
75
|
```powershell
|
|
71
|
-
npx nccgs@latest
|
|
76
|
+
npx nccgs@latest update --model-profile quality
|
|
72
77
|
```
|
|
73
78
|
|
|
74
79
|
Đổi profile trong project đã cài:
|
|
@@ -82,12 +87,17 @@ khoản hoặc context mode hiện tại, dùng `inherit` hoặc cho phép inher
|
|
|
82
87
|
|
|
83
88
|
## 4. Migrate project cũ
|
|
84
89
|
|
|
85
|
-
|
|
90
|
+
Thông thường chỉ cần gọi một command:
|
|
86
91
|
|
|
87
92
|
```text
|
|
88
|
-
|
|
93
|
+
/migrate-project
|
|
89
94
|
```
|
|
90
95
|
|
|
96
|
+
NCCGS vẫn thực hiện bốn phase `review -> plan -> apply -> verify` ở bên trong một
|
|
97
|
+
lần chạy. Nó chỉ dừng khi có quyết định Product Owner thật sự, thao tác destructive,
|
|
98
|
+
external side effect hoặc thiếu authority. Các mode riêng dưới đây vẫn tồn tại để
|
|
99
|
+
debug hoặc kiểm soát thủ công.
|
|
100
|
+
|
|
91
101
|
### Review
|
|
92
102
|
|
|
93
103
|
```text
|
|
@@ -258,7 +268,7 @@ Sau migration:
|
|
|
258
268
|
```powershell
|
|
259
269
|
git add .claude .nccgs CLAUDE.md
|
|
260
270
|
git status --short
|
|
261
|
-
git commit -m "chore: adopt NCCGS 1.
|
|
271
|
+
git commit -m "chore: adopt NCCGS 1.1 studio workflow"
|
|
262
272
|
```
|
|
263
273
|
|
|
264
274
|
Luôn đọc diff trước commit.
|
package/docs/MIGRATION-MATRIX.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
# CCGS to NCCGS 1.
|
|
1
|
+
# CCGS to NCCGS 1.1 migration matrix
|
|
2
2
|
|
|
3
|
-
| CCGS capability | NCCGS 1.
|
|
3
|
+
| CCGS capability | NCCGS 1.1 treatment | Reason |
|
|
4
4
|
|---|---|---|
|
|
5
5
|
| Three-tier studio hierarchy | ADAPT into 44 roles and a central one-level orchestrator | Preserve specialization without recursive context chains |
|
|
6
6
|
| Opus/Sonnet/Haiku agent pins | ADAPT into quality/balanced/inherit profiles | Preserve model routing while handling compatibility and cost |
|
package/package.json
CHANGED
package/scripts/cli.mjs
CHANGED
|
@@ -19,13 +19,15 @@ if (command === "version" || command === "--version" || command === "-v") {
|
|
|
19
19
|
process.exit(0);
|
|
20
20
|
}
|
|
21
21
|
|
|
22
|
-
if (
|
|
22
|
+
if (!new Set(["install", "update", "doctor"]).has(command)) {
|
|
23
23
|
console.error(`Unknown command: ${command}`);
|
|
24
24
|
printHelp();
|
|
25
25
|
process.exit(2);
|
|
26
26
|
}
|
|
27
27
|
|
|
28
|
-
const
|
|
28
|
+
const script = command === "doctor" ? "doctor.mjs" : "install.mjs";
|
|
29
|
+
const forwardedArgs = command === "update" ? ["--update-only", ...args] : args;
|
|
30
|
+
const result = spawnSync(process.execPath, [path.join(scriptDir, script), ...forwardedArgs], {
|
|
29
31
|
cwd: process.cwd(),
|
|
30
32
|
stdio: "inherit"
|
|
31
33
|
});
|
|
@@ -41,6 +43,8 @@ function printHelp() {
|
|
|
41
43
|
|
|
42
44
|
Usage:
|
|
43
45
|
nccgs install [options]
|
|
46
|
+
nccgs update [options]
|
|
47
|
+
nccgs doctor [options]
|
|
44
48
|
nccgs --version
|
|
45
49
|
|
|
46
50
|
Install target:
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import crypto from "node:crypto";
|
|
4
|
+
import fs from "node:fs";
|
|
5
|
+
import path from "node:path";
|
|
6
|
+
|
|
7
|
+
const args = process.argv.slice(2);
|
|
8
|
+
const projectIndex = args.indexOf("--project");
|
|
9
|
+
const allowPolicyMigration = args.includes("--allow-policy-migration");
|
|
10
|
+
const warnings = [];
|
|
11
|
+
if (projectIndex >= 0 && !args[projectIndex + 1]) {
|
|
12
|
+
console.error("--project requires a directory path.");
|
|
13
|
+
process.exit(2);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const project = projectIndex >= 0 ? path.resolve(args[projectIndex + 1]) : process.cwd();
|
|
17
|
+
const errors = [];
|
|
18
|
+
for (const marker of ["Assets", "Packages", "ProjectSettings"]) {
|
|
19
|
+
if (!fs.existsSync(path.join(project, marker))) errors.push(`Missing Unity marker: ${marker}`);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const statePath = path.join(project, ".claude", "nccgs", "install-state.json");
|
|
23
|
+
let state = null;
|
|
24
|
+
if (!fs.existsSync(statePath)) errors.push("NCCGS install state is missing.");
|
|
25
|
+
else {
|
|
26
|
+
try { state = JSON.parse(fs.readFileSync(statePath, "utf8")); }
|
|
27
|
+
catch (error) { errors.push(`Install state is invalid JSON: ${error.message}`); }
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
if (state) {
|
|
31
|
+
for (const item of state.files ?? []) {
|
|
32
|
+
const file = path.join(project, ...item.relative.split("/"));
|
|
33
|
+
if (!fs.existsSync(file)) errors.push(`Managed file is missing: ${item.relative}`);
|
|
34
|
+
else if (hashFile(file) !== item.sha256) errors.push(`Managed file was modified: ${item.relative}`);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const claudePath = path.join(project, "CLAUDE.md");
|
|
39
|
+
if (!fs.existsSync(claudePath) || !fs.readFileSync(claudePath, "utf8").includes("@.claude/nccgs/constitution.md")) {
|
|
40
|
+
errors.push("CLAUDE.md does not import the NCCGS constitution.");
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const policyPath = path.join(project, ".nccgs", "project.yaml");
|
|
44
|
+
let policySchema = null;
|
|
45
|
+
if (!fs.existsSync(policyPath)) errors.push(".nccgs/project.yaml is missing.");
|
|
46
|
+
else {
|
|
47
|
+
const match = fs.readFileSync(policyPath, "utf8").match(/^schema_version:\s*(\d+)\s*$/m);
|
|
48
|
+
policySchema = match ? Number(match[1]) : null;
|
|
49
|
+
if (policySchema !== 2) {
|
|
50
|
+
const message = `Project policy schema must be 2; found ${policySchema ?? "unknown"}.`;
|
|
51
|
+
if (allowPolicyMigration) warnings.push(message);
|
|
52
|
+
else errors.push(message);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
if (errors.length) {
|
|
57
|
+
for (const error of errors) console.error(`ERROR: ${error}`);
|
|
58
|
+
console.error(`NCCGS doctor failed with ${errors.length} error(s).`);
|
|
59
|
+
process.exit(1);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
for (const warning of warnings) console.warn(`WARN: ${warning}`);
|
|
63
|
+
console.log("NCCGS doctor passed.");
|
|
64
|
+
console.log(`Framework version: ${state.frameworkVersion}`);
|
|
65
|
+
console.log(`Model profile: ${state.modelProfile}`);
|
|
66
|
+
console.log(`Managed files verified: ${state.files.length}`);
|
|
67
|
+
console.log(`Project policy schema: ${policySchema ?? "migration required"}`);
|
|
68
|
+
|
|
69
|
+
function hashFile(file) {
|
|
70
|
+
return crypto.createHash("sha256").update(fs.readFileSync(file)).digest("hex");
|
|
71
|
+
}
|
package/scripts/install.mjs
CHANGED
|
@@ -10,6 +10,7 @@ const args = process.argv.slice(2);
|
|
|
10
10
|
const projectIndex = args.indexOf("--project");
|
|
11
11
|
const dryRun = args.includes("--dry-run");
|
|
12
12
|
const force = args.includes("--force");
|
|
13
|
+
const updateOnly = args.includes("--update-only");
|
|
13
14
|
const allowNonUnity = args.includes("--allow-non-unity");
|
|
14
15
|
const modelProfileIndex = args.indexOf("--model-profile");
|
|
15
16
|
const requestedModelProfile = modelProfileIndex >= 0 ? args[modelProfileIndex + 1] : null;
|
|
@@ -75,28 +76,45 @@ if (fs.existsSync(installStatePath)) {
|
|
|
75
76
|
process.exit(2);
|
|
76
77
|
}
|
|
77
78
|
}
|
|
79
|
+
if (updateOnly && !previousInstallState) {
|
|
80
|
+
console.error("NCCGS is not installed in this project. Run `npx nccgs@latest install` first.");
|
|
81
|
+
process.exit(2);
|
|
82
|
+
}
|
|
83
|
+
const previousFiles = new Map((previousInstallState?.files ?? []).map((item) => [item.relative, item]));
|
|
78
84
|
const currentFrameworkNames = new Set(frameworkFiles.map((item) => posix(item.relative)));
|
|
79
85
|
const staleFrameworkFiles = (previousInstallState?.files ?? [])
|
|
80
86
|
.filter((item) => !currentFrameworkNames.has(item.relative))
|
|
81
|
-
.map((item) => ({
|
|
87
|
+
.map((item) => ({
|
|
88
|
+
...item,
|
|
89
|
+
destination: path.join(project, ...item.relative.split("/")),
|
|
90
|
+
unmodified: fs.existsSync(path.join(project, ...item.relative.split("/"))) && hashFile(path.join(project, ...item.relative.split("/"))) === item.sha256
|
|
91
|
+
}))
|
|
82
92
|
.filter((item) => fs.existsSync(item.destination));
|
|
83
93
|
|
|
84
|
-
const
|
|
94
|
+
const differentFiles = frameworkFiles.filter((item) => {
|
|
85
95
|
if (!fs.existsSync(item.destination)) return false;
|
|
86
96
|
return !sameFile(item.source, item.destination);
|
|
87
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);
|
|
88
105
|
|
|
89
|
-
if ((conflicts.length ||
|
|
90
|
-
console.error(`
|
|
106
|
+
if ((conflicts.length || staleConflicts.length) && !force) {
|
|
107
|
+
console.error(`NCCGS found ${conflicts.length} modified and ${staleConflicts.length} modified-stale framework file(s):`);
|
|
91
108
|
for (const item of conflicts.slice(0, 30)) console.error(`- ${posix(item.relative)}`);
|
|
92
|
-
for (const item of
|
|
109
|
+
for (const item of staleConflicts.slice(0, 30)) console.error(`- stale: ${item.relative}`);
|
|
93
110
|
if (conflicts.length > 30) console.error(`- ... ${conflicts.length - 30} more`);
|
|
94
|
-
console.error("
|
|
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.");
|
|
95
113
|
process.exit(2);
|
|
96
114
|
}
|
|
97
115
|
|
|
98
116
|
const newFiles = frameworkFiles.filter((item) => !fs.existsSync(item.destination));
|
|
99
|
-
const identicalFiles = frameworkFiles.length - newFiles.length -
|
|
117
|
+
const identicalFiles = frameworkFiles.length - newFiles.length - differentFiles.length;
|
|
100
118
|
const scaffoldFiles = walk(path.join(root, "scaffold")).map((file) => ({
|
|
101
119
|
source: file,
|
|
102
120
|
relative: path.relative(path.join(root, "scaffold"), file),
|
|
@@ -114,8 +132,9 @@ if (!new Set(["quality", "balanced", "inherit"]).has(modelProfile)) {
|
|
|
114
132
|
}
|
|
115
133
|
|
|
116
134
|
console.log(`NCCGS installation target: ${project}`);
|
|
117
|
-
console.log(`Framework
|
|
118
|
-
console.log(`
|
|
135
|
+
console.log(`Framework version: ${previousInstallState?.frameworkVersion ?? "not installed"} -> ${fs.readFileSync(path.join(root, "VERSION"), "utf8").trim()}`);
|
|
136
|
+
console.log(`Framework files: ${frameworkFiles.length} (${newFiles.length} new, ${identicalFiles} current, ${automaticReplacements.length} automatic update, ${conflicts.length} forced override)`);
|
|
137
|
+
console.log(`Stale framework files: ${automaticStale.length} automatic removal, ${staleConflicts.length} forced removal`);
|
|
119
138
|
console.log(`Project scaffold files to initialize: ${newScaffold.length}`);
|
|
120
139
|
console.log(`Managed Claude settings: ${settingsPlan.changed ? "update" : "already current"}`);
|
|
121
140
|
console.log(`Agent model profile: ${modelProfile}`);
|
|
@@ -126,7 +145,7 @@ if (dryRun) {
|
|
|
126
145
|
|
|
127
146
|
const stamp = new Date().toISOString().replace(/[:.]/g, "-");
|
|
128
147
|
const backupRoot = path.join(project, ".nccgs", "backups", stamp);
|
|
129
|
-
for (const item of
|
|
148
|
+
for (const item of differentFiles) {
|
|
130
149
|
const backup = path.join(backupRoot, item.relative);
|
|
131
150
|
fs.mkdirSync(path.dirname(backup), { recursive: true });
|
|
132
151
|
fs.copyFileSync(item.destination, backup);
|
|
@@ -163,11 +182,31 @@ if (modelConfiguration.status !== 0) {
|
|
|
163
182
|
process.exit(1);
|
|
164
183
|
}
|
|
165
184
|
|
|
166
|
-
writeInstallState(project, modelProfile, frameworkFiles, installStatePath);
|
|
185
|
+
writeInstallState(project, modelProfile, frameworkFiles, installStatePath, previousInstallState);
|
|
186
|
+
|
|
187
|
+
const doctor = spawnSync(process.execPath, [
|
|
188
|
+
path.join(scriptDir, "doctor.mjs"),
|
|
189
|
+
"--project", project,
|
|
190
|
+
"--allow-policy-migration"
|
|
191
|
+
], { cwd: project, encoding: "utf8" });
|
|
192
|
+
process.stdout.write(doctor.stdout ?? "");
|
|
193
|
+
process.stderr.write(doctor.stderr ?? "");
|
|
194
|
+
if (doctor.status !== 0) {
|
|
195
|
+
console.error("NCCGS files were installed, but post-install verification failed.");
|
|
196
|
+
process.exit(1);
|
|
197
|
+
}
|
|
167
198
|
|
|
168
199
|
console.log("NCCGS installation complete.");
|
|
169
|
-
if (
|
|
170
|
-
|
|
200
|
+
if (differentFiles.length || staleFrameworkFiles.length || settingsPlan.changed) console.log(`Changed shared files backed up under: ${backupRoot}`);
|
|
201
|
+
const installedPolicy = fs.readFileSync(targetPolicyPath, "utf8");
|
|
202
|
+
const installedPolicySchema = Number(installedPolicy.match(/^schema_version:\s*(\d+)\s*$/m)?.[1] ?? 0);
|
|
203
|
+
if (installedPolicySchema !== 2) {
|
|
204
|
+
console.log("Project migration required: open a new Claude Code session and run `/migrate-project` once.");
|
|
205
|
+
} else if (!previousInstallState) {
|
|
206
|
+
console.log("First NCCGS adoption: run `/migrate-project` once for an existing project; a new project can start with `/status`.");
|
|
207
|
+
} else {
|
|
208
|
+
console.log("No project migration required for this NCCGS update.");
|
|
209
|
+
}
|
|
171
210
|
|
|
172
211
|
function walk(directory) {
|
|
173
212
|
const output = [];
|
|
@@ -234,11 +273,15 @@ function installManagedSettings(plan, backupDirectory) {
|
|
|
234
273
|
fs.writeFileSync(plan.settingsPath, plan.nextText, "utf8");
|
|
235
274
|
}
|
|
236
275
|
|
|
237
|
-
function writeInstallState(projectRoot, profile, files, statePath) {
|
|
276
|
+
function writeInstallState(projectRoot, profile, files, statePath, previousState) {
|
|
277
|
+
const now = new Date().toISOString();
|
|
238
278
|
const state = {
|
|
239
|
-
schemaVersion:
|
|
279
|
+
schemaVersion: 2,
|
|
240
280
|
frameworkVersion: fs.readFileSync(path.join(root, "VERSION"), "utf8").trim(),
|
|
281
|
+
previousFrameworkVersion: previousState?.frameworkVersion ?? null,
|
|
241
282
|
modelProfile: profile,
|
|
283
|
+
installedAt: previousState?.installedAt ?? now,
|
|
284
|
+
updatedAt: now,
|
|
242
285
|
files: files.map((item) => ({
|
|
243
286
|
relative: posix(item.relative),
|
|
244
287
|
sha256: hashFile(path.join(projectRoot, item.relative))
|
package/scripts/validate.mjs
CHANGED
|
@@ -51,7 +51,7 @@ for (const relative of [
|
|
|
51
51
|
"scaffold/.nccgs/project.yaml", "scaffold/.nccgs/state.md",
|
|
52
52
|
"scaffold/.nccgs/requirements.yaml", "scaffold/.nccgs/templates/context-packet.yaml",
|
|
53
53
|
"scaffold/.nccgs/templates/closure-record.md", "package-assets/setup-vivox-voice-chat-evals.gitignore",
|
|
54
|
-
"scripts/cli.mjs", "scripts/install.mjs",
|
|
54
|
+
"scripts/cli.mjs", "scripts/doctor.mjs", "scripts/install.mjs",
|
|
55
55
|
"scripts/sync-unity-skills.mjs", "tests/framework.test.mjs"
|
|
56
56
|
]) requireFile(relative);
|
|
57
57
|
|
|
@@ -128,7 +128,7 @@ for (const command of hookCommands) {
|
|
|
128
128
|
const check = spawnSync(process.execPath, ["--check", file], { encoding: "utf8" });
|
|
129
129
|
if (check.status !== 0) errors.push(`Hook syntax failure ${match[1]}: ${check.stderr.trim()}`);
|
|
130
130
|
}
|
|
131
|
-
for (const file of [path.join(root, ".claude/nccgs/tools/configure-models.mjs"), path.join(root, "scripts/cli.mjs"), path.join(root, "scripts/install.mjs")]) {
|
|
131
|
+
for (const file of [path.join(root, ".claude/nccgs/tools/configure-models.mjs"), path.join(root, "scripts/cli.mjs"), path.join(root, "scripts/doctor.mjs"), path.join(root, "scripts/install.mjs")]) {
|
|
132
132
|
const check = spawnSync(process.execPath, ["--check", file], { encoding: "utf8" });
|
|
133
133
|
if (check.status !== 0) errors.push(`Script syntax failure ${posix(path.relative(root, file))}: ${check.stderr.trim()}`);
|
|
134
134
|
}
|
package/tests/framework.test.mjs
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import assert from "node:assert/strict";
|
|
2
|
+
import crypto from "node:crypto";
|
|
2
3
|
import fs from "node:fs";
|
|
3
4
|
import os from "node:os";
|
|
4
5
|
import path from "node:path";
|
|
@@ -9,6 +10,8 @@ import { fileURLToPath } from "node:url";
|
|
|
9
10
|
const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
|
10
11
|
const studio = JSON.parse(fs.readFileSync(path.join(root, ".claude/nccgs/studio.json"), "utf8"));
|
|
11
12
|
const workflows = JSON.parse(fs.readFileSync(path.join(root, ".claude/nccgs/workflow-catalog.json"), "utf8"));
|
|
13
|
+
const frameworkVersion = fs.readFileSync(path.join(root, "VERSION"), "utf8").trim();
|
|
14
|
+
const sha256 = (value) => crypto.createHash("sha256").update(value).digest("hex");
|
|
12
15
|
|
|
13
16
|
test("studio roles are unique and workflow references resolve", () => {
|
|
14
17
|
const roles = new Set(studio.agents.map((agent) => agent.name));
|
|
@@ -24,6 +27,12 @@ test("quality profile assigns all model classes", () => {
|
|
|
24
27
|
}
|
|
25
28
|
});
|
|
26
29
|
|
|
30
|
+
test("migrate-project defaults to a single full run", () => {
|
|
31
|
+
const skill = fs.readFileSync(path.join(root, ".claude/skills/migrate-project/SKILL.md"), "utf8");
|
|
32
|
+
assert.match(skill, /Default to `full`/);
|
|
33
|
+
assert.match(skill, /review, plan, apply, and verify in one invocation/i);
|
|
34
|
+
});
|
|
35
|
+
|
|
27
36
|
test("model configurator can remove and restore model pins", () => {
|
|
28
37
|
const temp = fs.mkdtempSync(path.join(os.tmpdir(), "nccgs-model-test-"));
|
|
29
38
|
try {
|
|
@@ -104,7 +113,7 @@ test("installer preserves project truth, merges settings, and applies inherit pr
|
|
|
104
113
|
assert.ok(fs.existsSync(path.join(temp, ".nccgs/backups")));
|
|
105
114
|
const installStatePath = path.join(temp, ".claude/nccgs/install-state.json");
|
|
106
115
|
const installState = JSON.parse(fs.readFileSync(installStatePath, "utf8"));
|
|
107
|
-
assert.equal(installState.frameworkVersion,
|
|
116
|
+
assert.equal(installState.frameworkVersion, frameworkVersion);
|
|
108
117
|
assert.equal(installState.modelProfile, "inherit");
|
|
109
118
|
|
|
110
119
|
const obsoleteRelative = ".claude/agents/nccgs-obsolete.md";
|
|
@@ -119,3 +128,53 @@ test("installer preserves project truth, merges settings, and applies inherit pr
|
|
|
119
128
|
fs.rmSync(temp, { recursive: true, force: true });
|
|
120
129
|
}
|
|
121
130
|
});
|
|
131
|
+
|
|
132
|
+
test("update replaces untouched managed files, blocks local edits, and doctor verifies state", () => {
|
|
133
|
+
const temp = fs.mkdtempSync(path.join(os.tmpdir(), "nccgs-idempotent-update-test-"));
|
|
134
|
+
try {
|
|
135
|
+
for (const marker of ["Assets", "Packages", "ProjectSettings"]) fs.mkdirSync(path.join(temp, marker));
|
|
136
|
+
const install = spawnSync(process.execPath, [path.join(root, "scripts/cli.mjs"), "install", "--model-profile", "quality"], { cwd: temp, encoding: "utf8" });
|
|
137
|
+
assert.equal(install.status, 0, install.stderr || install.stdout);
|
|
138
|
+
|
|
139
|
+
const managedRelative = ".claude/nccgs/constitution.md";
|
|
140
|
+
const managedPath = path.join(temp, ...managedRelative.split("/"));
|
|
141
|
+
const statePath = path.join(temp, ".claude/nccgs/install-state.json");
|
|
142
|
+
const state = JSON.parse(fs.readFileSync(statePath, "utf8"));
|
|
143
|
+
const oldManagedContent = "managed by NCCGS 1.0.0\n";
|
|
144
|
+
fs.writeFileSync(managedPath, oldManagedContent, "utf8");
|
|
145
|
+
state.frameworkVersion = "1.0.0";
|
|
146
|
+
state.files.find((item) => item.relative === managedRelative).sha256 = sha256(oldManagedContent);
|
|
147
|
+
fs.writeFileSync(statePath, `${JSON.stringify(state, null, 2)}\n`, "utf8");
|
|
148
|
+
|
|
149
|
+
const update = spawnSync(process.execPath, [path.join(root, "scripts/cli.mjs"), "update"], { cwd: temp, encoding: "utf8" });
|
|
150
|
+
assert.equal(update.status, 0, update.stderr || update.stdout);
|
|
151
|
+
assert.match(update.stdout, /1\.0\.0 -> 1\.1\.0/);
|
|
152
|
+
assert.match(update.stdout, /automatic update/);
|
|
153
|
+
assert.match(update.stdout, /No project migration required/);
|
|
154
|
+
assert.equal(fs.readFileSync(managedPath, "utf8"), fs.readFileSync(path.join(root, managedRelative), "utf8"));
|
|
155
|
+
|
|
156
|
+
const doctor = spawnSync(process.execPath, [path.join(root, "scripts/cli.mjs"), "doctor"], { cwd: temp, encoding: "utf8" });
|
|
157
|
+
assert.equal(doctor.status, 0, doctor.stderr || doctor.stdout);
|
|
158
|
+
assert.match(doctor.stdout, /NCCGS doctor passed/);
|
|
159
|
+
|
|
160
|
+
fs.writeFileSync(managedPath, "project-local override\n", "utf8");
|
|
161
|
+
const blocked = spawnSync(process.execPath, [path.join(root, "scripts/cli.mjs"), "update", "--dry-run"], { cwd: temp, encoding: "utf8" });
|
|
162
|
+
assert.equal(blocked.status, 2);
|
|
163
|
+
assert.match(blocked.stderr, /modified/);
|
|
164
|
+
assert.equal(fs.readFileSync(managedPath, "utf8"), "project-local override\n");
|
|
165
|
+
} finally {
|
|
166
|
+
fs.rmSync(temp, { recursive: true, force: true });
|
|
167
|
+
}
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
test("update refuses a project without an NCCGS install state", () => {
|
|
171
|
+
const temp = fs.mkdtempSync(path.join(os.tmpdir(), "nccgs-update-missing-test-"));
|
|
172
|
+
try {
|
|
173
|
+
for (const marker of ["Assets", "Packages", "ProjectSettings"]) fs.mkdirSync(path.join(temp, marker));
|
|
174
|
+
const result = spawnSync(process.execPath, [path.join(root, "scripts/cli.mjs"), "update"], { cwd: temp, encoding: "utf8" });
|
|
175
|
+
assert.equal(result.status, 2);
|
|
176
|
+
assert.match(result.stderr, /not installed/);
|
|
177
|
+
} finally {
|
|
178
|
+
fs.rmSync(temp, { recursive: true, force: true });
|
|
179
|
+
}
|
|
180
|
+
});
|