nccgs 2.0.0-rc.3 → 2.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.
Files changed (76) hide show
  1. package/.claude/nccgs/MEMORY.md +7 -2
  2. package/.claude/nccgs/VERSION +1 -1
  3. package/.claude/nccgs/packages/ponytail/README.md +2 -2
  4. package/.claude/nccgs/protocols/agent-contract.md +15 -2
  5. package/.claude/nccgs/protocols/bounded-repair.md +135 -7
  6. package/.claude/nccgs/protocols/editor-session-evidence.md +52 -0
  7. package/.claude/nccgs/protocols/orchestration.md +7 -5
  8. package/.claude/nccgs/protocols/review-handoff.md +17 -1
  9. package/.claude/nccgs/protocols/standalone-gates.md +64 -8
  10. package/.claude/nccgs/protocols/standalone.md +33 -13
  11. package/.claude/nccgs/protocols/task-gates.md +13 -1
  12. package/.claude/nccgs/protocols/task-slices.md +106 -0
  13. package/.claude/nccgs/routing.generated.md +20 -0
  14. package/.claude/nccgs/tools/command-process.mjs +10 -4
  15. package/.claude/nccgs/tools/compile-policy.mjs +8 -0
  16. package/.claude/nccgs/tools/configure-models.mjs +4 -0
  17. package/.claude/nccgs/tools/editor-session-evidence.mjs +66 -0
  18. package/.claude/nccgs/tools/gates.mjs +7 -4
  19. package/.claude/nccgs/tools/handoff.mjs +51 -0
  20. package/.claude/nccgs/tools/inputs.mjs +36 -1
  21. package/.claude/nccgs/tools/installation-guard.mjs +55 -0
  22. package/.claude/nccgs/tools/lifecycle.mjs +21 -0
  23. package/.claude/nccgs/tools/orchestration.mjs +264 -14
  24. package/.claude/nccgs/tools/repair-packet.mjs +29 -10
  25. package/.claude/nccgs/tools/returned-handoff.mjs +38 -0
  26. package/.claude/nccgs/tools/review-budget.mjs +171 -0
  27. package/.claude/nccgs/tools/review-handoff.mjs +42 -8
  28. package/.claude/nccgs/tools/runtime.mjs +7 -2
  29. package/.claude/nccgs/tools/session-budget.mjs +67 -0
  30. package/.claude/nccgs/tools/task-slices.mjs +147 -0
  31. package/.claude/nccgs/tools/task.mjs +39 -2
  32. package/.claude/nccgs/tools/tool-guard.mjs +19 -5
  33. package/.claude/nccgs/tools/unity-repair.mjs +128 -0
  34. package/.claude/nccgs/tools/verification-jobs.mjs +13 -3
  35. package/.claude/nccgs/tools/windows-compiler-server.cs +163 -0
  36. package/.claude/nccgs/tools/windows-job.cs +8 -1
  37. package/CHANGELOG.md +142 -0
  38. package/MEMORY.md +7 -2
  39. package/README.md +57 -27
  40. package/UPGRADING.md +27 -18
  41. package/VERSION +1 -1
  42. package/docs/ARCHITECTURE.md +45 -18
  43. package/docs/BOUNDED-REPAIR.md +76 -7
  44. package/docs/EDITOR-SESSION-EVIDENCE.md +52 -0
  45. package/docs/HANDOFF-AUTOMATION.md +144 -0
  46. package/docs/HUONG-DAN-MIGRATE-VA-SU-DUNG.md +68 -11
  47. package/docs/PHASE-1-OPERATIONS.md +142 -1
  48. package/docs/PHASE-2-OPERATIONS.md +1 -1
  49. package/docs/RELEASE-STATUS.md +102 -4
  50. package/docs/TASK-SLICING.md +57 -0
  51. package/docs/UNINSTALL.md +134 -0
  52. package/docs/UPGRADE-QUALIFICATION.md +1 -1
  53. package/docs/WORKFLOWS.md +1 -1
  54. package/package.json +2 -2
  55. package/scaffold/.nccgs/templates/repair-unity-code.json +18 -0
  56. package/scaffold/.nccgs/templates/repair-unity-editor.json +21 -0
  57. package/scaffold/.nccgs/templates/task-slices.json +28 -0
  58. package/scripts/cli.mjs +15 -2
  59. package/scripts/install.mjs +14 -0
  60. package/scripts/recover-session.mjs +89 -0
  61. package/scripts/run.mjs +21 -8
  62. package/scripts/supervise.mjs +30 -6
  63. package/scripts/uninstall.mjs +292 -0
  64. package/scripts/validate.mjs +6 -0
  65. package/scripts/work-handoff.mjs +38 -0
  66. package/tests/editor-session-evidence.test.mjs +45 -0
  67. package/tests/fixtures/compiler-shutdown.cs +64 -0
  68. package/tests/orchestration-v15.test.mjs +52 -1
  69. package/tests/phase1.test.mjs +92 -0
  70. package/tests/policy-v15.test.mjs +14 -0
  71. package/tests/review-budget.test.mjs +177 -0
  72. package/tests/runtime-v15.test.mjs +572 -4
  73. package/tests/session-budget.test.mjs +34 -0
  74. package/tests/task-slices.test.mjs +285 -0
  75. package/tests/uninstall.test.mjs +477 -0
  76. package/tests/unity-generated.test.mjs +140 -0
@@ -4,10 +4,11 @@ import { taskPath, check } from "./gates.mjs";
4
4
  import { boundTask } from "./runtime.mjs";
5
5
  import { claim, findDispatch, rejectUnclaimed, admitRepairTool, dispatchPrompt, pinnedRepairEvidence } from "./orchestration.mjs";
6
6
  import { requireDesignReady } from "./design-readiness.mjs";
7
+ import { inFinalReviewPhase } from "./review-budget.mjs";
7
8
 
8
9
  const need = (condition, message) => { if (!condition) throw new Error(message); };
9
10
  const reads = new Set(["Read", "Glob", "Grep", "WebSearch", "WebFetch", "AskUserQuestion"]);
10
- const governance = new Set(["init", "bind", "inspect", "check", "snapshot", "ref", "record", "report", "reclassify", "refresh-inputs", "reserve", "dispatch-status", "dispatch-prompt", "checkpoint", "resume", "telemetry", "export-external", "close", "job-status", "job-wait", "acceptance-status", "design-validate", "design-compare"]);
11
+ const governance = new Set(["init", "bind", "inspect", "check", "snapshot", "ref", "record", "report", "reclassify", "refresh-inputs", "reserve", "dispatch-status", "dispatch-prompt", "checkpoint", "resume", "telemetry", "export-external", "close", "job-status", "job-wait", "acceptance-status", "design-validate", "design-compare", "handoff-validate", "record-review", "slice-preview", "slice-plan", "slice-status", "budget-status"]);
11
12
  export function parseTaskCommand(command, project = process.cwd()) {
12
13
  // Parse data quotes without interpreting a shell. Punctuation inside report
13
14
  // text is data; composition/redirection/expansion outside quotes stays blocked.
@@ -47,6 +48,16 @@ export function safeVerifyCommand(command, project = process.cwd()) {
47
48
  export function recoveryTool(input, project = process.cwd()) {
48
49
  return !input.agent_id && ["Bash", "PowerShell"].includes(input.tool_name) && ["report", "inspect"].includes(safeTaskCommand(input.tool_input?.command ?? "", project));
49
50
  }
51
+ function ownHandoffValidation(command, project, task, dispatch) {
52
+ const parsed = parseTaskCommand(command, project);
53
+ if (!parsed || parsed.command !== "handoff-validate" || parsed.forwarded || dispatch?.status !== "running") return false;
54
+ const values = new Map(), args = parsed.options.slice(3);
55
+ for (let i = 0; i < args.length; i += 2) {
56
+ if (!["--id", "--dispatch", "--file", "--project"].includes(args[i]) || values.has(args[i]) || !args[i + 1] || args[i + 1].startsWith("--")) return false;
57
+ values.set(args[i], args[i + 1]);
58
+ }
59
+ return values.get("--id") === task?.id && values.get("--dispatch") === dispatch.id && dispatch.handoffPaths?.includes(values.get("--file"));
60
+ }
50
61
  export function guardTool(project, input) {
51
62
  if (recoveryTool(input, project)) return;
52
63
  if (resolveExecution(project).name !== "standalone") return;
@@ -68,7 +79,7 @@ export function guardTool(project, input) {
68
79
  const readiness = check(project, task, "review");
69
80
  need(readiness.allowed, readiness.message);
70
81
  }
71
- const prompt = dispatch.workPacket ? dispatchPrompt(project, task.id, dispatch.id).prompt : null;
82
+ const prompt = dispatch.workPacket || dispatch.slice ? dispatchPrompt(project, task.id, dispatch.id).prompt : null;
72
83
  claim(project, dispatch.id, { sessionId: input.session_id, toolUseId: input.tool_use_id, agent: tool.subagent_type });
73
84
  if (prompt) return { updatedInput: { ...tool, prompt } };
74
85
  } catch (error) {
@@ -87,8 +98,9 @@ export function guardTool(project, input) {
87
98
  contained(project, relative);
88
99
  need(!/^\.(?:claude|nccgs)(?:\/|$)/i.test(relative) || relative.startsWith(".nccgs/evidence/"), "Framework/governance records must be changed through the task CLI or external configuration");
89
100
  if (relative.startsWith(".nccgs/evidence/")) {
101
+ need(!(dispatch?.handoffTransport === "native-result" && dispatch.handoffPaths?.includes(relative)), "Native-result handoff is captured only from the correlated final response; return text without writing this path");
90
102
  need(!pinnedRepairEvidence(project, relative), "Reserved repair packet/context evidence is immutable; preserve it and create a fresh packet for a new eligible attempt");
91
- need(!/^\.nccgs\/evidence\/(?:(?:runtime|criteria|design-checks|checkpoints|external\/requests)\/|check-)/i.test(relative), "Captured runtime/checkpoint/verification/external request evidence is immutable and never authored by Claude");
103
+ need(!/^\.nccgs\/evidence\/(?:(?:runtime|criteria|design-checks|checkpoints|repair-operations|slices|external\/requests)\/|check-)/i.test(relative), "Captured runtime/checkpoint/verification/external request evidence is immutable and never authored by Claude");
92
104
  if (input.agent_id && readOnly) need(task?.contract.analysisArtifacts?.includes(relative), "Read-only worker may write only declared analysis artifacts");
93
105
  return;
94
106
  }
@@ -96,18 +108,20 @@ export function guardTool(project, input) {
96
108
  requireDesignReady(project, task);
97
109
  need(dispatch && dispatch.status === "running" && dispatch.agentId === input.agent_id, "Source writes require a running, correlated implementation dispatch");
98
110
  const lower = relative.toLowerCase();
111
+ if (dispatch.slice) need(dispatch.writePaths.some(p => lower === p.toLowerCase()), "Slice writes require an exact planned file; directory ownership is not granted");
99
112
  need(dispatch.writePaths.some(p => lower === p.toLowerCase() || lower.startsWith(p.toLowerCase() + "/")), "Write is outside this dispatch's owned paths");
100
113
  return;
101
114
  }
102
115
  if (["Bash", "PowerShell"].includes(name)) {
103
116
  const command = tool.command ?? "";
104
- need(!/\b(?:dispatch-reconcile|extend-dispatch|extend-review|import-external|migrate-execution)\b|--stage\s+["']?(?:acceptance|astra)\b/i.test(command), "External decisions cannot be made by the Claude executor");
117
+ if (input.agent_id && ownHandoffValidation(command, project, task, dispatch)) return;
118
+ need(!/\b(?:dispatch-reconcile|extend-dispatch|extend-review|import-external|migrate-execution|migrate-framework|budget-plan)\b|--stage\s+["']?(?:acceptance|astra)\b/i.test(command), "External decisions cannot be made by the Claude executor");
105
119
  if (safeTaskCommand(command, project)) { need(!input.agent_id, "Coordinator owns lifecycle commands"); return; }
106
120
  need(task && !readOnly, "Audit/plan/review shell execution is blocked; use read tools or externally supplied verification evidence");
107
121
  requireDesignReady(project, task);
108
122
  // Coordinator may run declared checks after workers finish. This command
109
123
  // can execute a verifier, so it is allowed only for implementation tasks.
110
- if (!input.agent_id && safeVerifyCommand(command, project)) return;
124
+ if (!input.agent_id && safeVerifyCommand(command, project)) { need(!inFinalReviewPhase(project, input.session_id), "Final-review reserve cannot run new verification jobs; use existing receipts"); return; }
111
125
  need(dispatch?.kind === "implementation" && dispatch.status === "running" && input.agent_id === dispatch.agentId, "Shell mutation requires a correlated implementation worker");
112
126
  return;
113
127
  }
@@ -0,0 +1,128 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import crypto from "node:crypto";
4
+ import { artifact, atomicJson, contained, json, verifyArtifact } from "./policy.mjs";
5
+ import { snapshot } from "./gates.mjs";
6
+
7
+ const need = (ok, message) => { if (!ok) throw new Error(`Unity repair: ${message}`); };
8
+ const keys = (value, allowed) => value && typeof value === "object" && !Array.isArray(value) && Object.keys(value).every(k => allowed.includes(k));
9
+ const code = /\.(?:cs|asmdef|asmref|shader|hlsl|cginc|compute|uxml|uss)$/i;
10
+ const asset = /\.(?:unity|prefab|asset|mat|controller|overrideController|anim)$/i;
11
+ export const unityCodePath = relative => relative.startsWith("Assets/") && code.test(relative);
12
+ export const unitySceneSafety = "loaded-scenes-clean-v1";
13
+ export const unityRepairProfiles = Object.freeze({
14
+ "unity-code": Object.freeze({ files: 8, workCalls: 24, readCalls: 12, handoffCalls: 3, durationMs: 900000 }),
15
+ "unity-editor": Object.freeze({ files: 8, workCalls: 20, readCalls: 8, handoffCalls: 3, durationMs: 900000 })
16
+ });
17
+
18
+ function guid(project, relative) {
19
+ const value = fs.readFileSync(contained(project, relative), "utf8").match(/^guid: ([a-f0-9]{32})\r?$/m)?.[1];
20
+ need(value, `missing or invalid Unity GUID in ${relative}`);
21
+ return value;
22
+ }
23
+
24
+ // The packet pins a chosen operation, not a general shell/MCP capability. The
25
+ // installed Unity CLI binds every call to this project's absolute path.
26
+ export function validateUnityRepair(project, packet) {
27
+ const profile = unityRepairProfiles[packet.repairType];
28
+ need(profile, "repairType must be unity-code or unity-editor");
29
+ need(fs.existsSync(contained(project, "ProjectSettings/ProjectVersion.txt")), "requires an existing Unity project");
30
+ need(packet.changes.length <= profile.files, `at most ${profile.files} exact files per packet`);
31
+ for (const change of packet.changes) {
32
+ need(keys(change, ["path", "operation", "instruction"]) && ["edit", "create"].includes(change.operation), "each change needs edit or create operation");
33
+ need(unityCodePath(change.path) || (packet.repairType === "unity-editor" && change.path.startsWith("Assets/") && asset.test(change.path)), "file type must match the repair profile under Assets/; no raw metadata, packages or project settings");
34
+ const file = contained(project, change.path);
35
+ need(fs.statSync(path.dirname(file)).isDirectory(), "create files only in existing folders; folder creation needs a separate scoped change");
36
+ if (change.operation === "edit") {
37
+ need(fs.existsSync(file) && fs.statSync(file).isFile(), "edit requires an existing file");
38
+ guid(project, `${change.path}.meta`);
39
+ } else need(!fs.existsSync(file) && !fs.existsSync(contained(project, `${change.path}.meta`)), "create target or companion metadata already exists");
40
+ }
41
+ need(keys(packet.unity, ["capabilities", "steps"]) && Array.isArray(packet.unity.steps) && packet.unity.steps.length <= 4, "unity requires capabilities and at most four fixed steps");
42
+ if (packet.unity.steps.length || packet.unity.capabilities !== undefined) {
43
+ need(packet.unity.capabilities?.path?.startsWith(".nccgs/evidence/") && !packet.unity.capabilities.path.startsWith(".nccgs/evidence/work-packets/"), "pin Unity command discovery evidence");
44
+ verifyArtifact(project, packet.unity.capabilities);
45
+ }
46
+ need((packet.repairType === "unity-code" && packet.changes.every(c => c.operation === "edit")) || packet.unity.steps.length > 0, "asset repairs and new code require an Editor/import step before completion");
47
+ const ids = new Set();
48
+ for (const step of packet.unity.steps) {
49
+ need(keys(step, ["id", "command", "arguments", "timeoutMs"]) && /^[a-zA-Z][\w-]{0,39}$/.test(step.id ?? "") && !ids.has(step.id), "steps need unique bounded IDs");
50
+ ids.add(step.id);
51
+ need(/^[a-zA-Z][\w-]{0,79}$/.test(step.command ?? "") && Array.isArray(step.arguments) && step.arguments.length <= 8 &&
52
+ step.arguments.every(a => typeof a === "string" && a.length <= 8000 && !a.startsWith("-") && !a.includes("\0")), "use a discovered command and positional arguments only; routing/global flags are generated");
53
+ need(Number.isSafeInteger(step.timeoutMs) && step.timeoutMs >= 1000 && step.timeoutMs <= 60000, "step timeoutMs must be 1000-60000; long verification belongs to managed jobs");
54
+ need(step.command === "eval" && step.arguments.length === 1, "new Editor steps require eval with one C# statement body so the dirty-scene check runs atomically before the operation");
55
+ need(!/^\s*(?:global\s+)?using\s+(?:static\s+)?[\w.]+\s*(?:=|;)/m.test(step.arguments[0]), "eval bodies must use fully qualified types; top-level using directives cannot follow the scene guard");
56
+ }
57
+ const { files, ...limits } = profile;
58
+ return limits;
59
+ }
60
+
61
+ export function captureUnityRepairScope(project, dispatch, source) {
62
+ if (dispatch.workPacket?.schemaVersion !== 2) return;
63
+ const metaGuids = Object.fromEntries(dispatch.workPacket.changes.map(c => [c.path, c.operation === "edit" ? guid(project, `${c.path}.meta`) : null]));
64
+ const relative = `.nccgs/evidence/work-packets/${dispatch.id}-scope.json`;
65
+ atomicJson(contained(project, relative), { schemaVersion: 1, taskId: dispatch.taskId, dispatchId: dispatch.id,
66
+ sourceSnapshot: source.sha256, fileEntries: source.fileEntries, metaGuids });
67
+ dispatch.workPacket.scope = artifact(project, relative);
68
+ }
69
+
70
+ export function verifyUnityRepairScope(project, dispatch, complete = false) {
71
+ if (dispatch.workPacket?.schemaVersion !== 2) return;
72
+ const p = dispatch.workPacket, baseline = json(verifyArtifact(project, p.scope));
73
+ need(baseline.dispatchId === dispatch.id && baseline.taskId === dispatch.taskId && baseline.sourceSnapshot === dispatch.snapshot, "scope baseline identity changed");
74
+ const before = new Map(baseline.fileEntries), after = new Map(snapshot(project, null, { includeFiles: true }).fileEntries);
75
+ const owned = new Set(p.changes.flatMap(c => [c.path, `${c.path}.meta`]));
76
+ for (const [file, hash] of before) {
77
+ need(after.has(file), `source file removed: ${file}; deletions/renames are outside this packet`);
78
+ need(after.get(file) === hash || owned.has(file), `source changed outside exact ownership: ${file}`);
79
+ }
80
+ for (const file of after.keys()) need(before.has(file) || owned.has(file), `source created outside exact ownership: ${file}`);
81
+ for (const c of p.changes) {
82
+ const meta = `${c.path}.meta`, present = after.has(meta);
83
+ if (present) {
84
+ const current = guid(project, meta);
85
+ need(!baseline.metaGuids[c.path] || current === baseline.metaGuids[c.path], `existing GUID changed: ${meta}`);
86
+ }
87
+ if (complete) need(after.has(c.path) && present, `output or imported metadata missing: ${c.path}; finish import before native completion`);
88
+ }
89
+ }
90
+
91
+ export function repairEditorCommand(dispatch, step) {
92
+ return `node .claude/nccgs/tools/task.mjs repair-editor --id ${dispatch.taskId} --dispatch ${dispatch.id} --step ${step.id}`;
93
+ }
94
+
95
+ export function unityEditorToolScope(dispatch, input) {
96
+ if (dispatch.workPacket?.schemaVersion !== 2 || input.tool_name !== "Bash") return null;
97
+ const tool = input.tool_input ?? {}, step = dispatch.workPacket.unity.steps.find(s => repairEditorCommand(dispatch, s) === tool.command);
98
+ need(step && keys(tool, ["command", "timeout", "description", "run_in_background"]) && tool.run_in_background !== true &&
99
+ Number.isSafeInteger(tool.timeout) && tool.timeout >= step.timeoutMs + 5000 && tool.timeout <= 120000,
100
+ "only the fixed repair-editor command is permitted, foreground, with timeout at least step timeoutMs + 5000");
101
+ return { bucket: "work", read: false, editorStep: step.id };
102
+ }
103
+
104
+ export function unityEditorInvocation(project, step, safety = null) {
105
+ need(safety === null || safety === unitySceneSafety, "unknown pinned scene safety policy");
106
+ let sceneGuard = null, args = step.arguments;
107
+ if (safety) {
108
+ need(step.command === "eval" && args.length === 1, "guarded Editor step requires one eval body");
109
+ sceneGuard = { policy: safety, token: crypto.randomUUID() };
110
+ // Same Editor eval, before any packet statement: includes untitled/additive
111
+ // loaded scenes. Never auto-save, discard or switch a dirty scene.
112
+ const prefix = `{ for (int nccgsSceneIndex = 0; nccgsSceneIndex < UnityEngine.SceneManagement.SceneManager.sceneCount; nccgsSceneIndex++) { var nccgsScene = UnityEngine.SceneManagement.SceneManager.GetSceneAt(nccgsSceneIndex); if (nccgsScene.isDirty) return new { nccgsSceneGuard = "${safety}", token = "${sceneGuard.token}", blockedScene = string.IsNullOrEmpty(nccgsScene.path) ? "<untitled>" : nccgsScene.path }; } }\n`;
113
+ args = [prefix + args[0]];
114
+ }
115
+ return { ...(sceneGuard ? { sceneGuard } : {}), executable: process.platform === "win32" ? "unity.exe" : "unity", arguments: ["command", step.command, ...args,
116
+ "--caller", "plugin", "--skill", "unity-cli",
117
+ "--project-path", fs.realpathSync(project), "--timeout", String(Math.ceil(step.timeoutMs / 1000)), "--format", "json", "--no-pager", "--non-interactive"] };
118
+ }
119
+
120
+ export function blockedUnityScene(invocation, result) {
121
+ if (!invocation.sceneGuard || result.status !== 0 || result.error || result.signal) return null;
122
+ try {
123
+ const response = JSON.parse(result.stdout), envelope = response.data?.result, value = envelope?.result;
124
+ if (response.success === true && envelope.success === true && value?.nccgsSceneGuard === invocation.sceneGuard.policy &&
125
+ value.token === invocation.sceneGuard.token && typeof value.blockedScene === "string") return value.blockedScene;
126
+ } catch { /* Unknown output retains existing conservative execution handling. */ }
127
+ return null;
128
+ }
@@ -8,6 +8,7 @@ import { snapshot, taskPath } from "./gates.mjs";
8
8
  import { inputPolicy } from "./inputs.mjs";
9
9
  import { acquireLock, ownerState, pidState } from "./process-lock.mjs";
10
10
  import { captureCriteria, measurementBaseline } from "./criteria.mjs";
11
+ import { inFinalReviewPhase } from "./review-budget.mjs";
11
12
 
12
13
  const worker = fileURLToPath(new URL("./verification-worker.mjs", import.meta.url));
13
14
  const need = (condition, message) => { if (!condition) throw new Error(message); };
@@ -24,8 +25,14 @@ export function finishJob(project, job, result) {
24
25
  const directory = jobDirectory(project, job.id);
25
26
  const receipt = `.nccgs/evidence/check-${job.id}.json`;
26
27
  if (fs.existsSync(contained(project, receipt))) return jobStatus(project, job.id, job.taskId);
27
- let after = null, snapshotError = null;
28
- try { after = snapshot(project, json(taskPath(project, job.taskId))).sha256; } catch (error) { snapshotError = error.message; }
28
+ let after = null, afterGenerated = [], snapshotError = null;
29
+ try {
30
+ const captured = snapshot(project, json(taskPath(project, job.taskId)));
31
+ after = captured.sha256; afterGenerated = captured.unityGeneratedFiles ?? [];
32
+ } catch (error) { snapshotError = error.message; }
33
+ const beforeGenerated = new Map((job.unityGeneratedBaseline ?? []).map(file => [file.path, file.sha256]));
34
+ const generatedFileChanges = afterGenerated.filter(file => beforeGenerated.get(file.path) !== file.sha256)
35
+ .map(file => ({ path: file.path, beforeSha256: beforeGenerated.get(file.path) ?? null, afterSha256: file.sha256 }));
29
36
  const logPath = `.nccgs/evidence/check-${job.id}.log`;
30
37
  fs.mkdirSync(path.dirname(contained(project, logPath)), { recursive: true });
31
38
  if (!fs.existsSync(contained(project, logPath))) fs.writeFileSync(contained(project, logPath), result.error || "No command output captured\n");
@@ -38,6 +45,7 @@ export function finishJob(project, job, result) {
38
45
  timeoutMs: job.timeoutMs, supervisorDeadline: job.supervisorDeadline ?? null,
39
46
  timedOut: result.timedOut === true, signal: result.signal ?? null,
40
47
  snapshotError, executable: job.executable, arguments: job.arguments, exitCode: result.exitCode ?? null,
48
+ ...(job.unityGeneratedBaseline ? { unityGeneratedFiles: { before: job.unityGeneratedBaseline, after: afterGenerated, changes: generatedFileChanges } } : {}),
41
49
  error: result.error ?? null, sourceUnchanged: job.snapshot === after, status, log: artifact(project, logPath),
42
50
  acceptance: captureCriteria(project, job, status) });
43
51
  atomicJson(path.join(directory, "result.json"), { status, evidence: artifact(project, receipt) });
@@ -83,6 +91,7 @@ export function listJobs(project, taskId) {
83
91
  export function pendingJobs(project, taskId) { return listJobs(project, taskId).filter(job => !["PASS", "FAIL"].includes(job.status)); }
84
92
  export function startVerification(project, taskId, options) {
85
93
  project = fs.realpathSync(project);
94
+ need(!inFinalReviewPhase(project, process.env.NCCGS_SUPERVISED_SESSION), "Final-review reserve cannot run new verification jobs; use existing receipts");
86
95
  const task = json(taskPath(project, taskId));
87
96
  need(task.schemaVersion !== 2 || task.mode === "implement" || !(process.env.CLAUDECODE || process.env.CLAUDE_CODE_ENTRYPOINT), "Audit/plan tasks cannot run arbitrary verification commands inside Claude");
88
97
  need(typeof options.name === "string" && options.name.trim() && options.executable, "verify requires --name CHECK -- EXECUTABLE [ARGS]");
@@ -107,7 +116,7 @@ export function startVerification(project, taskId, options) {
107
116
  const lock = acquireLock(`${index}.lock`, { taskId, requestKey });
108
117
  let measurementLock;
109
118
  try {
110
- const current = snapshot(project, task).sha256;
119
+ const captured = snapshot(project, task), current = captured.sha256;
111
120
  const spec = { taskId, name: options.name, executable: options.executable, arguments: options.arguments ?? [],
112
121
  timeoutMs, snapshot: current, contractHash: task.contractHash ?? null,
113
122
  ...(supervisorDeadline === undefined ? {} : { supervisorDeadline }),
@@ -133,6 +142,7 @@ export function startVerification(project, taskId, options) {
133
142
  fs.mkdirSync(directory, { recursive: true });
134
143
  fs.mkdirSync(contained(project, ".nccgs/evidence"), { recursive: true });
135
144
  const job = { schemaVersion: 1, id: jobId, project, ...spec, requestKey, requestHash, measurementBaseline: baseline, queuedAt: new Date().toISOString(),
145
+ ...(captured.unityGeneratedFiles ? { unityGeneratedBaseline: captured.unityGeneratedFiles } : {}),
136
146
  sessionId: process.env.NCCGS_SUPERVISED_SESSION || null };
137
147
  atomicJson(path.join(directory, "request.json"), job);
138
148
  atomicJson(index, { jobId, requestHash }); // pinned before launch: an ambiguous crash cannot mint a duplicate
@@ -0,0 +1,163 @@
1
+ using System;
2
+ using System.Collections.Generic;
3
+ using System.IO;
4
+ using System.IO.Pipes;
5
+ using System.Runtime.InteropServices;
6
+ using System.Security.Cryptography;
7
+ using System.Security.Principal;
8
+ using System.Text;
9
+ using System.Text.RegularExpressions;
10
+
11
+ // Optional, fail-closed cleanup for the qualified Unity/Roslyn binary pair.
12
+ // Never kill or exempt a descendant. The caller still waits for an empty Job.
13
+ // Shutdown frame matches BuildRequest.CreateShutdown().WriteAsync() in Roslyn
14
+ // 41a5af9d2c459a06c0795bf21a1c046200f375bf (4.3.1). Unknown builds are untouched.
15
+ internal sealed class NccgsUnityCompilerShutdown {
16
+ const string CompilerHash = "a671cfd0e1ca5621845cc62c62e1de06233a9c48de76151bc51d36758cd1ca5f";
17
+ const string HostHash = "d2607dbc6c95c252baaba299b659f156b388c6130ac846f79eb4c768f91a019f";
18
+ const string Commit = "41a5af9d2c459a06c0795bf21a1c046200f375bf";
19
+ readonly HashSet<int> attempted = new HashSet<int>();
20
+ DateTime nextScan = DateTime.MinValue;
21
+ sealed class Candidate : IDisposable {
22
+ internal int Pid; internal IntPtr Handle; internal string Pipe;
23
+ public void Dispose() { if(Handle!=IntPtr.Zero) CloseHandle(Handle); Handle=IntPtr.Zero; }
24
+ }
25
+ [DllImport("kernel32.dll", SetLastError=true)] static extern bool QueryInformationJobObject(IntPtr job,int kind,IntPtr data,int size,IntPtr returned);
26
+ [DllImport("kernel32.dll", SetLastError=true)] static extern IntPtr OpenProcess(uint access,bool inherit,int pid);
27
+ [DllImport("kernel32.dll", SetLastError=true)] static extern bool IsProcessInJob(IntPtr process,IntPtr job,out bool member);
28
+ [DllImport("kernel32.dll", CharSet=CharSet.Unicode, SetLastError=true)] static extern bool QueryFullProcessImageName(IntPtr process,int flags,StringBuilder name,ref int size);
29
+ [DllImport("kernel32.dll", SetLastError=true)] static extern bool GetNamedPipeServerProcessId(IntPtr pipe,out uint pid);
30
+ [DllImport("kernel32.dll")] static extern bool CloseHandle(IntPtr handle);
31
+ [DllImport("kernel32.dll")] static extern uint WaitForSingleObject(IntPtr handle,uint timeout);
32
+ [DllImport("kernel32.dll")] static extern IntPtr LocalFree(IntPtr memory);
33
+ [DllImport("ntdll.dll")] static extern int NtQueryInformationProcess(IntPtr process,int kind,IntPtr data,int size,out int needed);
34
+ [DllImport("shell32.dll", CharSet=CharSet.Unicode, SetLastError=true)] static extern IntPtr CommandLineToArgvW(string command,out int count);
35
+
36
+ static bool OwnedAlive(IntPtr job,IntPtr process) {
37
+ bool member;
38
+ return WaitForSingleObject(process,0)==258 && IsProcessInJob(process,job,out member) && member;
39
+ }
40
+ static int[] Members(IntPtr job) {
41
+ for(int size=4096;size<=1048576;size*=2) {
42
+ IntPtr data=Marshal.AllocHGlobal(size);
43
+ try {
44
+ if(!QueryInformationJobObject(job,3,data,size,IntPtr.Zero)) {
45
+ if(Marshal.GetLastWin32Error()==234) continue;
46
+ return null;
47
+ }
48
+ int count=Marshal.ReadInt32(data,4);
49
+ if(count<0 || count>(size-8)/IntPtr.Size) return null;
50
+ int[] result=new int[count];
51
+ for(int i=0;i<count;i++) result[i]=checked((int)Marshal.ReadIntPtr(data,8+i*IntPtr.Size).ToInt64());
52
+ return result;
53
+ } finally { Marshal.FreeHGlobal(data); }
54
+ }
55
+ return null;
56
+ }
57
+ static string Image(IntPtr process) {
58
+ int length=32768; var text=new StringBuilder(length);
59
+ return QueryFullProcessImageName(process,0,text,ref length) ? text.ToString() : null;
60
+ }
61
+ static string Command(IntPtr process) {
62
+ int needed; NtQueryInformationProcess(process,60,IntPtr.Zero,0,out needed);
63
+ if(needed<16 || needed>131072) return null;
64
+ IntPtr data=Marshal.AllocHGlobal(needed);
65
+ try {
66
+ int size; if(NtQueryInformationProcess(process,60,data,needed,out size)!=0) return null;
67
+ int bytes=(ushort)Marshal.ReadInt16(data);
68
+ IntPtr text=Marshal.ReadIntPtr(data,IntPtr.Size==8?8:4);
69
+ long offset=text.ToInt64()-data.ToInt64();
70
+ if(bytes%2!=0 || offset<0 || offset+bytes>needed) return null;
71
+ return Marshal.PtrToStringUni(text,bytes/2);
72
+ } finally { Marshal.FreeHGlobal(data); }
73
+ }
74
+ static string[] Arguments(string command) {
75
+ if(String.IsNullOrEmpty(command)) return null;
76
+ int count; IntPtr data=CommandLineToArgvW(command,out count);
77
+ if(data==IntPtr.Zero) return null;
78
+ try {
79
+ var result=new string[count];
80
+ for(int i=0;i<count;i++) result[i]=Marshal.PtrToStringUni(Marshal.ReadIntPtr(data,i*IntPtr.Size));
81
+ return result;
82
+ } finally { LocalFree(data); }
83
+ }
84
+ static string Digest(string file) {
85
+ using(var stream=File.OpenRead(file)) using(var hash=SHA256.Create())
86
+ return BitConverter.ToString(hash.ComputeHash(stream)).Replace("-","").ToLowerInvariant();
87
+ }
88
+ internal static string QualifiedPipe(string image,string[] args) {
89
+ if(image==null || args==null || args.Length!=4 || args[1]!="exec" || !Path.IsPathRooted(args[2])) return null;
90
+ string compiler=Path.GetFullPath(args[2]);
91
+ string suffix=Path.Combine("Editor","Data","DotNetSdkRoslyn","VBCSCompiler.dll");
92
+ if(!compiler.EndsWith(Path.DirectorySeparatorChar+suffix,StringComparison.OrdinalIgnoreCase)) return null;
93
+ string data=Path.GetDirectoryName(Path.GetDirectoryName(compiler));
94
+ if(!String.Equals(Path.GetFullPath(image),Path.Combine(data,"NetCoreRuntime","dotnet.exe"),StringComparison.OrdinalIgnoreCase)) return null;
95
+ if(!args[3].StartsWith("-pipename:",StringComparison.Ordinal)) return null;
96
+ string pipe=args[3].Substring(10);
97
+ if(!Regex.IsMatch(pipe,@"\A[A-Za-z0-9_.-]{1,160}\z")) return null;
98
+ if(Digest(compiler)!=CompilerHash || Digest(image)!=HostHash) return null;
99
+ return pipe;
100
+ }
101
+ internal static byte[] ShutdownFrame() {
102
+ using(var body=new MemoryStream()) {
103
+ using(var writer=new BinaryWriter(body,Encoding.Unicode,true)) {
104
+ writer.Write(Guid.Empty.ToByteArray()); writer.Write((uint)1146299681);
105
+ writer.Write(Commit); writer.Write(1); writer.Write(1360294437);
106
+ writer.Write(0); writer.Write(0); writer.Flush();
107
+ }
108
+ using(var framed=new MemoryStream()) using(var writer=new BinaryWriter(framed)) {
109
+ writer.Write(checked((int)body.Length)); writer.Write(body.ToArray()); return framed.ToArray();
110
+ }
111
+ }
112
+ }
113
+ static bool ReadExact(Stream stream,byte[] bytes) {
114
+ int offset=0;
115
+ while(offset<bytes.Length) {
116
+ var read=stream.ReadAsync(bytes,offset,bytes.Length-offset);
117
+ if(!read.Wait(1000) || read.Result==0) return false;
118
+ offset+=read.Result;
119
+ }
120
+ return true;
121
+ }
122
+ // Peer PID is verified on this exact open pipe before any bytes are written.
123
+ // A same-name pipe belonging to a foreign server receives no shutdown request.
124
+ internal static bool Request(IntPtr job,IntPtr process,int pid,string pipeName) {
125
+ using(var pipe=new NamedPipeClientStream(".",pipeName,PipeDirection.InOut,PipeOptions.Asynchronous,TokenImpersonationLevel.Identification)) {
126
+ pipe.Connect(250);
127
+ uint serverPid;
128
+ if(!GetNamedPipeServerProcessId(pipe.SafePipeHandle.DangerousGetHandle(),out serverPid) || serverPid!=(uint)pid || !OwnedAlive(job,process)) return false;
129
+ byte[] frame=ShutdownFrame();
130
+ if(!pipe.WriteAsync(frame,0,frame.Length).Wait(1000)) return false;
131
+ byte[] length=new byte[4]; if(!ReadExact(pipe,length) || BitConverter.ToInt32(length,0)!=8) return false;
132
+ byte[] response=new byte[8]; if(!ReadExact(pipe,response)) return false;
133
+ return BitConverter.ToInt32(response,0)==3 && BitConverter.ToInt32(response,4)==pid;
134
+ }
135
+ }
136
+ internal void TryRequest(IntPtr job) {
137
+ if(DateTime.UtcNow<nextScan) return;
138
+ nextScan=DateTime.UtcNow.AddMilliseconds(500);
139
+ var candidates=new List<Candidate>();
140
+ try {
141
+ int[] members=Members(job); if(members==null || members.Length==0) return;
142
+ foreach(int pid in members) {
143
+ IntPtr process=OpenProcess(0x00101000,false,pid);
144
+ if(process==IntPtr.Zero) return; // uncertain membership/liveness: leave ownership alone
145
+ var candidate=new Candidate { Pid=pid,Handle=process };
146
+ candidates.Add(candidate);
147
+ if(!OwnedAlive(job,process)) return;
148
+ string image=Image(process);
149
+ if(String.Equals(image,Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.System),"conhost.exe"),StringComparison.OrdinalIgnoreCase)) continue;
150
+ candidate.Pipe=QualifiedPipe(image,Arguments(Command(process)));
151
+ // Do not shut down even a known compiler while another real worker remains.
152
+ if(candidate.Pipe==null) return;
153
+ }
154
+ foreach(var c in candidates) {
155
+ if(c.Pipe==null || !attempted.Add(c.Pid)) continue;
156
+ bool acknowledged=false;
157
+ try { acknowledged=Request(job,c.Handle,c.Pid,c.Pipe); } catch { /* keep original ownership and timeout */ }
158
+ Console.Error.WriteLine("[NCCGS] Owned Unity compiler {0}: graceful shutdown {1}; waiting for all descendants.",c.Pid,acknowledged?"acknowledged":"unconfirmed");
159
+ }
160
+ } catch { /* Unknown runtime/process data never authorizes cleanup or completion. */ }
161
+ finally { foreach(var c in candidates) c.Dispose(); }
162
+ }
163
+ }
@@ -73,7 +73,14 @@ public static class NccgsWindowsJob {
73
73
  if(!CreateProcess(executable,command,IntPtr.Zero,IntPtr.Zero,true,0x00080000|0x08000000,IntPtr.Zero,cwd,ref start,out proc)) throw Error("Create process in Job Object");
74
74
  WaitForSingleObject(proc.process,0xffffffff); uint code; if(!GetExitCodeProcess(proc.process,out code)) throw Error("Process result");
75
75
  // A root exit is not completion: retain ownership until every descendant exits.
76
- for(;;) { Accounting data; if(!QueryInformationJobObject(job,1,out data,Marshal.SizeOf(typeof(Accounting)),IntPtr.Zero)) throw Error("Descendant state"); if(data.active==0) break; System.Threading.Thread.Sleep(50); }
76
+ var compilerShutdown=new NccgsUnityCompilerShutdown();
77
+ for(;;) {
78
+ Accounting data;
79
+ if(!QueryInformationJobObject(job,1,out data,Marshal.SizeOf(typeof(Accounting)),IntPtr.Zero)) throw Error("Descendant state");
80
+ if(data.active==0) break;
81
+ compilerShutdown.TryRequest(job);
82
+ System.Threading.Thread.Sleep(50);
83
+ }
77
84
  return unchecked((int)code);
78
85
  } finally {
79
86
  if(proc.thread!=IntPtr.Zero) CloseHandle(proc.thread); if(proc.process!=IntPtr.Zero) CloseHandle(proc.process);
package/CHANGELOG.md CHANGED
@@ -1,5 +1,137 @@
1
1
  # Changelog
2
2
 
3
+ ## 2.1.0 — Unreleased
4
+
5
+ - Handle the final LOW diagnostic finding: empty, incomplete, unreadable or
6
+ disappearing admission locks refuse with an actionable message, preserving
7
+ the original error cause. Keep ownership checks and explicit retry unchanged.
8
+
9
+ - Resolve the follow-up review's two low-severity observations: distinguish
10
+ admission contention from active uninstall, and record session recovery as an
11
+ immutable pending receipt plus a separate success/failure outcome. Missing or
12
+ malformed outcomes remain unconfirmed; prior attempts and reviews stay intact.
13
+
14
+ - Resolve the first independent maintenance review's four findings: add explicit
15
+ hash-bound operator recovery for crashed launcher leases with immutable receipts;
16
+ retain unknown process-state refusal and all task/job/run ownership checks.
17
+ Serialize run/install/agent/migration/policy admissions against uninstall,
18
+ cancel pre-mutation refusals without stranded receipts, and return backup IDs
19
+ even when empty-directory cleanup produces warnings. Historical negative review
20
+ and packages stay unchanged; the corrected archive requires fresh qualification.
21
+
22
+ - Add `nccgs uninstall --project DIR`, a read-only `--dry-run` and guarded
23
+ `--rollback ID`. Remove unchanged inventoried framework files and exact NCCGS
24
+ hooks/imports; preserve customized files, shared Claude preferences, game source
25
+ and the full `.nccgs` policy/task/review/evidence/budget history.
26
+ - Save every removed/replaced byte in a project-local uninstall receipt before
27
+ changing files. Refuse unsafe paths, ambiguous custom hooks, active/unverified
28
+ work and rollback over later user edits; attempt restoration on write failure.
29
+ - Register `nccgs run` sessions so uninstallation cannot race a new managed
30
+ launch. An interrupted uninstall blocks launch/update until recovery. No process
31
+ is killed and no task, acceptance requirement, review or budget is reset.
32
+ - The published 2.0.0 archive remains unchanged. This feature receives local
33
+ framework and installation tests; it is not a new Claude Code/Unity benchmark.
34
+ See [uninstall and recovery](docs/UNINSTALL.md).
35
+
36
+ ## 2.0.0 — 2026-10-28
37
+
38
+ Published to npm on 2026-10-28. The following notes describe its original
39
+ preparation scope; the exact publication and native qualification have separate
40
+ frozen receipts. No new version is published by preparing 2.1.0.
41
+
42
+ - Publish existing external operator Editor start, graceful shutdown command and
43
+ settled stop receipts into immutable reviewer-readable project bundles. Preserve
44
+ original bytes, hashes and ownership, reject inconsistent/uncertain/forced stops,
45
+ and identify historical imports without rewriting sealed tasks or verdicts.
46
+ - Keep RC4 handoff, scene guard, source controls, process management, independent
47
+ review, acceptance and total-budget enforcement unchanged. No new game gate or
48
+ process permission is introduced.
49
+ - Record scoped exact-RC4 native qualification: real dirty-scene refusal, new repair,
50
+ automatic PARTIAL handoff with no continuation, all 28 criteria/385 assertions,
51
+ independent PASS and normal closure. The original LOW evidence-location finding
52
+ remains in that sealed review; its addendum is assessed separately.
53
+ - Stable archive validation and independent maintenance review are recorded in
54
+ release receipts. They do not claim another full Unity task through these bytes.
55
+
56
+
57
+ ## 2.0.0-rc.4 — 2026-10-28
58
+
59
+ Prepared candidate; unpublished. No npm tag or stable promotion.
60
+
61
+ - Settle a protected implementation work phase with a deterministic PARTIAL
62
+ report when current implementation and measured checks are ready for review.
63
+ A prose-only response no longer spends another continuation solely on handoff.
64
+ Preserve independent verdicts, all acceptance gates, history and original caps.
65
+ - Pin an atomic loaded-scene precondition to new Unity repair reservations.
66
+ Dirty named, additive or untitled scenes block the operation without auto-save
67
+ or discard. Retain failed attempts and old reservations; no implicit retry.
68
+ - Reserve final independent review money/time within the original root budget.
69
+ Share the work pool across design, slices, repair and verification. Admit
70
+ unknown worker cost only through verified, exclusive native session coverage;
71
+ missing session usage fails closed, and resumed cumulative cost is never summed
72
+ twice. The prior native reserve case qualified its disclosed new-script and
73
+ asset repair path with 28 criteria/385 assertions and independent final PASS.
74
+ - RC4-specific handoff/scene changes receive framework tests and operator Unity
75
+ checks. A fresh native task through this exact archive remains pending.
76
+
77
+ - Qualify the full component lifecycle on a fresh Unity project through actual
78
+ Claude Code: reviewed design, two dependent implementation slices from API
79
+ scaffolds, first-import verification, independent review, a disclosed fault,
80
+ typed repair and independent re-review. All 24 original criteria (349 assertions)
81
+ pass; the real failed PlayMode attempt remains recorded. The combined design and
82
+ implementation phases used 7/12 dispatches, three independent reviews and
83
+ USD 10.214066/12 within the original 75-minute envelope. Installed source guards,
84
+ process ownership, role limits, criteria and history remained unchanged. This
85
+ known-test component qualification does not establish broad game productivity.
86
+ - Add opt-in exact `unityGeneratedFiles` declarations before task initialization
87
+ so Unity's first import can create IDE/local metadata without a false source
88
+ mutation failure. Pin the declaration as a canonical input and retain output
89
+ before/after hashes in managed receipts. Keep all undeclared files tracked;
90
+ reject source paths, folders, globs and canonical overlaps. Existing policies,
91
+ historical attempts, process ownership, review and budgets remain unchanged.
92
+ - Settle cold Unity compilation without waiting for the qualified Roslyn server's
93
+ idle keepalive. After the command root exits and no other worker remains, request
94
+ graceful shutdown only from the verified compiler pipe inside the owned Windows
95
+ Job Object. Match the tested compiler/host binary hashes; unknown builds retain
96
+ the original wait/timeout behavior. Still drain every descendant and preserve the
97
+ command exit code, deadlines, independent review, criteria, history and budgets.
98
+ - Avoid Unity CLI's two-minute idle auth broker in managed verification command
99
+ trees using the supported process-local `UNITY_NO_AUTH_BROKER=1` setting.
100
+ Preserve OS process ownership, descendant draining, failure/timeout semantics,
101
+ account settings, independent review, acceptance criteria and spent budgets.
102
+ - Add bounded task slices with preview, pinned plans, dependency tracking and
103
+ focused worker prompts inside one original task. Final integration retains all
104
+ original criteria/checks and a reviewer independent of every contributor.
105
+ - Protect aggregate dispatch capacity, original repair/QA/review reserves and
106
+ review limits. Preserve plan revisions, partial attempts and dependency history;
107
+ retries do not reset limits or create child/session budgets. Cost/time admission
108
+ stays unchanged.
109
+ - Qualify the two-slice and existing-file `unity-code` repair path with actual
110
+ Claude Code workers and Unity tests on 2026-10-27. The controlled regression
111
+ retains all 24 criteria, failed evidence, both independent reviews and original
112
+ total budgets. This known-component case does not qualify large-game productivity,
113
+ new-file imports, Editor asset operations or the exact published RC archive.
114
+ - Add opt-in schema-2 Unity repair packets: exact code edits/creation or pinned
115
+ Editor asset operations. Preserve schema-1 limits and historical reservations.
116
+ - Bound Editor operations to one project, fixed positional arguments, finite
117
+ foreground calls and durable single-attempt receipts. Check source scope and
118
+ existing GUIDs; import new metadata before completion. Uncertain operations
119
+ retain ownership until externally reconciled, without refunds or automatic retry.
120
+ - Unity repair still requires original measured checks, independent review and
121
+ manual/external acceptance. Synthetic tests do not qualify native Unity delivery.
122
+ - Synchronize architecture and orchestration guidance with standalone defaults;
123
+ generate native role ceilings from agent-library frontmatter alongside routing.
124
+ - Extend `task dispatch-prompt` to prepare registered implementation, analysis and
125
+ review assignments with safe PARTIAL handoff templates. Repair packets keep
126
+ their existing prompt and permissions.
127
+ - Add read-only `task handoff-validate` for a worker's own declared handoff and
128
+ coordinator inspection, so schema errors can be corrected before completion.
129
+ - Add coordinator `task record-review` to import a completed reviewer's exact
130
+ pinned result through the existing record gate. Review independence, objective
131
+ criteria, external/manual gates, task history and spent budgets are preserved.
132
+ - The scoped native qualifications above predate the final RC4 handoff/scene
133
+ additions. Their frozen source, tests and historical evidence remain separate.
134
+
3
135
  ## 2.0.0-rc.3 — 2026-10-07
4
136
 
5
137
  Published-1.3.0 upgrade correction on next; latest remains unchanged.
@@ -56,3 +188,13 @@ The game case ran a frozen framework with a session-only reviewer turn-capacity
56
188
  override. This RC packages the maintained source. Upgrade qualification from
57
189
  published 1.3.0 and a fresh end-to-end task using this exact RC are pending.
58
190
  Try a disposable project or backed-up copy; see UPGRADING.md.
191
+ # Stable preparation follow-up — 2.0.0 (unpublished)
192
+
193
+ - Correct remaining current-version RC labels while retaining historical release
194
+ records and their original qualification scope.
195
+ - Add opt-in, correlated native-result transport for read-only analysis/review
196
+ handoffs when the native client requires returned text. Copies exact output;
197
+ existing review, acceptance, source, process and budget gates still apply.
198
+ - Failed audit attempts and their consumed allowances remain in their original
199
+ history. New qualification and publication require their own explicit evidence.
200
+
package/MEMORY.md CHANGED
@@ -1,8 +1,10 @@
1
1
  # NCCGS — portable framework decisions
2
2
 
3
3
  NCCGS means **New Claude Code Game Studio** and targets **Claude Code**.
4
- This is the public 2.0.0-rc.3 release candidate, distributed through npm's next tag.
5
- It is not stable promotion. See README.md and CHANGELOG.md for release scope.
4
+ This is the unpublished 2.1.0 source, adding reversible project uninstallation.
5
+ The earlier 2.0.0 release was published on 2026-10-28; its archive is unchanged.
6
+ Local preparation does not change npm tags. Native qualification remains scoped
7
+ to its frozen archives; see README.md and CHANGELOG.md for evidence boundaries.
6
8
  Project-specific authorizations, provider sessions, budgets and internal development
7
9
  history are retained separately, outside the public package. Nothing in this file
8
10
  authorizes spending, provider access or changes to a user's project by itself.
@@ -19,6 +21,9 @@ authorizes spending, provider access or changes to a user's project by itself.
19
21
  remains unknown. Never infer execution from a requested alias or a handoff marker.
20
22
  - Preserve failed attempts, negative reviews, task history, evidence and consumed
21
23
  allowances. An update, migration, retry or new report must not reset spent budgets.
24
+ - Uninstall only from an external terminal after sessions and owned work stop.
25
+ Preserve game source, custom files and .nccgs history/budgets. Uninstall never
26
+ closes tasks, waives review/acceptance or authorizes deleting retained records.
22
27
  - Known-solution or contaminated benchmark baselines remain reference-only; do not
23
28
  score them as independent NCCGS runs.
24
29