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
|
@@ -6,9 +6,15 @@ import { spawn, spawnSync } from "node:child_process";
|
|
|
6
6
|
import { fileURLToPath } from "node:url";
|
|
7
7
|
import test from "node:test";
|
|
8
8
|
import { initializeStandalone, recordStandalone } from "../.claude/nccgs/tools/lifecycle.mjs";
|
|
9
|
-
import { reserve, status, rejectUnclaimed, dispatchPrompt } from "../.claude/nccgs/tools/orchestration.mjs";
|
|
9
|
+
import { reserve, status, rejectUnclaimed, dispatchPrompt, runRepairEditor, reconcile } from "../.claude/nccgs/tools/orchestration.mjs";
|
|
10
|
+
import { repairEditorCommand, unityEditorInvocation } from "../.claude/nccgs/tools/unity-repair.mjs";
|
|
11
|
+
import { settleWorkHandoff } from "../scripts/work-handoff.mjs";
|
|
12
|
+
import { supervise } from "../scripts/supervise.mjs";
|
|
13
|
+
import { registerReviewBudget, reviewBudgetStatus } from "../.claude/nccgs/tools/review-budget.mjs";
|
|
14
|
+
import { startVerification, waitVerification } from "../.claude/nccgs/tools/verification-jobs.mjs";
|
|
10
15
|
import { snapshot } from "../.claude/nccgs/tools/gates.mjs";
|
|
11
16
|
import { observe } from "../.claude/nccgs/tools/runtime.mjs";
|
|
17
|
+
import { captureReturnedHandoff } from "../.claude/nccgs/tools/returned-handoff.mjs";
|
|
12
18
|
import { guardTool } from "../.claude/nccgs/tools/tool-guard.mjs";
|
|
13
19
|
import { artifact, json } from "../.claude/nccgs/tools/policy.mjs";
|
|
14
20
|
import { run as taskRun } from "../.claude/nccgs/tools/task.mjs";
|
|
@@ -16,7 +22,7 @@ import { run as taskRun } from "../.claude/nccgs/tools/task.mjs";
|
|
|
16
22
|
// All envelopes, source files, model identifiers and measurements below are
|
|
17
23
|
// SYNTHETIC TEST FIXTURES. They do not establish actual Claude Code execution.
|
|
18
24
|
const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
|
19
|
-
function fixture(mode = "implement", reviewContractRequired = false) {
|
|
25
|
+
function fixture(mode = "implement", reviewContractRequired = false, intakeOverrides = {}) {
|
|
20
26
|
const temporaryRoot = fs.realpathSync(os.tmpdir());
|
|
21
27
|
const project = fs.mkdtempSync(path.join(temporaryRoot, "nccgs-runtime-v15-"));
|
|
22
28
|
const write = (relative, value) => {
|
|
@@ -31,7 +37,7 @@ function fixture(mode = "implement", reviewContractRequired = false) {
|
|
|
31
37
|
write("Assets/fixture.cs", "// SYNTHETIC TEST SOURCE; no game implementation.\n");
|
|
32
38
|
const analysis = ".nccgs/evidence/analysis/synthetic-result.md";
|
|
33
39
|
write(".nccgs/intake.json", { schemaVersion: 1, mode, objective: "SYNTHETIC TEST: inspect bounded fixture behavior", scope: "feature", target: "production-change", readiness: "ready",
|
|
34
|
-
requiredChecks: ["synthetic-check"], analysisArtifacts: [analysis, ".nccgs/evidence/analysis/synthetic-review.md"], acceptance: [] });
|
|
40
|
+
requiredChecks: ["synthetic-check"], analysisArtifacts: [analysis, ".nccgs/evidence/analysis/synthetic-review.md"], acceptance: [], ...intakeOverrides });
|
|
35
41
|
initializeStandalone(project, "synthetic", { intake: ".nccgs/intake.json" });
|
|
36
42
|
write(".nccgs/runtime/bindings/synthetic-session.json", { taskId: "synthetic" });
|
|
37
43
|
const verification = write(".nccgs/evidence/synthetic-check.txt", "SYNTHETIC TEST: assertion fixture, not an actual game or provider check.\n");
|
|
@@ -71,6 +77,99 @@ function fixture(mode = "implement", reviewContractRequired = false) {
|
|
|
71
77
|
} };
|
|
72
78
|
}
|
|
73
79
|
|
|
80
|
+
test("opt-in returned analysis is captured verbatim and pins native identity without closing audit", () => {
|
|
81
|
+
const f = fixture("audit", true);
|
|
82
|
+
try {
|
|
83
|
+
const run = f.dispatch({ agent: "nccgs-engine-programmer", handoffPaths: [f.analysis], handoffTransport: "native-result" });
|
|
84
|
+
assert.match(dispatchPrompt(f.project, "synthetic", run.reservation.id).prompt, /sole final text response/);
|
|
85
|
+
f.start(run);
|
|
86
|
+
assert.throws(() => guardTool(f.project, { session_id: "synthetic-session", agent_id: run.agentId, tool_name: "Write",
|
|
87
|
+
tool_input: { file_path: path.join(f.project, f.analysis), content: "forged" } }), /captured only/);
|
|
88
|
+
const text = "NCCGS-HANDOFF: COMPLETE\n\nSYNTHETIC factual analysis. No source edits; not independent review.\n";
|
|
89
|
+
const envelope = f.result(run, { content: [{ type: "text", text }] });
|
|
90
|
+
const observation = observe(f.project, envelope, "agent_result");
|
|
91
|
+
assert.equal(fs.readFileSync(path.join(f.project, f.analysis), "utf8"), text);
|
|
92
|
+
assert.equal(f.state().reservations[0].status, "completed");
|
|
93
|
+
assert.deepEqual(observation.returnedHandoff.evidence, artifact(f.project, f.analysis));
|
|
94
|
+
recordStandalone(f.project, f.task(), "analysis", { ...f.stage(run, observation), evidence: artifact(f.project, f.analysis) });
|
|
95
|
+
assert.ok(!f.task().closed);
|
|
96
|
+
assert.ok(observe(f.project, envelope, "agent_result").duplicate);
|
|
97
|
+
const reviewPath = ".nccgs/evidence/analysis/synthetic-review.md";
|
|
98
|
+
const review = f.dispatch({ kind: "review", agent: "nccgs-adversarial-reviewer", writePaths: [], handoffPaths: [reviewPath], handoffTransport: "native-result" });
|
|
99
|
+
f.start(review);
|
|
100
|
+
const verdict = { schemaVersion: 1, taskId: "synthetic", contractHash: f.task().contractHash, snapshot: review.reservation.snapshot,
|
|
101
|
+
verdict: "BLOCKED", criteriaMet: false, findings: [], conditions: [{ id: "R1", description: "Synthetic required check remains unresolved." }] };
|
|
102
|
+
const reviewText = "NCCGS-HANDOFF: COMPLETE\n\n```nccgs-review\n" + JSON.stringify(verdict) + "\n```\n";
|
|
103
|
+
const observedReview = observe(f.project, f.result(review, { content: [{ type: "text", text: reviewText }] }), "agent_result");
|
|
104
|
+
assert.equal(fs.readFileSync(path.join(f.project, reviewPath), "utf8"), reviewText);
|
|
105
|
+
assert.equal(f.state().reservations[1].status, "completed");
|
|
106
|
+
assert.throws(() => recordStandalone(f.project, f.task(), "review", { ...f.stage(review, observedReview), evidence: artifact(f.project, reviewPath),
|
|
107
|
+
...verdict, verdict: "PASS", criteriaMet: true, conditions: [] }), /differs from.*handoff/);
|
|
108
|
+
assert.ok(!f.task().closed);
|
|
109
|
+
} finally { f.dispose(); }
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
for (const scenario of ["partial", "missing", "multiple", "oversized", "incomplete", "existing", "source-change"]) test(`returned handoff fails closed: ${scenario}`, () => {
|
|
113
|
+
const f = fixture("audit", true);
|
|
114
|
+
try {
|
|
115
|
+
const run = f.dispatch({ agent: "nccgs-engine-programmer", handoffPaths: [f.analysis], handoffTransport: "native-result" }); f.start(run);
|
|
116
|
+
const text = "NCCGS-HANDOFF: COMPLETE\nSYNTHETIC returned content.\n";
|
|
117
|
+
let content = [{ type: "text", text }], extra = {};
|
|
118
|
+
if (scenario === "partial") content[0].text = text.replace("COMPLETE", "PARTIAL");
|
|
119
|
+
if (scenario === "missing") content = "not a supported native content envelope";
|
|
120
|
+
if (scenario === "multiple") content.push({ type: "text", text: "extra" });
|
|
121
|
+
if (scenario === "oversized") content[0].text += "x".repeat(262144);
|
|
122
|
+
if (scenario === "incomplete") extra = { harnessNoteCount: 1 };
|
|
123
|
+
if (scenario === "existing") f.write(f.analysis, "NCCGS-HANDOFF: COMPLETE\nPreserved existing bytes.\n");
|
|
124
|
+
if (scenario === "source-change") f.write("Assets/fixture.cs", "// SYNTHETIC forbidden mutation.\n");
|
|
125
|
+
observe(f.project, f.result(run, { content, ...extra }), "agent_result");
|
|
126
|
+
assert.equal(f.state().reservations[0].status, "failed");
|
|
127
|
+
if (scenario === "existing") assert.match(fs.readFileSync(path.join(f.project, f.analysis), "utf8"), /Preserved existing/);
|
|
128
|
+
else if (scenario !== "partial") assert.ok(!fs.existsSync(path.join(f.project, f.analysis)));
|
|
129
|
+
assert.ok(!f.task().closed);
|
|
130
|
+
} finally { f.dispose(); }
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
test("returned handoff cannot be requested for source writers, multiple paths or unknown transport", () => {
|
|
134
|
+
const f = fixture();
|
|
135
|
+
try {
|
|
136
|
+
assert.throws(() => f.dispatch({ handoffPaths: [f.analysis], handoffTransport: "native-result" }), /read-only artifact/);
|
|
137
|
+
assert.throws(() => f.dispatch({ handoffTransport: "invented" }), /Unknown handoff transport/);
|
|
138
|
+
assert.equal(f.state().usage.dispatches, 0);
|
|
139
|
+
} finally { f.dispose(); }
|
|
140
|
+
const a = fixture("audit");
|
|
141
|
+
try {
|
|
142
|
+
assert.throws(() => a.dispatch({ handoffTransport: "native-result", handoffPaths: [a.analysis, ".nccgs/evidence/analysis/synthetic-review.md"] }), /one declared/);
|
|
143
|
+
assert.equal(a.state().usage.dispatches, 0);
|
|
144
|
+
} finally { a.dispose(); }
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
test("native-result transport rejects mismatched hook identities without publishing", () => {
|
|
148
|
+
const f = fixture("audit");
|
|
149
|
+
try {
|
|
150
|
+
const run = f.dispatch({ handoffPaths: [f.analysis], handoffTransport: "native-result" }); f.start(run);
|
|
151
|
+
const dispatch = f.state().reservations[0], base = f.result(run, { content: [{ type: "text", text: "NCCGS-HANDOFF: COMPLETE\nSYNTHETIC" }] });
|
|
152
|
+
for (const field of ["session_id", "tool_use_id", "hook_event_name", "tool_name"]) {
|
|
153
|
+
const result = captureReturnedHandoff(f.project, dispatch, { ...base, [field]: "incorrect" }, { status: "completed" }, dispatch.snapshot);
|
|
154
|
+
assert.equal(result.status, "rejected");
|
|
155
|
+
}
|
|
156
|
+
for (const envelope of [{ ...base, tool_response: { ...base.tool_response, agentId: "wrong" } },
|
|
157
|
+
{ ...base, tool_input: { ...base.tool_input, subagent_type: "wrong" } }])
|
|
158
|
+
assert.equal(captureReturnedHandoff(f.project, dispatch, envelope, { status: "completed" }, dispatch.snapshot).status, "rejected");
|
|
159
|
+
assert.ok(!fs.existsSync(path.join(f.project, f.analysis)));
|
|
160
|
+
} finally { f.dispose(); }
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
test("legacy file transport never captures arbitrary native response content", () => {
|
|
164
|
+
const f = fixture("audit");
|
|
165
|
+
try {
|
|
166
|
+
const run = f.dispatch({ handoffPaths: [f.analysis] }); f.start(run);
|
|
167
|
+
observe(f.project, f.result(run, { content: [{ type: "text", text: "NCCGS-HANDOFF: COMPLETE\nSYNTHETIC" }] }), "agent_result");
|
|
168
|
+
assert.equal(f.state().reservations[0].status, "failed");
|
|
169
|
+
assert.ok(!fs.existsSync(path.join(f.project, f.analysis)));
|
|
170
|
+
} finally { f.dispose(); }
|
|
171
|
+
});
|
|
172
|
+
|
|
74
173
|
test("synthetic foreground native result completes a reserved dispatch and records lifecycle output", () => {
|
|
75
174
|
const f = fixture();
|
|
76
175
|
try {
|
|
@@ -312,6 +411,379 @@ function repairCall(f, run, id, name = "Read", tool = { file_path: "Assets/fixtu
|
|
|
312
411
|
return { hook_event_name: "PreToolUse", session_id: "synthetic-session", agent_id: run.agentId, tool_use_id: id, tool_name: name, tool_input: tool };
|
|
313
412
|
}
|
|
314
413
|
|
|
414
|
+
function prepareUnityRepair(f, type = "unity-code", create = false, steps = 1) {
|
|
415
|
+
f.write("ProjectSettings/ProjectVersion.txt", "m_EditorVersion: SYNTHETIC-FIXTURE\n");
|
|
416
|
+
f.write("Assets/fixture.cs.meta", "fileFormatVersion: 2\nguid: 11111111111111111111111111111111\n");
|
|
417
|
+
const target = type === "unity-editor" ? "Assets/Fixture.prefab" : create ? "Assets/Regression.cs" : "Assets/fixture.cs";
|
|
418
|
+
if (type === "unity-editor" && !create) {
|
|
419
|
+
f.write(target, "%YAML SYNTHETIC FIXTURE; not a real Unity prefab\n");
|
|
420
|
+
f.write(`${target}.meta`, "fileFormatVersion: 2\nguid: 22222222222222222222222222222222\n");
|
|
421
|
+
}
|
|
422
|
+
const capabilities = f.write(".nccgs/evidence/unity-capabilities.txt", "SYNTHETIC CLI catalog fixture; no Unity or provider execution. synthetic_operation(positionals)\n");
|
|
423
|
+
const old = prepareRepair(f);
|
|
424
|
+
const packet = { ...old.packet, schemaVersion: 2, repairType: type,
|
|
425
|
+
changes: [{ path: target, operation: create ? "create" : "edit", instruction: "Apply only the synthetic fixture change" }],
|
|
426
|
+
unity: { capabilities, steps: Array.from({ length: steps }, (_, i) => ({ id: `step-${i + 1}`, command: "eval", arguments: ['return "SYNTHETIC ONLY";'], timeoutMs: 1000 })) } };
|
|
427
|
+
f.write(old.request.workPacket, packet);
|
|
428
|
+
return { packet, target, request: { ...old.request, writePaths: [target] } };
|
|
429
|
+
}
|
|
430
|
+
function editorCall(f, run, step, id = step.id) {
|
|
431
|
+
return repairCall(f, run, id, "Bash", { command: repairEditorCommand(run.reservation, step), timeout: step.timeoutMs + 5000 });
|
|
432
|
+
}
|
|
433
|
+
const syntheticEditorSuccess = () => ({ status: 0, stdout: '{"synthetic":true}', stderr: "" });
|
|
434
|
+
|
|
435
|
+
test("dirty-scene refusal is a retained failed attempt, with no automatic retry, source change or review waiver", () => {
|
|
436
|
+
const f = fixture();
|
|
437
|
+
try {
|
|
438
|
+
const { packet, request, target } = prepareUnityRepair(f, "unity-editor"), run = f.dispatch(request); f.start(run);
|
|
439
|
+
const before = fs.readFileSync(path.join(f.project, target));
|
|
440
|
+
guardTool(f.project, editorCall(f, run, packet.unity.steps[0]));
|
|
441
|
+
const receipt = runRepairEditor(f.project, "synthetic", run.reservation.id, "step-1", (exe, args) => {
|
|
442
|
+
const token = args[2].match(/token = "([a-f0-9-]+)"/)[1];
|
|
443
|
+
return { status: 0, stdout: JSON.stringify({ success: true, data: { result: { success: true,
|
|
444
|
+
result: { nccgsSceneGuard: "loaded-scenes-clean-v1", token, blockedScene: "<untitled>" } } } }), stderr: "" };
|
|
445
|
+
});
|
|
446
|
+
assert.equal(receipt.status, "FAIL"); assert.equal(receipt.operationStatus, "failed");
|
|
447
|
+
assert.match(receipt.message, /Unsaved scene/);
|
|
448
|
+
assert.equal(json(path.join(f.project, receipt.evidence.path)).blockedScene, "<untitled>");
|
|
449
|
+
assert.deepEqual(fs.readFileSync(path.join(f.project, target)), before);
|
|
450
|
+
assert.throws(() => runRepairEditor(f.project, "synthetic", run.reservation.id, "step-1", syntheticEditorSuccess), /already attempted/);
|
|
451
|
+
f.write(request.handoffPaths[0], "NCCGS-HANDOFF: PARTIAL\nUnsaved scene; owner action needed.");
|
|
452
|
+
observe(f.project, f.result(run), "agent_result");
|
|
453
|
+
assert.equal(f.state().usage.dispatches, 3);
|
|
454
|
+
assert.equal(f.state().active.length, 0);
|
|
455
|
+
assert.equal(f.task().stages.review.verdict, "CHANGES_REQUIRED");
|
|
456
|
+
} finally { f.dispose(); }
|
|
457
|
+
});
|
|
458
|
+
|
|
459
|
+
test("previously pinned Unity reservations retain their original operation without retroactive guard injection", () => {
|
|
460
|
+
const f = fixture();
|
|
461
|
+
try {
|
|
462
|
+
const { packet, request } = prepareUnityRepair(f), run = f.dispatch(request);
|
|
463
|
+
const file = path.join(f.project, ".nccgs/orchestration.json"), ledger = json(file);
|
|
464
|
+
delete ledger.dispatches[run.reservation.id].workPacket.sceneSafety; // Pre-RC4 reservation fixture.
|
|
465
|
+
fs.writeFileSync(file, JSON.stringify(ledger)); f.start(run);
|
|
466
|
+
guardTool(f.project, editorCall(f, run, packet.unity.steps[0]));
|
|
467
|
+
const receipt = runRepairEditor(f.project, "synthetic", run.reservation.id, "step-1", (exe, args) => {
|
|
468
|
+
assert.equal(args[2], packet.unity.steps[0].arguments[0]); return syntheticEditorSuccess();
|
|
469
|
+
});
|
|
470
|
+
assert.equal(receipt.operationStatus, "completed");
|
|
471
|
+
assert.equal(json(path.join(f.project, receipt.evidence.path)).sceneGuard, undefined);
|
|
472
|
+
assert.throws(() => unityEditorInvocation(f.project, packet.unity.steps[0], "unknown-policy"), /unknown pinned/);
|
|
473
|
+
} finally { f.dispose(); }
|
|
474
|
+
});
|
|
475
|
+
|
|
476
|
+
test("work handoff records PARTIAL only with current output and never changes review, acceptance, history or spent quota", () => {
|
|
477
|
+
for (const variant of ["ready", "stale", "failed-check", "unmeasured", "reported", "no-allocation", "final-review", "unknown-cost", "active"]) {
|
|
478
|
+
const f = fixture("implement", false, { manualGates: ["OWNER-01"], ...(variant === "unmeasured" ? {
|
|
479
|
+
acceptance: [{ id: "UNMEASURED", check: "synthetic-check", description: "Missing measured evidence" }]
|
|
480
|
+
} : {}) });
|
|
481
|
+
try {
|
|
482
|
+
const work = f.dispatch(); f.start(work); const observed = observe(f.project, f.result(work), "agent_result");
|
|
483
|
+
recordStandalone(f.project, f.task(), "implementation", f.stage(work, observed));
|
|
484
|
+
const state = { id: "synthetic-run", taskId: "synthetic", contractHash: f.task().contractHash, createdAt: f.task().createdAt,
|
|
485
|
+
budgetAllocation: { synthetic: true }, budgetPhase: "work", costCoverage: "complete", invocations: [{ status: "finished", code: 0, terminal: { type: "result" } }] };
|
|
486
|
+
if (variant === "stale") f.write("Assets/fixture.cs", "// changed after verification");
|
|
487
|
+
if (variant === "failed-check") {
|
|
488
|
+
const task = f.task();
|
|
489
|
+
task.stages.implementation.checks[0].status = "FAIL";
|
|
490
|
+
f.write(".nccgs/tasks/synthetic.json", task);
|
|
491
|
+
}
|
|
492
|
+
if (variant === "reported") recordStandalone(f.project, f.task(), "report", { outcome: "blocked", summary: "Preserve explicit coordinator assessment" });
|
|
493
|
+
if (variant === "no-allocation") delete state.budgetAllocation;
|
|
494
|
+
if (variant === "final-review") state.budgetPhase = "final-review";
|
|
495
|
+
if (variant === "unknown-cost") state.costCoverage = "unknown-current-invocation";
|
|
496
|
+
if (variant === "active") f.dispatch({ kind: "review", agent: "nccgs-adversarial-reviewer", writePaths: [] });
|
|
497
|
+
const before = f.task(), ledger = fs.readFileSync(path.join(f.project, ".nccgs/orchestration.json"));
|
|
498
|
+
const result = settleWorkHandoff(f.project, state);
|
|
499
|
+
if (variant === "ready") {
|
|
500
|
+
assert.ok(result); assert.equal(f.task().outcome, "partial");
|
|
501
|
+
assert.equal(f.task().stages.report.origin, "supervisor-work-handoff");
|
|
502
|
+
assert.equal(f.task().stages.review, undefined); assert.equal(f.task().closed, undefined);
|
|
503
|
+
assert.deepEqual(f.task().contract, before.contract); assert.deepEqual(f.task().reviewBudget, before.reviewBudget);
|
|
504
|
+
assert.deepEqual(f.task().history.slice(0, before.history.length), before.history);
|
|
505
|
+
assert.equal(settleWorkHandoff(f.project, state), null);
|
|
506
|
+
assert.throws(() => taskRun(["close", "--project", f.project, "--id", "synthetic"]), /review.*missing|stale/i);
|
|
507
|
+
} else { assert.equal(result, null, variant); assert.deepEqual(f.task(), before, variant); }
|
|
508
|
+
assert.deepEqual(fs.readFileSync(path.join(f.project, ".nccgs/orchestration.json")), ledger);
|
|
509
|
+
} finally { f.dispose(); }
|
|
510
|
+
}
|
|
511
|
+
});
|
|
512
|
+
|
|
513
|
+
test("protected work supervisor settles a prose-only PARTIAL without spending a continuation or final-review reserve", async () => {
|
|
514
|
+
const f = fixture();
|
|
515
|
+
try {
|
|
516
|
+
fs.cpSync(path.join(root, ".claude/nccgs/tools"), path.join(f.project, ".claude/nccgs/tools"), { recursive: true });
|
|
517
|
+
f.write(".nccgs/evidence/budget.json", { schemaVersion: 1, authority: "Synthetic fixture only", totalUsd: 1, finalReviewUsd: 0.4, totalDurationMs: 60000, finalReviewDurationMs: 10000 });
|
|
518
|
+
registerReviewBudget(f.project, "synthetic", ".nccgs/evidence/budget.json");
|
|
519
|
+
f.write(".nccgs/native-fixture.mjs", `import {reserve} from '../.claude/nccgs/tools/orchestration.mjs';
|
|
520
|
+
import {guardTool} from '../.claude/nccgs/tools/tool-guard.mjs';
|
|
521
|
+
import {observe} from '../.claude/nccgs/tools/runtime.mjs';
|
|
522
|
+
import {record} from '../.claude/nccgs/tools/gates.mjs';
|
|
523
|
+
import {artifact} from '../.claude/nccgs/tools/policy.mjs';
|
|
524
|
+
const project=process.cwd(), session=process.env.NCCGS_SUPERVISED_SESSION;
|
|
525
|
+
const d=reserve(project,'synthetic',{kind:'implementation',agent:'nccgs-gameplay-programmer',requestKey:'once',reason:'SYNTHETIC',writePaths:['Assets/fixture.cs']});
|
|
526
|
+
const input={session_id:session,tool_name:'Agent',tool_use_id:'synthetic-tool',tool_input:{subagent_type:d.agent,prompt:'[NCCGS-DISPATCH:'+d.id+'] SYNTHETIC'}};
|
|
527
|
+
guardTool(project,input);observe(project,{session_id:session,agent_id:'synthetic-worker',agent_type:d.agent},'agent_start');
|
|
528
|
+
const o=observe(project,{...input,tool_response:{agentId:'synthetic-worker',status:'completed',resolvedModel:'claude-opus-5-5',totalDurationMs:123,totalToolUseCount:2,totalTokens:456}},'agent_result');
|
|
529
|
+
record(project,'synthetic','implementation',{snapshot:o.snapshot,dispatchId:d.id,runtime:artifact(project,o.path),checks:[{name:'synthetic-check',status:'PASS',evidence:artifact(project,'.nccgs/evidence/synthetic-check.txt')}]});
|
|
530
|
+
console.log(JSON.stringify({type:'result',session_id:session,total_cost_usd:0.2,result:'PARTIAL: ready for independent review. Synthetic provider fixture only.'}));`);
|
|
531
|
+
f.write(".nccgs/prompt.txt", "SYNTHETIC provider integration test; no Claude execution");
|
|
532
|
+
const env = { ...process.env }; for (const key of ["NCCGS_SUPERVISED_RUN", "NCCGS_SUPERVISED_SESSION", "NCCGS_TASK_ID", "CLAUDECODE"]) delete env[key];
|
|
533
|
+
const plan = { project: f.project, binary: process.execPath, mode: "full", args: [path.join(f.project, ".nccgs/native-fixture.mjs")], env,
|
|
534
|
+
supervision: { taskId: "synthetic", promptFile: path.join(f.project, ".nccgs/prompt.txt"), budgetUsd: 1, maxResumes: 0, timeoutMs: 20000, budgetPhase: "work" } };
|
|
535
|
+
assert.equal(await supervise(plan), 0);
|
|
536
|
+
const id = fs.readdirSync(path.join(f.project, ".nccgs/runs"), { withFileTypes: true }).find(e => e.isDirectory()).name;
|
|
537
|
+
const run = json(path.join(f.project, `.nccgs/runs/${id}/run.json`));
|
|
538
|
+
assert.equal(run.invocations.length, 1); assert.equal(run.cumulativeCostUsd, 0.2); assert.equal(run.taskOutcome, "partial");
|
|
539
|
+
assert.equal(run.workHandoff.origin, "supervisor-work-handoff"); assert.equal(f.task().stages.review, undefined);
|
|
540
|
+
assert.equal(reviewBudgetStatus(f.project, "synthetic").protectedReviewUsd, 0.4);
|
|
541
|
+
const history = f.task().history;
|
|
542
|
+
assert.equal(await supervise({ ...plan, supervision: { recoverRun: id } }), 0);
|
|
543
|
+
assert.deepEqual(f.task().history, history); assert.equal(f.state().usage.dispatches, 1);
|
|
544
|
+
} finally { f.dispose(); }
|
|
545
|
+
});
|
|
546
|
+
|
|
547
|
+
test("work handoff rejects the latest failed check even when an older PASS was recorded again", () => {
|
|
548
|
+
const f = fixture();
|
|
549
|
+
try {
|
|
550
|
+
const state = { id: "synthetic-run", taskId: "synthetic", contractHash: f.task().contractHash, createdAt: f.task().createdAt,
|
|
551
|
+
budgetAllocation: { synthetic: true }, budgetPhase: "work", costCoverage: "complete", invocations: [{ status: "finished", code: 0, terminal: { type: "result" } }] };
|
|
552
|
+
for (const exitCode of [1, 0]) {
|
|
553
|
+
const work = f.dispatch(); f.start(work); const observed = observe(f.project, f.result(work), "agent_result");
|
|
554
|
+
const started = startVerification(f.project, "synthetic", { name: "synthetic-check", executable: process.execPath, arguments: ["-e", `process.exit(${exitCode})`] });
|
|
555
|
+
const receipt = waitVerification(f.project, started.jobId, "synthetic", 10000);
|
|
556
|
+
assert.equal(receipt.status, exitCode ? "FAIL" : "PASS");
|
|
557
|
+
recordStandalone(f.project, f.task(), "implementation", f.stage(work, observed));
|
|
558
|
+
const report = settleWorkHandoff(f.project, state);
|
|
559
|
+
if (exitCode) assert.equal(report, null); else assert.ok(report);
|
|
560
|
+
}
|
|
561
|
+
} finally { f.dispose(); }
|
|
562
|
+
});
|
|
563
|
+
|
|
564
|
+
test("Unity repair creates code and imports metadata before completion, retaining negative history and manual gates", () => {
|
|
565
|
+
const f = fixture("implement", false, { manualGates: ["OWNER-01"] });
|
|
566
|
+
try {
|
|
567
|
+
const { packet, request, target } = prepareUnityRepair(f, "unity-code", true);
|
|
568
|
+
const original = f.task(), feedback = fs.readFileSync(path.join(f.project, f.analysis));
|
|
569
|
+
const run = f.dispatch(request); f.start(run);
|
|
570
|
+
assert.equal(run.reservation.workPacket.budget.workCalls, 24);
|
|
571
|
+
assert.equal(run.reservation.workPacket.budget.readCalls, 12);
|
|
572
|
+
assert.match(dispatchPrompt(f.project, "synthetic", run.reservation.id).prompt, /Never hand-write \.meta/);
|
|
573
|
+
assert.throws(() => runRepairEditor(f.project, "synthetic", run.reservation.id, "step-1", syntheticEditorSuccess), /tool admission/);
|
|
574
|
+
guardTool(f.project, repairCall(f, run, "new-code", "Write", { file_path: target }));
|
|
575
|
+
f.write(target, "// SYNTHETIC new test source; no Unity execution\n");
|
|
576
|
+
const call = editorCall(f, run, packet.unity.steps[0]);
|
|
577
|
+
guardTool(f.project, call);
|
|
578
|
+
const result = runRepairEditor(f.project, "synthetic", run.reservation.id, "step-1", (executable, args, options) => {
|
|
579
|
+
assert.match(executable, /^unity(?:\.exe)?$/); assert.equal(options.shell, false); assert.equal(options.windowsHide, true);
|
|
580
|
+
assert.equal(args[args.indexOf("--project-path") + 1], fs.realpathSync(f.project));
|
|
581
|
+
assert.deepEqual(args.slice(0, 2), ["command", "eval"]);
|
|
582
|
+
assert.match(args[2], /GetSceneAt.*isDirty/s);
|
|
583
|
+
assert.ok(args[2].endsWith('return "SYNTHETIC ONLY";'));
|
|
584
|
+
f.write(`${target}.meta`, "fileFormatVersion: 2\nguid: 33333333333333333333333333333333\n");
|
|
585
|
+
return syntheticEditorSuccess();
|
|
586
|
+
});
|
|
587
|
+
assert.equal(result.status, "EXECUTED");
|
|
588
|
+
assert.equal(json(path.join(f.project, result.evidence.path)).origin, "unity-repair-operation");
|
|
589
|
+
f.write(request.handoffPaths[0], "NCCGS-HANDOFF: COMPLETE\nSynthetic edit/import completed; verification remains separate.");
|
|
590
|
+
const observed = observe(f.project, f.result(run), "agent_result");
|
|
591
|
+
assert.equal(f.state().reservations.at(-1).status, "completed");
|
|
592
|
+
assert.throws(() => recordStandalone(f.project, f.task(), "implementation", { ...f.stage(run, observed),
|
|
593
|
+
checks: [{ name: "synthetic-check", status: "PASS", evidence: result.evidence }] }), /operation receipts are not verification/);
|
|
594
|
+
recordStandalone(f.project, f.task(), "implementation", f.stage(run, observed));
|
|
595
|
+
assert.deepEqual(f.task().contract, original.contract);
|
|
596
|
+
assert.equal(f.task().reviewBudget.used, original.reviewBudget.used);
|
|
597
|
+
assert.ok(f.task().history.some(h => h.stages?.review?.verdict === "CHANGES_REQUIRED"));
|
|
598
|
+
assert.deepEqual(fs.readFileSync(path.join(f.project, f.analysis)), feedback);
|
|
599
|
+
assert.throws(() => recordStandalone(f.project, f.task(), "report", { outcome: "success", summary: "Cannot skip fresh review" }), /review.*missing|stale/i);
|
|
600
|
+
const review = f.dispatch({ kind: "review", agent: "nccgs-adversarial-reviewer", writePaths: [] }); f.start(review);
|
|
601
|
+
const reviewed = observe(f.project, f.result(review), "agent_result");
|
|
602
|
+
const evidence = f.write(".nccgs/evidence/analysis/synthetic-review.md", "SYNTHETIC independent review, not actual provider evidence");
|
|
603
|
+
recordStandalone(f.project, f.task(), "review", { snapshot: reviewed.snapshot, dispatchId: review.reservation.id,
|
|
604
|
+
runtime: artifact(f.project, reviewed.path), verdict: "PASS", criteriaMet: true, findings: [], evidence });
|
|
605
|
+
recordStandalone(f.project, f.task(), "report", { outcome: "success", summary: "Synthetic technical pass; manual decision pending" });
|
|
606
|
+
assert.throws(() => taskRun(["close", "--project", f.project, "--id", "synthetic"]), /acceptance: missing/);
|
|
607
|
+
assert.equal(f.state().usage.dispatches, 4); assert.equal(f.task().reviewBudget.used, original.reviewBudget.used + 1);
|
|
608
|
+
} finally { f.dispose(); }
|
|
609
|
+
});
|
|
610
|
+
|
|
611
|
+
test("Unity repair Editor operations are pinned, ordered, exclusive and never replayed", () => {
|
|
612
|
+
const f = fixture();
|
|
613
|
+
try {
|
|
614
|
+
const { packet, request, target } = prepareUnityRepair(f, "unity-editor", false, 2);
|
|
615
|
+
packet.changes.push({ path: "Assets/fixture.cs", operation: "edit", instruction: "SYNTHETIC paired code correction for the prefab" });
|
|
616
|
+
request.writePaths.push("Assets/fixture.cs"); f.write(request.workPacket, packet);
|
|
617
|
+
const run = f.dispatch(request); f.start(run);
|
|
618
|
+
guardTool(f.project, repairCall(f, run, "paired-code", "Edit", { file_path: "Assets/fixture.cs" }));
|
|
619
|
+
f.write("Assets/fixture.cs", "// SYNTHETIC code correction paired with the prefab\n");
|
|
620
|
+
assert.throws(() => f.dispatch({ writePaths: ["Assets/elsewhere.cs"] }), /owns the project/);
|
|
621
|
+
for (const input of [
|
|
622
|
+
repairCall(f, run, "raw", "Write", { file_path: target }),
|
|
623
|
+
repairCall(f, run, "meta", "Edit", { file_path: `${target}.meta` }),
|
|
624
|
+
repairCall(f, run, "shell", "Bash", { command: "unity command eval evil", timeout: 6000 }),
|
|
625
|
+
repairCall(f, run, "mcp", "mcp__unityMCP__execute_code", { code: "unbounded" }),
|
|
626
|
+
{ ...editorCall(f, run, packet.unity.steps[0], "background"), tool_input: { ...editorCall(f, run, packet.unity.steps[0]).tool_input, run_in_background: true } },
|
|
627
|
+
{ ...editorCall(f, run, packet.unity.steps[0], "chain"), tool_input: { command: repairEditorCommand(run.reservation, packet.unity.steps[0]) + "; echo bad", timeout: 6000 } },
|
|
628
|
+
{ ...editorCall(f, run, packet.unity.steps[0], "short"), tool_input: { command: repairEditorCommand(run.reservation, packet.unity.steps[0]), timeout: 1000 } }
|
|
629
|
+
]) assert.throws(() => guardTool(f.project, input));
|
|
630
|
+
assert.throws(() => runRepairEditor(f.project, "synthetic", run.reservation.id, "step-2", syntheticEditorSuccess), /packet order/);
|
|
631
|
+
guardTool(f.project, editorCall(f, run, packet.unity.steps[0]));
|
|
632
|
+
let executions = 0;
|
|
633
|
+
const executor = () => { executions++; f.write(target, "%YAML SYNTHETIC changed through fake Editor\n"); return syntheticEditorSuccess(); };
|
|
634
|
+
const receipt = runRepairEditor(f.project, "synthetic", run.reservation.id, "step-1", executor);
|
|
635
|
+
assert.throws(() => runRepairEditor(f.project, "synthetic", run.reservation.id, "step-1", executor), /already attempted/);
|
|
636
|
+
assert.equal(executions, 1);
|
|
637
|
+
for (const file_path of [receipt.evidence.path, run.reservation.workPacket.scope.path, packet.unity.capabilities.path])
|
|
638
|
+
assert.throws(() => guardTool(f.project, { session_id: "synthetic-session", tool_name: "Write", tool_input: { file_path } }), /immutable/);
|
|
639
|
+
guardTool(f.project, editorCall(f, run, packet.unity.steps[1]));
|
|
640
|
+
runRepairEditor(f.project, "synthetic", run.reservation.id, "step-2", syntheticEditorSuccess);
|
|
641
|
+
f.write(request.handoffPaths[0], "NCCGS-HANDOFF: COMPLETE\nSYNTHETIC saved asset");
|
|
642
|
+
const observed = observe(f.project, f.result(run), "agent_result");
|
|
643
|
+
assert.equal(f.state().reservations.at(-1).status, "completed");
|
|
644
|
+
f.write(receipt.evidence.path, "SYNTHETIC tampering");
|
|
645
|
+
assert.throws(() => recordStandalone(f.project, f.task(), "implementation", f.stage(run, observed)), /stale artifact/);
|
|
646
|
+
} finally { f.dispose(); }
|
|
647
|
+
});
|
|
648
|
+
|
|
649
|
+
test("Unity repair rejects malformed profiles and scope before consuming another dispatch", () => {
|
|
650
|
+
const f = fixture();
|
|
651
|
+
try {
|
|
652
|
+
const { packet, request } = prepareUnityRepair(f);
|
|
653
|
+
const invalid = [
|
|
654
|
+
{ ...packet, repairType: "unrestricted" }, { ...packet, budget: { workCalls: 25 } },
|
|
655
|
+
{ ...packet, changes: [{ ...packet.changes[0], operation: "delete" }] },
|
|
656
|
+
{ ...packet, changes: [{ ...packet.changes[0], operation: "create" }] },
|
|
657
|
+
{ ...packet, changes: [{ ...packet.changes[0], path: "Assets/fixture.cs.meta" }] },
|
|
658
|
+
{ ...packet, changes: [{ ...packet.changes[0], path: "Packages/manifest.json" }] },
|
|
659
|
+
{ ...packet, changes: [{ ...packet.changes[0], path: "Assets/missing/new.cs", operation: "create" }] },
|
|
660
|
+
{ ...packet, unity: { ...packet.unity, capabilities: { ...packet.unity.capabilities, sha256: "changed" } } },
|
|
661
|
+
...[{ arguments: ["--project-path", "/other"] }, { command: "eval;evil" }, { command: "scene_open" }, { arguments: ["one", "two"] },
|
|
662
|
+
{ arguments: ['using UnityEngine;\nreturn "bad";'] }, { timeoutMs: 60001 }, { id: "../x" }, { unknown: true }]
|
|
663
|
+
.map(override => ({ ...packet, unity: { ...packet.unity, steps: [{ ...packet.unity.steps[0], ...override }] } }))
|
|
664
|
+
];
|
|
665
|
+
const before = fs.readFileSync(path.join(f.project, ".nccgs/orchestration.json"));
|
|
666
|
+
for (const bad of invalid) {
|
|
667
|
+
f.write(request.workPacket, bad); assert.throws(() => f.dispatch(request));
|
|
668
|
+
assert.deepEqual(fs.readFileSync(path.join(f.project, ".nccgs/orchestration.json")), before);
|
|
669
|
+
}
|
|
670
|
+
f.write(request.workPacket, packet);
|
|
671
|
+
const run = f.dispatch(request);
|
|
672
|
+
const same = reserve(f.project, "synthetic", run.request);
|
|
673
|
+
assert.equal(same.id, run.reservation.id); assert.equal(f.state().usage.dispatches, 3);
|
|
674
|
+
} finally { f.dispose(); }
|
|
675
|
+
});
|
|
676
|
+
|
|
677
|
+
test("Unity repair refuses out-of-scope side effects, changed GUIDs and unfinished import", () => {
|
|
678
|
+
for (const variant of ["outside", "deleted", "guid", "missing-meta", "missing-step"]) {
|
|
679
|
+
const f = fixture();
|
|
680
|
+
try {
|
|
681
|
+
const { packet, request, target } = prepareUnityRepair(f, "unity-code", variant === "missing-meta"), run = f.dispatch(request); f.start(run);
|
|
682
|
+
if (variant === "missing-step") {
|
|
683
|
+
f.write(request.handoffPaths[0], "NCCGS-HANDOFF: COMPLETE\nSYNTHETIC false completion");
|
|
684
|
+
} else {
|
|
685
|
+
guardTool(f.project, editorCall(f, run, packet.unity.steps[0]));
|
|
686
|
+
const result = runRepairEditor(f.project, "synthetic", run.reservation.id, "step-1", () => {
|
|
687
|
+
if (variant === "outside") f.write("Assets/Unowned.cs", "// synthetic unexpected import side effect");
|
|
688
|
+
if (variant === "deleted") fs.unlinkSync(path.join(f.project, "Assets/fixture.cs"));
|
|
689
|
+
if (variant === "guid") f.write("Assets/fixture.cs.meta", "guid: aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\n");
|
|
690
|
+
if (variant === "missing-meta") f.write(target, "// synthetic new file without imported metadata");
|
|
691
|
+
return syntheticEditorSuccess();
|
|
692
|
+
});
|
|
693
|
+
assert.equal(result.operationStatus, variant === "missing-meta" ? "completed" : "failed");
|
|
694
|
+
f.write(request.handoffPaths[0], "NCCGS-HANDOFF: COMPLETE\nSYNTHETIC false completion");
|
|
695
|
+
}
|
|
696
|
+
const observed = observe(f.project, f.result(run), "agent_result");
|
|
697
|
+
assert.equal(f.state().reservations.at(-1).status, "failed", variant);
|
|
698
|
+
assert.throws(() => recordStandalone(f.project, f.task(), "implementation", f.stage(run, observed)), /completed managed dispatch/);
|
|
699
|
+
assert.equal(f.state().usage.dispatches, 3); assert.equal(f.task().stages.review.verdict, "CHANGES_REQUIRED");
|
|
700
|
+
} finally { f.dispose(); }
|
|
701
|
+
}
|
|
702
|
+
});
|
|
703
|
+
|
|
704
|
+
test("Unity repair interruption retains ownership, partial evidence and spent attempts", () => {
|
|
705
|
+
for (const variant of ["timeout", "signal", "nonzero", "missing-cli"]) {
|
|
706
|
+
const f = fixture();
|
|
707
|
+
try {
|
|
708
|
+
const { packet, request } = prepareUnityRepair(f), run = f.dispatch(request); f.start(run);
|
|
709
|
+
guardTool(f.project, editorCall(f, run, packet.unity.steps[0]));
|
|
710
|
+
const error = Object.assign(new Error("SYNTHETIC interrupted editor operation"), { code: variant === "missing-cli" ? "ENOENT" : "ETIMEDOUT" });
|
|
711
|
+
const result = runRepairEditor(f.project, "synthetic", run.reservation.id, "step-1", () => variant === "signal" ? { status: null, signal: "SIGTERM" }
|
|
712
|
+
: variant === "nonzero" ? { status: 6 } : { status: null, error });
|
|
713
|
+
assert.equal(result.status, "FAIL");
|
|
714
|
+
assert.throws(() => guardTool(f.project, repairCall(f, run, "read-after-failure")), /running, correlated|failed or remains uncertain/);
|
|
715
|
+
const partial = "NCCGS-HANDOFF: PARTIAL\nSYNTHETIC interrupted Editor operation; retain original attempt";
|
|
716
|
+
guardTool(f.project, repairCall(f, run, "partial", "Write", { file_path: request.handoffPaths[0], content: partial }));
|
|
717
|
+
f.write(request.handoffPaths[0], partial);
|
|
718
|
+
if (variant !== "missing-cli") {
|
|
719
|
+
assert.equal(f.state().reservations.at(-1).status, "uncertain");
|
|
720
|
+
const observed = observe(f.project, f.result(run), "agent_result");
|
|
721
|
+
const retained = f.state().reservations.at(-1);
|
|
722
|
+
assert.equal(retained.status, "uncertain"); assert.equal(retained.workStatus, "incomplete-editor");
|
|
723
|
+
assert.equal(retained.telemetry.totalTokens, 456); assert.equal(retained.telemetry.durationMs, 123);
|
|
724
|
+
assert.equal(retained.runtime.path, observed.path);
|
|
725
|
+
assert.equal(observe(f.project, f.result(run), "agent_result").duplicate, true);
|
|
726
|
+
assert.throws(() => recordStandalone(f.project, f.task(), "implementation", f.stage(run, observed)), /handoff is partial|completed managed dispatch/);
|
|
727
|
+
assert.throws(() => f.dispatch(), /owns the project/);
|
|
728
|
+
reconcile(f.project, run.reservation.id, { status: "failed", reason: "SYNTHETIC external operator confirmed the fake operation stopped", reference: "SYNTHETIC TEST ONLY" });
|
|
729
|
+
} else observe(f.project, f.result(run), "agent_result");
|
|
730
|
+
assert.equal(f.state().usage.dispatches, 3); assert.equal(f.state().active.length, 0);
|
|
731
|
+
assert.equal(fs.readFileSync(path.join(f.project, request.handoffPaths[0]), "utf8"), partial);
|
|
732
|
+
assert.ok(f.state().reservations.at(-1).editorSteps[0].evidence);
|
|
733
|
+
assert.equal(f.state().reservations.at(-1).telemetry.totalTokens, 456);
|
|
734
|
+
} finally { f.dispose(); }
|
|
735
|
+
}
|
|
736
|
+
});
|
|
737
|
+
|
|
738
|
+
test("Unity repair CLI rejects caller-supplied commands, duplicate flags and forwarded payloads", () => {
|
|
739
|
+
const f = fixture();
|
|
740
|
+
try {
|
|
741
|
+
const base = ["repair-editor", "--project", f.project, "--id", "synthetic", "--dispatch", "synthetic", "--step", "one"];
|
|
742
|
+
for (const extra of [["--command", "evil"], ["--step", "two"], ["--", "unity"], ["--timeout", "999999"]])
|
|
743
|
+
assert.throws(() => taskRun([...base, ...extra]), /unique.*no forwarded/);
|
|
744
|
+
} finally { f.dispose(); }
|
|
745
|
+
});
|
|
746
|
+
|
|
747
|
+
test("Unity repair executable hook and CLI retain a missing-tool failure without launching Unity", () => {
|
|
748
|
+
const f = fixture();
|
|
749
|
+
try {
|
|
750
|
+
const { packet, request } = prepareUnityRepair(f), run = f.dispatch(request); f.start(run);
|
|
751
|
+
const admitted = f.hook("closure-guard.mjs", "pre", editorCall(f, run, packet.unity.steps[0]));
|
|
752
|
+
assert.equal(admitted.status, 0, admitted.stderr);
|
|
753
|
+
const env = Object.fromEntries(Object.entries(process.env).filter(([key]) => key.toLowerCase() !== "path"));
|
|
754
|
+
env.PATH = f.project; // No unity executable in this disposable synthetic fixture.
|
|
755
|
+
const child = spawnSync(process.execPath, [path.join(root, ".claude/nccgs/tools/task.mjs"), "repair-editor", "--project", f.project,
|
|
756
|
+
"--id", "synthetic", "--dispatch", run.reservation.id, "--step", "step-1"], { cwd: f.project, env, windowsHide: true, encoding: "utf8" });
|
|
757
|
+
assert.equal(child.status, 1, child.stderr);
|
|
758
|
+
const response = JSON.parse(child.stdout);
|
|
759
|
+
assert.equal(response.operationStatus, "failed");
|
|
760
|
+
assert.match(json(path.join(f.project, response.evidence.path)).error, /ENOENT/);
|
|
761
|
+
assert.equal(f.state().reservations.at(-1).editorSteps.length, 1);
|
|
762
|
+
assert.equal(f.state().usage.dispatches, 3);
|
|
763
|
+
} finally { f.dispose(); }
|
|
764
|
+
});
|
|
765
|
+
|
|
766
|
+
test("Unity repair code-only profile does not require Editor availability for existing files", () => {
|
|
767
|
+
const f = fixture();
|
|
768
|
+
try {
|
|
769
|
+
const { packet, request, target } = prepareUnityRepair(f, "unity-code", false, 0);
|
|
770
|
+
delete packet.unity.capabilities;
|
|
771
|
+
f.write(request.workPacket, packet);
|
|
772
|
+
const run = f.dispatch(request); f.start(run);
|
|
773
|
+
const prompt = dispatchPrompt(f.project, "synthetic", run.reservation.id).prompt;
|
|
774
|
+
assert.match(prompt, /No Editor steps are reserved/); assert.doesNotMatch(prompt, /undefined/);
|
|
775
|
+
guardTool(f.project, repairCall(f, run, "code-only", "Edit", { file_path: target }));
|
|
776
|
+
f.write(target, "// SYNTHETIC compile-error correction without an available Editor\n");
|
|
777
|
+
f.write(request.handoffPaths[0], "NCCGS-HANDOFF: COMPLETE\nSYNTHETIC code fix; coordinator checks pending");
|
|
778
|
+
const observed = observe(f.project, f.result(run), "agent_result");
|
|
779
|
+
assert.equal(f.state().reservations.at(-1).status, "completed");
|
|
780
|
+
assert.equal(f.state().reservations.at(-1).editorSteps, undefined);
|
|
781
|
+
recordStandalone(f.project, f.task(), "implementation", f.stage(run, observed));
|
|
782
|
+
assert.ok(f.task().history.some(h => h.stages?.review?.verdict === "CHANGES_REQUIRED"));
|
|
783
|
+
assert.equal(f.task().reviewBudget.used, 1);
|
|
784
|
+
} finally { f.dispose(); }
|
|
785
|
+
});
|
|
786
|
+
|
|
315
787
|
test("repair refuses missing, stale, broad and malformed packets before spending quota", () => {
|
|
316
788
|
const f = fixture();
|
|
317
789
|
try {
|
|
@@ -560,7 +1032,7 @@ test("native completion ignores quoted model prose and fails closed on unsupport
|
|
|
560
1032
|
}
|
|
561
1033
|
});
|
|
562
1034
|
|
|
563
|
-
function prepareStructuredReview(f, values = {}, body = null) {
|
|
1035
|
+
function prepareStructuredReview(f, values = {}, body = null, beforeCompletion = null) {
|
|
564
1036
|
const author = f.dispatch({ agent: "nccgs-game-designer" }); f.start(author);
|
|
565
1037
|
f.write(f.analysis, "Synthetic analysis; no game or provider acceptance.");
|
|
566
1038
|
const authorRuntime = observe(f.project, f.result(author), "agent_result");
|
|
@@ -572,6 +1044,7 @@ function prepareStructuredReview(f, values = {}, body = null) {
|
|
|
572
1044
|
const result = { schemaVersion: 1, taskId: task.id, contractHash: task.contractHash, snapshot: snapshot(f.project, task).sha256,
|
|
573
1045
|
verdict: "PASS", criteriaMet: true, findings: [], conditions: [], ...values };
|
|
574
1046
|
f.write(reviewPath, body ?? `NCCGS-HANDOFF: COMPLETE\n\n\`\`\`nccgs-review\n${JSON.stringify(result)}\n\`\`\`\n`);
|
|
1047
|
+
beforeCompletion?.(reviewer, result, reviewPath);
|
|
575
1048
|
const observed = observe(f.project, f.result(reviewer), "agent_result");
|
|
576
1049
|
return { ...f.stage(reviewer, observed), evidence: artifact(f.project, reviewPath), verdict: result.verdict,
|
|
577
1050
|
criteriaMet: result.criteriaMet, findings: result.findings, conditions: result.conditions };
|
|
@@ -591,6 +1064,101 @@ test("review conditions are pinned to the reviewer and cannot be removed by coor
|
|
|
591
1064
|
} finally { f.dispose(); }
|
|
592
1065
|
});
|
|
593
1066
|
|
|
1067
|
+
test("dispatch preparation produces bounded PARTIAL drafts without changing task, ledger or files", () => {
|
|
1068
|
+
const f = fixture(); try {
|
|
1069
|
+
const handoffPath = ".nccgs/evidence/analysis/worker handoff.md";
|
|
1070
|
+
const worker = f.dispatch({ handoffPaths: [handoffPath] });
|
|
1071
|
+
const before = JSON.stringify({ task: f.task(), state: f.state() });
|
|
1072
|
+
const prepared = taskRun(["dispatch-prompt", "--project", f.project, "--id", "synthetic", "--dispatch", worker.reservation.id]);
|
|
1073
|
+
assert.ok(prepared.prompt.startsWith(`[NCCGS-DISPATCH:${worker.reservation.id}]`));
|
|
1074
|
+
assert.match(prepared.prompt, /Source write ownership: \["Assets\/fixture.cs"\]/);
|
|
1075
|
+
assert.match(prepared.prompt, /synthetic-check|pinned task contract/);
|
|
1076
|
+
assert.match(prepared.handoffs[0].content, /^NCCGS-HANDOFF: PARTIAL/);
|
|
1077
|
+
assert.equal(fs.existsSync(path.join(f.project, handoffPath)), false);
|
|
1078
|
+
assert.equal(JSON.stringify({ task: f.task(), state: f.state() }), before);
|
|
1079
|
+
f.write("Assets/fixture.cs", "// changed before launch");
|
|
1080
|
+
assert.throws(() => taskRun(["dispatch-prompt", "--project", f.project, "--id", "synthetic", "--dispatch", worker.reservation.id]), /snapshot stale/);
|
|
1081
|
+
} finally { f.dispose(); }
|
|
1082
|
+
});
|
|
1083
|
+
|
|
1084
|
+
test("reviewer validates and corrects its draft before completion; coordinator imports exact pinned result", () => {
|
|
1085
|
+
const f = fixture("plan", true); try {
|
|
1086
|
+
const stage = prepareStructuredReview(f, { findings: [{ severity: "INFO", status: "NOTED", description: "Synthetic disclosed limitation" }] }, null,
|
|
1087
|
+
(reviewer, value, file) => {
|
|
1088
|
+
const args = ["handoff-validate", "--project", f.project, "--id", "synthetic", "--dispatch", reviewer.reservation.id, "--file", file];
|
|
1089
|
+
const before = JSON.stringify({ task: f.task(), state: f.state() });
|
|
1090
|
+
assert.throws(() => taskRun(args), /OPEN, RESOLVED or ACCEPTED/);
|
|
1091
|
+
assert.equal(JSON.stringify({ task: f.task(), state: f.state() }), before);
|
|
1092
|
+
value.findings[0].status = "ACCEPTED";
|
|
1093
|
+
f.write(file, `NCCGS-HANDOFF: PARTIAL\n\`\`\`nccgs-review\n${JSON.stringify(value)}\n\`\`\`\n`);
|
|
1094
|
+
assert.equal(taskRun(args).complete, false);
|
|
1095
|
+
assert.equal(f.task().stages.review, undefined);
|
|
1096
|
+
f.write(file, `NCCGS-HANDOFF: COMPLETE\n\`\`\`nccgs-review\n${JSON.stringify(value)}\n\`\`\`\n`);
|
|
1097
|
+
assert.equal(taskRun(args).complete, true);
|
|
1098
|
+
const command = `node .claude/nccgs/tools/task.mjs handoff-validate --id synthetic --dispatch ${reviewer.reservation.id} --file ${file}`;
|
|
1099
|
+
const envelope = { session_id: "synthetic-session", agent_id: reviewer.agentId, tool_name: "Bash", tool_input: { command } };
|
|
1100
|
+
assert.doesNotThrow(() => guardTool(f.project, envelope));
|
|
1101
|
+
const hook = f.hook("closure-guard.mjs", "pre", { ...envelope, hook_event_name: "PreToolUse" });
|
|
1102
|
+
assert.equal(hook.status, 0, hook.stderr);
|
|
1103
|
+
for (const bad of [command + " --verdict PASS", command + " --file " + file, command + " -- node evil.mjs", command + "; echo unsafe",
|
|
1104
|
+
command.replace("--id synthetic", "--id other"), command.replace(file, f.analysis), command.replace("handoff-validate", "record-review"),
|
|
1105
|
+
command.replace(reviewer.reservation.id, "different-dispatch")]) {
|
|
1106
|
+
assert.throws(() => guardTool(f.project, { ...envelope, tool_input: { command: bad } }));
|
|
1107
|
+
}
|
|
1108
|
+
});
|
|
1109
|
+
const bytes = fs.readFileSync(path.join(f.project, stage.evidence.path));
|
|
1110
|
+
const oldTask = f.task(), oldLedger = JSON.stringify(f.state());
|
|
1111
|
+
const args = ["record-review", "--project", f.project, "--id", "synthetic", "--dispatch", stage.dispatchId, "--file", stage.evidence.path];
|
|
1112
|
+
const recorded = taskRun(args);
|
|
1113
|
+
for (const key of ["verdict", "criteriaMet", "findings", "conditions", "runtime", "evidence", "snapshot", "dispatchId"]) assert.deepEqual(recorded.stages.review[key], stage[key]);
|
|
1114
|
+
assert.equal(recorded.reviewBudget.used, oldTask.reviewBudget.used + 1);
|
|
1115
|
+
assert.equal(recorded.history.length, oldTask.history.length + 1);
|
|
1116
|
+
assert.deepEqual(recorded.history.slice(0, oldTask.history.length), oldTask.history);
|
|
1117
|
+
assert.equal(JSON.stringify(f.state()), oldLedger);
|
|
1118
|
+
assert.deepEqual(fs.readFileSync(path.join(f.project, stage.evidence.path)), bytes);
|
|
1119
|
+
assert.equal(recorded.closed, undefined);
|
|
1120
|
+
const saved = JSON.stringify(f.task());
|
|
1121
|
+
assert.throws(() => taskRun(args), /only once/);
|
|
1122
|
+
assert.equal(JSON.stringify(f.task()), saved);
|
|
1123
|
+
assert.throws(() => taskRun([...args, "--verdict", "PASS"]), /unique/);
|
|
1124
|
+
assert.throws(() => taskRun([...args, "--dispatch", stage.dispatchId]), /unique/);
|
|
1125
|
+
} finally { f.dispose(); }
|
|
1126
|
+
});
|
|
1127
|
+
|
|
1128
|
+
test("automatic review import preserves negative verdicts and mandatory manual acceptance", () => {
|
|
1129
|
+
for (const verdict of ["BLOCKED", "CHANGES_REQUIRED", "PASS"]) {
|
|
1130
|
+
const f = fixture("plan", true, { manualGates: ["OWNER-01"] }); try {
|
|
1131
|
+
const stage = prepareStructuredReview(f, { verdict, criteriaMet: verdict === "PASS",
|
|
1132
|
+
conditions: verdict === "PASS" ? [] : [{ id: "C1", description: "Required investigation remains" }] });
|
|
1133
|
+
const recorded = taskRun(["record-review", "--project", f.project, "--id", "synthetic", "--dispatch", stage.dispatchId, "--file", stage.evidence.path]);
|
|
1134
|
+
assert.equal(recorded.stages.review.verdict, verdict);
|
|
1135
|
+
assert.deepEqual(recorded.stages.review.conditions, stage.conditions);
|
|
1136
|
+
if (verdict === "PASS") {
|
|
1137
|
+
recordStandalone(f.project, f.task(), "report", { outcome: "success", summary: "Synthetic technical result; owner decision still pending" });
|
|
1138
|
+
assert.throws(() => taskRun(["close", "--project", f.project, "--id", "synthetic"]), /acceptance: missing/);
|
|
1139
|
+
} else assert.throws(() => recordStandalone(f.project, f.task(), "report", { outcome: "success", summary: "Cannot invent success" }), /review has not passed/);
|
|
1140
|
+
} finally { f.dispose(); }
|
|
1141
|
+
}
|
|
1142
|
+
});
|
|
1143
|
+
|
|
1144
|
+
test("automatic review import rejects tampering, conditional PASS, missing measurements and legacy policy without consuming a record", () => {
|
|
1145
|
+
for (const variant of ["tampered", "source", "conditional", "unmeasured", "legacy", "unattached", "incomplete", "malformed"]) {
|
|
1146
|
+
const f = fixture("plan", variant !== "legacy", variant === "unmeasured" ? { acceptance: [{ id: "AC", description: "Measured result", check: "synthetic-check",
|
|
1147
|
+
assertions: [{ artifact: ".nccgs/evidence/measurements/result.json", pointer: "/actual", op: "eq", value: true }] }] } : {});
|
|
1148
|
+
try {
|
|
1149
|
+
const stage = prepareStructuredReview(f, variant === "conditional" ? { conditions: [{ id: "C1", description: "Still needs evidence" }] } : {},
|
|
1150
|
+
variant === "incomplete" ? "NCCGS-HANDOFF: PARTIAL\nNot finished" : variant === "malformed" ? "NCCGS-HANDOFF: COMPLETE\n```nccgs-review\ninvalid\n```" : null);
|
|
1151
|
+
let file = stage.evidence.path;
|
|
1152
|
+
if (variant === "tampered") f.write(file, fs.readFileSync(path.join(f.project, file), "utf8") + "\nEdited after completion\n");
|
|
1153
|
+
if (variant === "source") f.write("Assets/fixture.cs", "// unrelated source change invalidates evidence");
|
|
1154
|
+
if (variant === "unattached") { file = ".nccgs/evidence/analysis/copied.md"; f.write(file, fs.readFileSync(path.join(f.project, stage.evidence.path), "utf8")); }
|
|
1155
|
+
const before = JSON.stringify({ task: f.task(), state: f.state() });
|
|
1156
|
+
assert.throws(() => taskRun(["record-review", "--project", f.project, "--id", "synthetic", "--dispatch", stage.dispatchId, "--file", file]));
|
|
1157
|
+
assert.equal(JSON.stringify({ task: f.task(), state: f.state() }), before);
|
|
1158
|
+
} finally { f.dispose(); }
|
|
1159
|
+
}
|
|
1160
|
+
});
|
|
1161
|
+
|
|
594
1162
|
test("complete negative review retains pending conditions; complete unconditional review may close", () => {
|
|
595
1163
|
for (const verdict of ["BLOCKED", "PASS"]) {
|
|
596
1164
|
const f = fixture("plan", true); try {
|