nccgs 2.0.0-rc.2 → 2.0.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/MEMORY.md +3 -2
- package/.claude/nccgs/VERSION +1 -1
- package/.claude/nccgs/packages/ponytail/README.md +2 -2
- package/.claude/nccgs/protocols/agent-contract.md +15 -2
- package/.claude/nccgs/protocols/bounded-repair.md +135 -7
- package/.claude/nccgs/protocols/editor-session-evidence.md +52 -0
- package/.claude/nccgs/protocols/orchestration.md +7 -5
- package/.claude/nccgs/protocols/review-handoff.md +17 -1
- package/.claude/nccgs/protocols/standalone-gates.md +64 -8
- package/.claude/nccgs/protocols/standalone.md +33 -13
- package/.claude/nccgs/protocols/task-gates.md +13 -1
- package/.claude/nccgs/protocols/task-slices.md +106 -0
- package/.claude/nccgs/routing.generated.md +20 -0
- package/.claude/nccgs/tools/command-process.mjs +10 -4
- package/.claude/nccgs/tools/compile-policy.mjs +4 -0
- package/.claude/nccgs/tools/editor-session-evidence.mjs +66 -0
- package/.claude/nccgs/tools/gates.mjs +7 -4
- package/.claude/nccgs/tools/handoff.mjs +51 -0
- package/.claude/nccgs/tools/inputs.mjs +36 -1
- package/.claude/nccgs/tools/lifecycle.mjs +21 -0
- package/.claude/nccgs/tools/orchestration.mjs +264 -14
- package/.claude/nccgs/tools/repair-packet.mjs +29 -10
- package/.claude/nccgs/tools/returned-handoff.mjs +38 -0
- package/.claude/nccgs/tools/review-budget.mjs +171 -0
- package/.claude/nccgs/tools/review-handoff.mjs +42 -8
- package/.claude/nccgs/tools/runtime.mjs +7 -2
- package/.claude/nccgs/tools/session-budget.mjs +67 -0
- package/.claude/nccgs/tools/task-slices.mjs +147 -0
- package/.claude/nccgs/tools/task.mjs +39 -2
- package/.claude/nccgs/tools/tool-guard.mjs +19 -5
- package/.claude/nccgs/tools/unity-repair.mjs +128 -0
- package/.claude/nccgs/tools/verification-jobs.mjs +13 -3
- package/.claude/nccgs/tools/windows-compiler-server.cs +163 -0
- package/.claude/nccgs/tools/windows-job.cs +8 -1
- package/CHANGELOG.md +123 -0
- package/MEMORY.md +3 -2
- package/README.md +35 -27
- package/UPGRADING.md +26 -22
- package/VERSION +1 -1
- package/docs/ARCHITECTURE.md +45 -18
- package/docs/BOUNDED-REPAIR.md +76 -7
- package/docs/EDITOR-SESSION-EVIDENCE.md +52 -0
- package/docs/HANDOFF-AUTOMATION.md +144 -0
- package/docs/HUONG-DAN-MIGRATE-VA-SU-DUNG.md +40 -11
- package/docs/PHASE-1-OPERATIONS.md +142 -1
- package/docs/PHASE-2-OPERATIONS.md +1 -1
- package/docs/RELEASE-STATUS.md +92 -9
- package/docs/TASK-SLICING.md +57 -0
- package/docs/UPGRADE-QUALIFICATION.md +45 -0
- package/docs/WORKFLOWS.md +1 -1
- package/package.json +2 -2
- package/scaffold/.nccgs/templates/repair-unity-code.json +18 -0
- package/scaffold/.nccgs/templates/repair-unity-editor.json +21 -0
- package/scaffold/.nccgs/templates/task-slices.json +28 -0
- package/scripts/cli.mjs +4 -0
- package/scripts/install.mjs +10 -0
- package/scripts/run.mjs +4 -3
- package/scripts/supervise.mjs +30 -6
- package/scripts/validate.mjs +4 -0
- package/scripts/work-handoff.mjs +38 -0
- package/tests/editor-session-evidence.test.mjs +45 -0
- package/tests/fixtures/compiler-shutdown.cs +64 -0
- package/tests/framework.test.mjs +38 -0
- package/tests/orchestration-v15.test.mjs +52 -1
- package/tests/phase1.test.mjs +92 -0
- package/tests/policy-v15.test.mjs +14 -0
- package/tests/review-budget.test.mjs +177 -0
- package/tests/runtime-v15.test.mjs +572 -4
- package/tests/session-budget.test.mjs +34 -0
- package/tests/task-slices.test.mjs +285 -0
- 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
|
-
|
|
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 {
|
|
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
|
|
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
|
-
|
|
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,118 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 2.0.0 — 2026-10-28
|
|
4
|
+
|
|
5
|
+
Prepared stable package; unpublished. Local preparation does not move npm tags.
|
|
6
|
+
|
|
7
|
+
- Publish existing external operator Editor start, graceful shutdown command and
|
|
8
|
+
settled stop receipts into immutable reviewer-readable project bundles. Preserve
|
|
9
|
+
original bytes, hashes and ownership, reject inconsistent/uncertain/forced stops,
|
|
10
|
+
and identify historical imports without rewriting sealed tasks or verdicts.
|
|
11
|
+
- Keep RC4 handoff, scene guard, source controls, process management, independent
|
|
12
|
+
review, acceptance and total-budget enforcement unchanged. No new game gate or
|
|
13
|
+
process permission is introduced.
|
|
14
|
+
- Record scoped exact-RC4 native qualification: real dirty-scene refusal, new repair,
|
|
15
|
+
automatic PARTIAL handoff with no continuation, all 28 criteria/385 assertions,
|
|
16
|
+
independent PASS and normal closure. The original LOW evidence-location finding
|
|
17
|
+
remains in that sealed review; its addendum is assessed separately.
|
|
18
|
+
- Stable archive validation and independent maintenance review are recorded in
|
|
19
|
+
release receipts. They do not claim another full Unity task through these bytes.
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
## 2.0.0-rc.4 — 2026-10-28
|
|
23
|
+
|
|
24
|
+
Prepared candidate; unpublished. No npm tag or stable promotion.
|
|
25
|
+
|
|
26
|
+
- Settle a protected implementation work phase with a deterministic PARTIAL
|
|
27
|
+
report when current implementation and measured checks are ready for review.
|
|
28
|
+
A prose-only response no longer spends another continuation solely on handoff.
|
|
29
|
+
Preserve independent verdicts, all acceptance gates, history and original caps.
|
|
30
|
+
- Pin an atomic loaded-scene precondition to new Unity repair reservations.
|
|
31
|
+
Dirty named, additive or untitled scenes block the operation without auto-save
|
|
32
|
+
or discard. Retain failed attempts and old reservations; no implicit retry.
|
|
33
|
+
- Reserve final independent review money/time within the original root budget.
|
|
34
|
+
Share the work pool across design, slices, repair and verification. Admit
|
|
35
|
+
unknown worker cost only through verified, exclusive native session coverage;
|
|
36
|
+
missing session usage fails closed, and resumed cumulative cost is never summed
|
|
37
|
+
twice. The prior native reserve case qualified its disclosed new-script and
|
|
38
|
+
asset repair path with 28 criteria/385 assertions and independent final PASS.
|
|
39
|
+
- RC4-specific handoff/scene changes receive framework tests and operator Unity
|
|
40
|
+
checks. A fresh native task through this exact archive remains pending.
|
|
41
|
+
|
|
42
|
+
- Qualify the full component lifecycle on a fresh Unity project through actual
|
|
43
|
+
Claude Code: reviewed design, two dependent implementation slices from API
|
|
44
|
+
scaffolds, first-import verification, independent review, a disclosed fault,
|
|
45
|
+
typed repair and independent re-review. All 24 original criteria (349 assertions)
|
|
46
|
+
pass; the real failed PlayMode attempt remains recorded. The combined design and
|
|
47
|
+
implementation phases used 7/12 dispatches, three independent reviews and
|
|
48
|
+
USD 10.214066/12 within the original 75-minute envelope. Installed source guards,
|
|
49
|
+
process ownership, role limits, criteria and history remained unchanged. This
|
|
50
|
+
known-test component qualification does not establish broad game productivity.
|
|
51
|
+
- Add opt-in exact `unityGeneratedFiles` declarations before task initialization
|
|
52
|
+
so Unity's first import can create IDE/local metadata without a false source
|
|
53
|
+
mutation failure. Pin the declaration as a canonical input and retain output
|
|
54
|
+
before/after hashes in managed receipts. Keep all undeclared files tracked;
|
|
55
|
+
reject source paths, folders, globs and canonical overlaps. Existing policies,
|
|
56
|
+
historical attempts, process ownership, review and budgets remain unchanged.
|
|
57
|
+
- Settle cold Unity compilation without waiting for the qualified Roslyn server's
|
|
58
|
+
idle keepalive. After the command root exits and no other worker remains, request
|
|
59
|
+
graceful shutdown only from the verified compiler pipe inside the owned Windows
|
|
60
|
+
Job Object. Match the tested compiler/host binary hashes; unknown builds retain
|
|
61
|
+
the original wait/timeout behavior. Still drain every descendant and preserve the
|
|
62
|
+
command exit code, deadlines, independent review, criteria, history and budgets.
|
|
63
|
+
- Avoid Unity CLI's two-minute idle auth broker in managed verification command
|
|
64
|
+
trees using the supported process-local `UNITY_NO_AUTH_BROKER=1` setting.
|
|
65
|
+
Preserve OS process ownership, descendant draining, failure/timeout semantics,
|
|
66
|
+
account settings, independent review, acceptance criteria and spent budgets.
|
|
67
|
+
- Add bounded task slices with preview, pinned plans, dependency tracking and
|
|
68
|
+
focused worker prompts inside one original task. Final integration retains all
|
|
69
|
+
original criteria/checks and a reviewer independent of every contributor.
|
|
70
|
+
- Protect aggregate dispatch capacity, original repair/QA/review reserves and
|
|
71
|
+
review limits. Preserve plan revisions, partial attempts and dependency history;
|
|
72
|
+
retries do not reset limits or create child/session budgets. Cost/time admission
|
|
73
|
+
stays unchanged.
|
|
74
|
+
- Qualify the two-slice and existing-file `unity-code` repair path with actual
|
|
75
|
+
Claude Code workers and Unity tests on 2026-10-27. The controlled regression
|
|
76
|
+
retains all 24 criteria, failed evidence, both independent reviews and original
|
|
77
|
+
total budgets. This known-component case does not qualify large-game productivity,
|
|
78
|
+
new-file imports, Editor asset operations or the exact published RC archive.
|
|
79
|
+
- Add opt-in schema-2 Unity repair packets: exact code edits/creation or pinned
|
|
80
|
+
Editor asset operations. Preserve schema-1 limits and historical reservations.
|
|
81
|
+
- Bound Editor operations to one project, fixed positional arguments, finite
|
|
82
|
+
foreground calls and durable single-attempt receipts. Check source scope and
|
|
83
|
+
existing GUIDs; import new metadata before completion. Uncertain operations
|
|
84
|
+
retain ownership until externally reconciled, without refunds or automatic retry.
|
|
85
|
+
- Unity repair still requires original measured checks, independent review and
|
|
86
|
+
manual/external acceptance. Synthetic tests do not qualify native Unity delivery.
|
|
87
|
+
- Synchronize architecture and orchestration guidance with standalone defaults;
|
|
88
|
+
generate native role ceilings from agent-library frontmatter alongside routing.
|
|
89
|
+
- Extend `task dispatch-prompt` to prepare registered implementation, analysis and
|
|
90
|
+
review assignments with safe PARTIAL handoff templates. Repair packets keep
|
|
91
|
+
their existing prompt and permissions.
|
|
92
|
+
- Add read-only `task handoff-validate` for a worker's own declared handoff and
|
|
93
|
+
coordinator inspection, so schema errors can be corrected before completion.
|
|
94
|
+
- Add coordinator `task record-review` to import a completed reviewer's exact
|
|
95
|
+
pinned result through the existing record gate. Review independence, objective
|
|
96
|
+
criteria, external/manual gates, task history and spent budgets are preserved.
|
|
97
|
+
- The scoped native qualifications above predate the final RC4 handoff/scene
|
|
98
|
+
additions. Their frozen source, tests and historical evidence remain separate.
|
|
99
|
+
|
|
100
|
+
## 2.0.0-rc.3 — 2026-10-07
|
|
101
|
+
|
|
102
|
+
Published-1.3.0 upgrade correction on next; latest remains unchanged.
|
|
103
|
+
|
|
104
|
+
- Fixed false overrides when a role generated by a 1.3.0 profile becomes a managed
|
|
105
|
+
file in 2.0. Existing generated fingerprints now identify unchanged agent files;
|
|
106
|
+
later user edits and explicit overrides still require review.
|
|
107
|
+
- RC2 qualification exposed the issue in quality, balanced and inherit profiles.
|
|
108
|
+
The astra-claude path was unaffected. Original failed results are retained.
|
|
109
|
+
- Release qualification uses the published 1.3.0 archive and the exact candidate
|
|
110
|
+
archive across four model profiles, owner settings and open task history,
|
|
111
|
+
override preservation, dry runs, explicit execution migration, rollback and
|
|
112
|
+
refusal of changed configuration/backups. See docs/UPGRADE-QUALIFICATION.md.
|
|
113
|
+
- A new task executed by Claude Code through this exact release remains pending;
|
|
114
|
+
this correction does not promote stable or change the frozen game evaluation.
|
|
115
|
+
|
|
3
116
|
## 2.0.0-rc.2 — 2026-10-07
|
|
4
117
|
|
|
5
118
|
Release documentation follow-up on next; latest remains unchanged.
|
|
@@ -40,3 +153,13 @@ The game case ran a frozen framework with a session-only reviewer turn-capacity
|
|
|
40
153
|
override. This RC packages the maintained source. Upgrade qualification from
|
|
41
154
|
published 1.3.0 and a fresh end-to-end task using this exact RC are pending.
|
|
42
155
|
Try a disposable project or backed-up copy; see UPGRADING.md.
|
|
156
|
+
# Stable preparation follow-up — 2.0.0 (unpublished)
|
|
157
|
+
|
|
158
|
+
- Correct remaining current-version RC labels while retaining historical release
|
|
159
|
+
records and their original qualification scope.
|
|
160
|
+
- Add opt-in, correlated native-result transport for read-only analysis/review
|
|
161
|
+
handoffs when the native client requires returned text. Copies exact output;
|
|
162
|
+
existing review, acceptance, source, process and budget gates still apply.
|
|
163
|
+
- Failed audit attempts and their consumed allowances remain in their original
|
|
164
|
+
history. New qualification and publication require their own explicit evidence.
|
|
165
|
+
|
package/MEMORY.md
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
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
|
|
5
|
-
|
|
4
|
+
This is the prepared 2.0.0 stable package. It has not been published.
|
|
5
|
+
Local preparation does not change npm tags. Native RC4 qualification is scoped
|
|
6
|
+
to its frozen archive; see README.md and CHANGELOG.md for evidence boundaries.
|
|
6
7
|
Project-specific authorizations, provider sessions, budgets and internal development
|
|
7
8
|
history are retained separately, outside the public package. Nothing in this file
|
|
8
9
|
authorizes spending, provider access or changes to a user's project by itself.
|