nccgs 2.0.0 → 2.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/agents/nccgs-adversarial-reviewer.md +1 -1
- package/.claude/agents/nccgs-engine-programmer.md +1 -1
- package/.claude/agents/nccgs-fast-implementer.md +1 -1
- package/.claude/agents/nccgs-game-designer.md +1 -1
- package/.claude/agents/nccgs-gameplay-programmer.md +1 -1
- package/.claude/agents/nccgs-qa-engineer.md +1 -1
- package/.claude/agents/nccgs-task-scout.md +1 -1
- package/.claude/agents/nccgs-ui-programmer.md +1 -1
- package/.claude/agents/nccgs-unity-build-specialist.md +1 -1
- package/.claude/agents/nccgs-unity-implementer.md +1 -1
- package/.claude/agents/nccgs-unity-systems-specialist.md +1 -1
- package/.claude/agents/nccgs-unity-ui-specialist.md +1 -1
- package/.claude/agents/nccgs-verification-engineer.md +1 -1
- package/.claude/nccgs/MEMORY.md +9 -3
- package/.claude/nccgs/VERSION +1 -1
- package/.claude/nccgs/constitution.md +187 -176
- package/.claude/nccgs/hooks/closure-guard.mjs +2 -0
- package/.claude/nccgs/hooks/unity-transport-guard.mjs +11 -0
- package/.claude/nccgs/pipeline.json +260 -260
- package/.claude/nccgs/protocols/context-packets.md +5 -0
- package/.claude/nccgs/protocols/model-routing.md +46 -46
- package/.claude/nccgs/protocols/review-handoff.md +21 -1
- package/.claude/nccgs/protocols/shared-workflow-context.md +33 -0
- package/.claude/nccgs/protocols/unity-boundary.md +20 -0
- package/.claude/nccgs/routing.generated.md +4 -4
- package/.claude/nccgs/settings.fragment.json +84 -24
- package/.claude/nccgs/studio.json +53 -53
- package/.claude/nccgs/tools/autonomy.mjs +13 -0
- package/.claude/nccgs/tools/command-tokens.mjs +22 -0
- package/.claude/nccgs/tools/compile-policy.mjs +4 -0
- package/.claude/nccgs/tools/configure-models.mjs +4 -0
- package/.claude/nccgs/tools/hook-transport.mjs +34 -0
- package/.claude/nccgs/tools/installation-guard.mjs +55 -0
- package/.claude/nccgs/tools/native-completion.mjs +10 -1
- package/.claude/nccgs/tools/returned-handoff.mjs +11 -2
- package/.claude/nccgs/tools/shared-context-guard.mjs +60 -0
- package/.claude/nccgs/tools/shared-context.mjs +25 -0
- package/.claude/nccgs/tools/tool-guard.mjs +120 -131
- package/.claude/nccgs/tools/unity-transport.mjs +170 -0
- package/.claude/nccgs/workflow-catalog.json +4 -4
- package/CHANGELOG.md +235 -165
- package/CLAUDE.md +10 -8
- package/MEMORY.md +109 -56
- package/README.md +206 -168
- package/UPGRADING.md +130 -112
- package/VERSION +1 -1
- package/docs/ARCHITECTURE.md +131 -131
- package/docs/ASSET-PRODUCTION-GRAPHIFY-PLAN.md +230 -0
- package/docs/HUONG-DAN-MIGRATE-VA-SU-DUNG.md +117 -87
- package/docs/NCCGS-1.5.md +237 -237
- package/docs/PROJECT-POLICY.md +94 -94
- package/docs/RELEASE-STATUS.md +32 -7
- package/docs/UNINSTALL.md +134 -0
- package/docs/WFK17-HOOK-REPAIR.md +71 -0
- package/docs/WORKFLOWS.md +85 -85
- package/package.json +1 -1
- package/scaffold/.nccgs/project.yaml +109 -108
- package/scripts/benchmark-v15.mjs +173 -173
- package/scripts/cli.mjs +11 -2
- package/scripts/doctor.mjs +261 -261
- package/scripts/install.mjs +530 -515
- package/scripts/recover-session.mjs +89 -0
- package/scripts/run.mjs +22 -9
- package/scripts/supervise.mjs +4 -0
- package/scripts/uninstall.mjs +292 -0
- package/scripts/validate.mjs +285 -278
- package/tests/autonomy.test.mjs +126 -113
- package/tests/design-readiness.test.mjs +199 -199
- package/tests/fixtures/compiler-shutdown.cs +67 -64
- package/tests/fixtures/native-provider.mjs +61 -0
- package/tests/framework.test.mjs +740 -740
- package/tests/hook-repair.test.mjs +106 -0
- package/tests/lifecycle-v15.test.mjs +346 -300
- package/tests/native-completion-notices.test.mjs +181 -0
- package/tests/phase1.test.mjs +427 -416
- package/tests/phase2.test.mjs +250 -250
- package/tests/policy-v15.test.mjs +234 -234
- package/tests/returned-handoff-notices.test.mjs +170 -0
- package/tests/runtime-v15.test.mjs +1197 -1191
- package/tests/task-slices.test.mjs +285 -285
- package/tests/uninstall.test.mjs +477 -0
- package/tests/unity-transport.test.mjs +348 -0
|
@@ -1,285 +1,285 @@
|
|
|
1
|
-
import assert from "node:assert/strict";
|
|
2
|
-
import fs from "node:fs";
|
|
3
|
-
import os from "node:os";
|
|
4
|
-
import path from "node:path";
|
|
5
|
-
import { fileURLToPath } from "node:url";
|
|
6
|
-
import test from "node:test";
|
|
7
|
-
import { artifact, json } from "../.claude/nccgs/tools/policy.mjs";
|
|
8
|
-
import { initializeStandalone, recordStandalone } from "../.claude/nccgs/tools/lifecycle.mjs";
|
|
9
|
-
import { reserve, status, planTaskSlices, taskSliceStatus, rejectUnclaimed, dispatchPrompt } from "../.claude/nccgs/tools/orchestration.mjs";
|
|
10
|
-
import { snapshot } from "../.claude/nccgs/tools/gates.mjs";
|
|
11
|
-
import { observe } from "../.claude/nccgs/tools/runtime.mjs";
|
|
12
|
-
import { guardTool } from "../.claude/nccgs/tools/tool-guard.mjs";
|
|
13
|
-
import { run as taskRun } from "../.claude/nccgs/tools/task.mjs";
|
|
14
|
-
import { slicePromptContext } from "../.claude/nccgs/tools/task-slices.mjs";
|
|
15
|
-
|
|
16
|
-
// All workers, model observations, design documents and measurements here are
|
|
17
|
-
// SYNTHETIC fixtures. No Claude provider or Unity process is launched.
|
|
18
|
-
const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
|
19
|
-
function fixture(parent = false) {
|
|
20
|
-
const temporaryRoot = fs.realpathSync(os.tmpdir()), project = fs.mkdtempSync(path.join(temporaryRoot, "nccgs-slices-"));
|
|
21
|
-
const write = (relative, value) => {
|
|
22
|
-
const file = path.join(project, relative); fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
23
|
-
fs.writeFileSync(file, typeof value === "string" || Buffer.isBuffer(value) ? value : JSON.stringify(value)); return artifact(project, relative);
|
|
24
|
-
};
|
|
25
|
-
for (const file of ["pipeline.json", "studio.json"]) write(`.claude/nccgs/${file}`, fs.readFileSync(path.join(root, ".claude/nccgs", file)));
|
|
26
|
-
write(".nccgs/project.yaml", "schema_version: 2\nmodels:\n profile: claude-standalone\n");
|
|
27
|
-
write(".nccgs/execution.json", { schemaVersion: 1, name: "standalone", externalReview: { mode: "off" }, acceptanceRequired: false,
|
|
28
|
-
requireObservedEffort: false, reviewContractRequired: true, maxParallelWorkers: 2 });
|
|
29
|
-
write(".nccgs/requirements.yaml", "schema_version: 1\nrequirements: []\n");
|
|
30
|
-
write("Assets/first.cs", "// SYNTHETIC first file\n"); write("Assets/second.cs", "// SYNTHETIC second file\n");
|
|
31
|
-
const acceptance = ["first", "second"].map(name => ({ id: `AC-${name}`, description: `Synthetic ${name} acceptance`, check: "integration-check",
|
|
32
|
-
assertions: [{ artifact: ".nccgs/evidence/measurements/result.json", pointer: `/${name}`, op: "eq", value: true }] }));
|
|
33
|
-
acceptance.push({ id: "OWNER", description: "Synthetic mandatory owner decision", manual: true });
|
|
34
|
-
write(".nccgs/intake.json", { schemaVersion: 1, mode: "implement", objective: "SYNTHETIC large task", scope: "feature", target: "production-change", readiness: "ready",
|
|
35
|
-
acceptance, requiredChecks: ["integration-check"], analysisArtifacts: [".nccgs/evidence/analysis/review-1.md", ".nccgs/evidence/analysis/review-2.md", ".nccgs/evidence/analysis/review-3.md"] });
|
|
36
|
-
if (parent) initializeStandalone(project, "parent", { intake: ".nccgs/intake.json" });
|
|
37
|
-
initializeStandalone(project, "large", { intake: ".nccgs/intake.json", ...(parent ? { parentId: "parent" } : {}) });
|
|
38
|
-
write(".nccgs/runtime/bindings/synthetic-session.json", { taskId: "large" });
|
|
39
|
-
const task = () => json(path.join(project, ".nccgs/tasks/large.json"));
|
|
40
|
-
const snap = () => snapshot(project, task()).sha256;
|
|
41
|
-
const draft = ".nccgs/evidence/slice-plans/plan.json";
|
|
42
|
-
const plan = () => ({ schemaVersion: 1, taskId: "large", contractHash: task().contractHash, sourceSnapshot: snap(), planId: "plan-one", supersedes: null,
|
|
43
|
-
reason: "SYNTHETIC bounded decomposition", integration: "integrate", slices: [
|
|
44
|
-
{ id: "first", objective: "Implement the first bounded part", agent: "nccgs-gameplay-programmer", dependsOn: [], writePaths: ["Assets/first.cs"], criteria: ["AC-first"] },
|
|
45
|
-
{ id: "integrate", objective: "Implement second part and integrate the full result", agent: "nccgs-gameplay-programmer", dependsOn: ["first"], writePaths: ["Assets/second.cs"], criteria: ["AC-second", "OWNER"] }
|
|
46
|
-
] });
|
|
47
|
-
const register = (p = plan()) => { write(draft, p); return planTaskSlices(project, "large", draft, true); };
|
|
48
|
-
let sequence = 0;
|
|
49
|
-
const start = (request, agentId) => {
|
|
50
|
-
const dispatch = reserve(project, "large", request), agent = agentId ?? `synthetic-agent-${++sequence}`;
|
|
51
|
-
const envelope = { hook_event_name: "PreToolUse", session_id: "synthetic-session", tool_name: "Agent", tool_use_id: `tool-${dispatch.id}`,
|
|
52
|
-
tool_input: { subagent_type: request.agent, prompt: `[NCCGS-DISPATCH:${dispatch.id}] Caller prose cannot replace the slice contract` } };
|
|
53
|
-
const prompt = guardTool(project, envelope)?.updatedInput?.prompt;
|
|
54
|
-
observe(project, { session_id: "synthetic-session", agent_id: agent, agent_type: request.agent }, "agent_start");
|
|
55
|
-
return { dispatch, request, envelope, agentId: agent, prompt };
|
|
56
|
-
};
|
|
57
|
-
const finish = (run, { partial = false, model = "claude-opus-5-5" } = {}) => observe(project, { ...run.envelope, tool_response: {
|
|
58
|
-
agentId: run.agentId, status: "completed", resolvedModel: model, modelsUsed: model ? [model] : [], totalDurationMs: 123, totalTokens: 45,
|
|
59
|
-
totalToolUseCount: 3, content: partial ? "SYNTHETIC partial" : "SYNTHETIC completed" } }, "agent_result");
|
|
60
|
-
const deliver = (request = taskSliceStatus(project, "large").next, options = {}) => {
|
|
61
|
-
const run = start(request);
|
|
62
|
-
if (!options.partial) for (const [index, file] of request.writePaths.entries()) {
|
|
63
|
-
guardTool(project, { session_id: "synthetic-session", agent_id: run.agentId, tool_use_id: `write-${run.dispatch.id}-${index}`, tool_name: "Write", tool_input: { file_path: file, content: "synthetic" } });
|
|
64
|
-
write(file, `// SYNTHETIC change from ${run.dispatch.id}\n`);
|
|
65
|
-
}
|
|
66
|
-
write(request.handoffPaths[0], `NCCGS-HANDOFF: ${options.partial ? "PARTIAL" : "COMPLETE"}\nSynthetic handoff, never acceptance proof.`);
|
|
67
|
-
return { ...run, observed: finish(run, options) };
|
|
68
|
-
};
|
|
69
|
-
const verify = (second = true) => {
|
|
70
|
-
write(".nccgs/verify.mjs", `import fs from 'node:fs'; fs.mkdirSync('.nccgs/evidence/measurements',{recursive:true}); fs.writeFileSync('.nccgs/evidence/measurements/result.json',JSON.stringify({first:true,second:${second}})); console.log('SYNTHETIC measurement fixture only');`);
|
|
71
|
-
return taskRun(["verify", "--project", project, "--id", "large", "--name", "integration-check", "--", process.execPath, ".nccgs/verify.mjs"]);
|
|
72
|
-
};
|
|
73
|
-
const stage = (run, verification) => ({ snapshot: run.observed.snapshot, dispatchId: run.dispatch.id, runtime: artifact(project, run.observed.path),
|
|
74
|
-
checks: [{ name: "integration-check", status: verification.status, evidence: verification.evidence }] });
|
|
75
|
-
const review = (number = 1, agentId, verdict = "PASS", conditions = []) => {
|
|
76
|
-
const file = `.nccgs/evidence/analysis/review-${number}.md`;
|
|
77
|
-
const run = start({ kind: "review", agent: "nccgs-adversarial-reviewer", requestKey: `review-${number}`, reason: "SYNTHETIC independent integrated review", writePaths: [], handoffPaths: [file] }, agentId);
|
|
78
|
-
write(file, `NCCGS-HANDOFF: COMPLETE\n\n\`\`\`nccgs-review\n${JSON.stringify({ schemaVersion: 1, taskId: "large", contractHash: task().contractHash, snapshot: snap(),
|
|
79
|
-
verdict, criteriaMet: verdict === "PASS", findings: [], conditions })}\n\`\`\`\n`);
|
|
80
|
-
const observed = finish(run);
|
|
81
|
-
return { ...run, observed, record: () => taskRun(["record-review", "--project", project, "--id", "large", "--dispatch", run.dispatch.id, "--file", file]) };
|
|
82
|
-
};
|
|
83
|
-
return { project, write, task, snap, draft, plan, register, start, finish, deliver, verify, stage, review,
|
|
84
|
-
dispose() {
|
|
85
|
-
assert.equal(path.dirname(project).toLowerCase(), temporaryRoot.toLowerCase()); assert.ok(path.basename(project).startsWith("nccgs-slices-"));
|
|
86
|
-
assert.equal(fs.lstatSync(project).isSymbolicLink(), false); assert.equal(fs.realpathSync(project).toLowerCase(), project.toLowerCase());
|
|
87
|
-
fs.rmSync(project, { recursive: true, force: true });
|
|
88
|
-
} };
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
test("slice preview/registration preserve the full task and budgets; injected contexts select original criteria", () => {
|
|
92
|
-
const f = fixture();
|
|
93
|
-
try {
|
|
94
|
-
const original = fs.readFileSync(path.join(f.project, ".nccgs/tasks/large.json")), plan = f.plan(); f.write(f.draft, plan);
|
|
95
|
-
const preview = planTaskSlices(f.project, "large", f.draft);
|
|
96
|
-
assert.equal(preview.status, "PREVIEW"); assert.equal(fs.existsSync(path.join(f.project, ".nccgs/orchestration.json")), false);
|
|
97
|
-
assert.equal(preview.budgets[0].minimumRemainingSlices, 2); assert.equal(preview.budgets[0].protectedSlots, 4);
|
|
98
|
-
const saved = f.register(plan), ledger = fs.readFileSync(path.join(f.project, ".nccgs/orchestration.json"));
|
|
99
|
-
assert.equal(f.register(plan).status, "ALREADY_REGISTERED");
|
|
100
|
-
assert.deepEqual(fs.readFileSync(path.join(f.project, ".nccgs/orchestration.json")), ledger);
|
|
101
|
-
assert.deepEqual(fs.readFileSync(path.join(f.project, ".nccgs/tasks/large.json")), original);
|
|
102
|
-
assert.equal(status(f.project, "large").usage.dispatches, 0);
|
|
103
|
-
const run = f.start(taskSliceStatus(f.project, "large").next);
|
|
104
|
-
assert.ok(run.prompt.startsWith(`[NCCGS-DISPATCH:${run.dispatch.id}]`));
|
|
105
|
-
assert.match(run.prompt, /AC-first/); assert.doesNotMatch(run.prompt, /AC-second/);
|
|
106
|
-
assert.match(run.prompt, /same task.*shared budget/i);
|
|
107
|
-
const contextTask = structuredClone(f.task());
|
|
108
|
-
contextTask.contract.design = { document: f.write(".nccgs/evidence/synthetic-design.json", { requirements: [
|
|
109
|
-
{ id: "REQ-A", criteria: ["AC-first"], rules: "Preserve exact fixed rule A" }, { id: "REQ-B", criteria: ["AC-second"], rules: "Fixed rule B" }
|
|
110
|
-
], decisions: [{ resolution: "Keep the global decision" }], outOfScope: ["No product changes"] }) };
|
|
111
|
-
const context = slicePromptContext(f.project, contextTask, run.dispatch);
|
|
112
|
-
assert.match(context, /Preserve exact fixed rule A/); assert.doesNotMatch(context, /Fixed rule B/); assert.match(context, /Keep the global decision/);
|
|
113
|
-
assert.throws(() => guardTool(f.project, { session_id: "synthetic-session", tool_name: "Write", tool_input: { file_path: saved.reference.path } }), /immutable/);
|
|
114
|
-
assert.throws(() => guardTool(f.project, { session_id: "synthetic-session", agent_id: run.agentId, tool_name: "Write", tool_input: { file_path: "Assets/second.cs" } }), /exact planned file/);
|
|
115
|
-
} finally { f.dispose(); }
|
|
116
|
-
});
|
|
117
|
-
|
|
118
|
-
test("slices reject omitted criteria, scope expansion, cycles and unaffordable decomposition without mutation", () => {
|
|
119
|
-
const f = fixture();
|
|
120
|
-
try {
|
|
121
|
-
const base = f.plan();
|
|
122
|
-
const variants = [
|
|
123
|
-
{ ...base, acceptance: [] }, { ...base, budgets: { maxDispatches: 999 } }, { ...base, risk: "FAST" }, { ...base, sourceSnapshot: "stale" },
|
|
124
|
-
{ ...base, slices: [base.slices[0], { ...base.slices[1], criteria: ["AC-second"] }] },
|
|
125
|
-
{ ...base, slices: [base.slices[0], { ...base.slices[1], criteria: ["AC-first", "AC-second", "OWNER"] }] },
|
|
126
|
-
{ ...base, slices: [{ ...base.slices[0], dependsOn: ["integrate"] }, base.slices[1]] },
|
|
127
|
-
{ ...base, slices: [base.slices[0], { ...base.slices[1], dependsOn: [] }] },
|
|
128
|
-
{ ...base, slices: [{ ...base.slices[0], writePaths: ["Assets"] }, base.slices[1]] },
|
|
129
|
-
{ ...base, slices: [{ ...base.slices[0], writePaths: [".nccgs/tasks/large.json"] }, base.slices[1]] },
|
|
130
|
-
{ ...base, slices: [{ ...base.slices[0], agent: "nccgs-adversarial-reviewer" }, base.slices[1]] }
|
|
131
|
-
];
|
|
132
|
-
const many = Array.from({ length: 9 }, (_, i) => ({ id: `part-${i}`, objective: "Synthetic small unit", agent: "nccgs-gameplay-programmer",
|
|
133
|
-
dependsOn: i === 8 ? Array.from({ length: 8 }, (_, j) => `part-${j}`) : [], writePaths: [`Assets/part-${i}.cs`], criteria: i === 0 ? ["AC-first", "AC-second", "OWNER"] : [] }));
|
|
134
|
-
variants.push({ ...base, slices: many, integration: "part-8" });
|
|
135
|
-
for (const plan of variants) {
|
|
136
|
-
f.write(f.draft, plan); assert.throws(() => planTaskSlices(f.project, "large", f.draft, true));
|
|
137
|
-
assert.equal(fs.existsSync(path.join(f.project, ".nccgs/orchestration.json")), false);
|
|
138
|
-
}
|
|
139
|
-
f.register(); const before = fs.readFileSync(path.join(f.project, ".nccgs/orchestration.json"));
|
|
140
|
-
const request = taskSliceStatus(f.project, "large").next;
|
|
141
|
-
for (const changed of [{ ...request, sliceId: "integrate", agent: base.slices[1].agent, writePaths: base.slices[1].writePaths },
|
|
142
|
-
{ ...request, sliceId: undefined }, { ...request, writePaths: ["Assets"] }, { ...request, agent: "nccgs-engine-programmer" }]) {
|
|
143
|
-
assert.throws(() => reserve(f.project, "large", changed));
|
|
144
|
-
assert.deepEqual(fs.readFileSync(path.join(f.project, ".nccgs/orchestration.json")), before);
|
|
145
|
-
}
|
|
146
|
-
} finally { f.dispose(); }
|
|
147
|
-
});
|
|
148
|
-
|
|
149
|
-
test("integrated slices require current full measurements, independent review of every contributor and original manual acceptance", () => {
|
|
150
|
-
const f = fixture();
|
|
151
|
-
try {
|
|
152
|
-
const original = f.task(); f.register();
|
|
153
|
-
const first = f.deliver(), earlyCheck = f.verify();
|
|
154
|
-
assert.throws(() => recordStandalone(f.project, f.task(), "implementation", f.stage(first, earlyCheck)), /All planned slices/);
|
|
155
|
-
assert.throws(() => f.review(), /implementation.*missing|stale/);
|
|
156
|
-
const integrated = f.deliver();
|
|
157
|
-
assert.match(integrated.prompt, /AC-first/); assert.match(integrated.prompt, /AC-second/); assert.match(integrated.prompt, /OWNER/);
|
|
158
|
-
assert.throws(() => recordStandalone(f.project, f.task(), "implementation", f.stage(integrated, earlyCheck)), /Verification execution failed or stale/);
|
|
159
|
-
const failedMeasure = f.verify(false);
|
|
160
|
-
recordStandalone(f.project, f.task(), "implementation", f.stage(integrated, failedMeasure));
|
|
161
|
-
const negative = f.review(1, undefined, "CHANGES_REQUIRED", [{ id: "C1", description: "Second original criterion is still false" }]);
|
|
162
|
-
negative.record(); assert.equal(f.task().stages.review.conditions[0].id, "C1");
|
|
163
|
-
assert.throws(() => recordStandalone(f.project, f.task(), "report", { outcome: "success", summary: "Cannot waive criterion" }), /criteria failed|not passed/);
|
|
164
|
-
// A fresh measured check, without mutating source or historical observations.
|
|
165
|
-
f.verify(true);
|
|
166
|
-
const formerContributor = f.review(2, first.agentId);
|
|
167
|
-
assert.throws(() => formerContributor.record(), /independent of every implementation contributor/);
|
|
168
|
-
const independent = f.review(3); independent.record();
|
|
169
|
-
assert.throws(() => reserve(f.project, "large", { ...first.request, requestKey: "no-extra-review",
|
|
170
|
-
handoffPaths: [".nccgs/evidence/analysis/no-extra-review.md"] }), /independent-review budget exhausted/);
|
|
171
|
-
recordStandalone(f.project, f.task(), "report", { outcome: "success", summary: "Synthetic technical result, manual decision pending" });
|
|
172
|
-
assert.throws(() => taskRun(["close", "--project", f.project, "--id", "large"]), /acceptance: missing/);
|
|
173
|
-
assert.deepEqual(f.task().contract, original.contract);
|
|
174
|
-
assert.equal(f.task().reviewBudget.limit, original.reviewBudget.limit); assert.equal(f.task().reviewBudget.used, 2);
|
|
175
|
-
assert.ok(f.task().history.some(h => h.stages?.review?.verdict === "CHANGES_REQUIRED"));
|
|
176
|
-
assert.equal(status(f.project, "large").usage.dispatches, 5);
|
|
177
|
-
} finally { f.dispose(); }
|
|
178
|
-
});
|
|
179
|
-
|
|
180
|
-
test("slice retries and revisions preserve failed attempts, invalidate dependent work and never reset budget", () => {
|
|
181
|
-
const f = fixture();
|
|
182
|
-
try {
|
|
183
|
-
const plan = f.plan(); f.register(plan);
|
|
184
|
-
const failed = f.deliver(undefined, { partial: true });
|
|
185
|
-
assert.equal(taskSliceStatus(f.project, "large").slices[0].state, "PARTIAL");
|
|
186
|
-
const revision = { ...plan, planId: "plan-two", supersedes: "plan-one", sourceSnapshot: f.snap(), reason: "Refine only unattempted integration", slices: structuredClone(plan.slices) };
|
|
187
|
-
revision.slices[1].objective = "Refined integration assignment";
|
|
188
|
-
f.register(revision); assert.equal(taskSliceStatus(f.project, "large").slices[0].attempts[0].dispatchId, failed.dispatch.id);
|
|
189
|
-
const changed = structuredClone(revision); changed.planId = "plan-three"; changed.supersedes = "plan-two";
|
|
190
|
-
changed.slices[0].id = "hide-first"; changed.slices[1].dependsOn = ["hide-first"];
|
|
191
|
-
assert.throws(() => f.register(changed), /attempted slice first cannot be removed or renamed/);
|
|
192
|
-
const first = f.deliver(), second = f.deliver();
|
|
193
|
-
assert.equal(taskSliceStatus(f.project, "large").slices[1].state, "DELIVERED");
|
|
194
|
-
const retry = { ...first.request, requestKey: "explicit-first-retry", handoffPaths: [".nccgs/evidence/analysis/first-retry.md"] };
|
|
195
|
-
f.deliver(retry);
|
|
196
|
-
const progress = taskSliceStatus(f.project, "large");
|
|
197
|
-
assert.equal(progress.slices[1].state, "STALE"); assert.equal(progress.next.sliceId, "integrate");
|
|
198
|
-
assert.equal(progress.slices[1].dispatchId, second.dispatch.id);
|
|
199
|
-
assert.equal(status(f.project, "large").usage.dispatches, 4);
|
|
200
|
-
const expanded = structuredClone(revision); expanded.planId = "plan-three"; expanded.supersedes = "plan-two"; expanded.sourceSnapshot = f.snap();
|
|
201
|
-
expanded.reason = "Explicitly refine the attempted slice without resetting its history"; expanded.slices[0].writePaths.push("Assets/focused-regression.cs");
|
|
202
|
-
f.register(expanded);
|
|
203
|
-
assert.equal(taskSliceStatus(f.project, "large").slices[0].state, "STALE");
|
|
204
|
-
assert.equal(taskSliceStatus(f.project, "large").slices[0].attempts.length, 3);
|
|
205
|
-
f.write(f.draft, plan); assert.equal(planTaskSlices(f.project, "large", f.draft, true).status, "SUPERSEDED");
|
|
206
|
-
assert.equal(taskSliceStatus(f.project, "large").planId, "plan-three");
|
|
207
|
-
assert.equal(f.task().history.length, 0); // Planning never rewrites lifecycle history/stages.
|
|
208
|
-
} finally { f.dispose(); }
|
|
209
|
-
});
|
|
210
|
-
|
|
211
|
-
test("slice allocation protects ancestor dispatch, cost and duration budgets", () => {
|
|
212
|
-
for (const metric of ["cost", "duration"]) {
|
|
213
|
-
const f = fixture(true);
|
|
214
|
-
try {
|
|
215
|
-
const request = { kind: "analysis", agent: "nccgs-task-scout", requestKey: "prior-parent", reason: "SYNTHETIC existing parent allocation", writePaths: [],
|
|
216
|
-
limits: { maxDispatches: 7, ...(metric === "cost" ? { maxCostUsd: 1 } : { maxDurationMs: 100 }) } };
|
|
217
|
-
const old = reserve(f.project, "parent", request);
|
|
218
|
-
rejectUnclaimed(f.project, old.id, { sessionId: "never-launched", toolUseId: "old", agent: request.agent, reason: "SYNTHETIC rejected launch, no refund" });
|
|
219
|
-
const registered = f.register();
|
|
220
|
-
assert.equal(registered.budgets.find(b => b.taskId === "parent").spareDispatches, 0);
|
|
221
|
-
assert.throws(() => reserve(f.project, "parent", { ...request, requestKey: "steal-integration" }), /cannot fit/);
|
|
222
|
-
assert.equal(status(f.project, "parent").usage.dispatches, 1);
|
|
223
|
-
f.deliver();
|
|
224
|
-
assert.throws(() => reserve(f.project, "large", taskSliceStatus(f.project, "large").next), metric === "cost" ? /missing costUsd telemetry/ : /maxDurationMs budget exhausted/);
|
|
225
|
-
assert.equal(status(f.project, "parent").limits[metric === "cost" ? "maxCostUsd" : "maxDurationMs"], metric === "cost" ? 1 : 100);
|
|
226
|
-
assert.equal(status(f.project, "parent").limits.maxDispatches, 7);
|
|
227
|
-
assert.equal(status(f.project, "parent").usage.dispatches, 2);
|
|
228
|
-
} finally { f.dispose(); }
|
|
229
|
-
}
|
|
230
|
-
});
|
|
231
|
-
|
|
232
|
-
test("integrated task slices retain the existing bounded repair and independent re-review path", () => {
|
|
233
|
-
const f = fixture();
|
|
234
|
-
try {
|
|
235
|
-
const original = f.task(); f.register(); f.deliver(); const integrated = f.deliver();
|
|
236
|
-
recordStandalone(f.project, f.task(), "implementation", f.stage(integrated, f.verify(false)));
|
|
237
|
-
f.review(1, undefined, "CHANGES_REQUIRED", [{ id: "FIX", description: "Correct second behavior without changing its criterion" }]).record();
|
|
238
|
-
const workPacket = ".nccgs/evidence/work-packets/repair.json";
|
|
239
|
-
f.write(workPacket, { schemaVersion: 1, taskId: "large", contractHash: f.task().contractHash,
|
|
240
|
-
sourceSnapshot: f.snap(), problem: "Synthetic second behavior fails the original criterion", solution: "Correct only second behavior",
|
|
241
|
-
changes: [{ path: "Assets/second.cs", instruction: "Apply the chosen correction" }], context: [], verification: ["integration-check"],
|
|
242
|
-
budget: { workCalls: 12, readCalls: 6, handoffCalls: 3, durationMs: 600000 } });
|
|
243
|
-
const repaired = f.deliver({ kind: "implementation", purpose: "repair", agent: "nccgs-gameplay-programmer", requestKey: "bounded-repair",
|
|
244
|
-
reason: "SYNTHETIC repair against recorded feedback", writePaths: ["Assets/second.cs"], workPacket,
|
|
245
|
-
handoffPaths: [".nccgs/evidence/analysis/repair-result.md"] });
|
|
246
|
-
recordStandalone(f.project, f.task(), "implementation", f.stage(repaired, f.verify(true)));
|
|
247
|
-
f.review(2).record();
|
|
248
|
-
assert.equal(f.task().stages.implementation.dispatchId, repaired.dispatch.id);
|
|
249
|
-
assert.equal(f.task().stages.review.verdict, "PASS");
|
|
250
|
-
assert.equal(f.task().reviewBudget.used, 2); assert.equal(f.task().reviewBudget.limit, original.reviewBudget.limit);
|
|
251
|
-
assert.deepEqual(f.task().contract, original.contract);
|
|
252
|
-
assert.ok(f.task().history.some(h => h.stages?.review?.verdict === "CHANGES_REQUIRED"));
|
|
253
|
-
assert.equal(taskSliceStatus(f.project, "large").next, null);
|
|
254
|
-
assert.equal(status(f.project, "large").usage.dispatches, 5);
|
|
255
|
-
} finally { f.dispose(); }
|
|
256
|
-
});
|
|
257
|
-
|
|
258
|
-
test("slice closure rejects missing native model, tampered handoffs or deleted plan history", () => {
|
|
259
|
-
for (const variant of ["model", "handoff", "plan", "history"]) {
|
|
260
|
-
const f = fixture();
|
|
261
|
-
try {
|
|
262
|
-
const registration = f.register(), delivered = f.deliver(undefined, { model: variant === "model" ? null : "claude-opus-5-5" });
|
|
263
|
-
if (variant === "handoff") f.write(delivered.request.handoffPaths[0], "SYNTHETIC tampering");
|
|
264
|
-
if (variant === "plan") f.write(registration.reference.path, "{}");
|
|
265
|
-
if (variant === "history") {
|
|
266
|
-
const ledger = json(path.join(f.project, ".nccgs/orchestration.json")); delete ledger.tasks.large.slicePlan; f.write(".nccgs/orchestration.json", ledger);
|
|
267
|
-
}
|
|
268
|
-
if (["model", "handoff"].includes(variant)) {
|
|
269
|
-
const state = taskSliceStatus(f.project, "large"); assert.equal(state.slices[0].state, "BLOCKED");
|
|
270
|
-
assert.equal(state.slices[1].ready, false);
|
|
271
|
-
} else assert.throws(() => taskSliceStatus(f.project, "large"), /stale artifact|history missing/);
|
|
272
|
-
} finally { f.dispose(); }
|
|
273
|
-
}
|
|
274
|
-
});
|
|
275
|
-
|
|
276
|
-
test("slice commands reject budget overrides and workers cannot register plans", () => {
|
|
277
|
-
const f = fixture();
|
|
278
|
-
try {
|
|
279
|
-
f.register(); const run = f.start(taskSliceStatus(f.project, "large").next);
|
|
280
|
-
for (const extra of [["--file", "second.json"], ["--budget", "999"], ["--", "node"]]) assert.throws(() => taskRun([
|
|
281
|
-
"slice-preview", "--project", f.project, "--id", "large", "--file", f.draft, ...extra]), /unique/);
|
|
282
|
-
assert.throws(() => guardTool(f.project, { session_id: "synthetic-session", agent_id: run.agentId, tool_name: "Bash", tool_input: {
|
|
283
|
-
command: `node .claude/nccgs/tools/task.mjs slice-plan --id large --file ${f.draft}` } }), /Coordinator owns lifecycle/);
|
|
284
|
-
} finally { f.dispose(); }
|
|
285
|
-
});
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
import os from "node:os";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import { fileURLToPath } from "node:url";
|
|
6
|
+
import test from "node:test";
|
|
7
|
+
import { artifact, json } from "../.claude/nccgs/tools/policy.mjs";
|
|
8
|
+
import { initializeStandalone, recordStandalone } from "../.claude/nccgs/tools/lifecycle.mjs";
|
|
9
|
+
import { reserve, status, planTaskSlices, taskSliceStatus, rejectUnclaimed, dispatchPrompt } from "../.claude/nccgs/tools/orchestration.mjs";
|
|
10
|
+
import { snapshot } from "../.claude/nccgs/tools/gates.mjs";
|
|
11
|
+
import { observe } from "../.claude/nccgs/tools/runtime.mjs";
|
|
12
|
+
import { guardTool } from "../.claude/nccgs/tools/tool-guard.mjs";
|
|
13
|
+
import { run as taskRun } from "../.claude/nccgs/tools/task.mjs";
|
|
14
|
+
import { slicePromptContext } from "../.claude/nccgs/tools/task-slices.mjs";
|
|
15
|
+
|
|
16
|
+
// All workers, model observations, design documents and measurements here are
|
|
17
|
+
// SYNTHETIC fixtures. No Claude provider or Unity process is launched.
|
|
18
|
+
const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
|
19
|
+
function fixture(parent = false) {
|
|
20
|
+
const temporaryRoot = fs.realpathSync(os.tmpdir()), project = fs.mkdtempSync(path.join(temporaryRoot, "nccgs-slices-"));
|
|
21
|
+
const write = (relative, value) => {
|
|
22
|
+
const file = path.join(project, relative); fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
23
|
+
fs.writeFileSync(file, typeof value === "string" || Buffer.isBuffer(value) ? value : JSON.stringify(value)); return artifact(project, relative);
|
|
24
|
+
};
|
|
25
|
+
for (const file of ["pipeline.json", "studio.json"]) write(`.claude/nccgs/${file}`, fs.readFileSync(path.join(root, ".claude/nccgs", file)));
|
|
26
|
+
write(".nccgs/project.yaml", "schema_version: 2\nmodels:\n profile: claude-standalone\n");
|
|
27
|
+
write(".nccgs/execution.json", { schemaVersion: 1, name: "standalone", externalReview: { mode: "off" }, acceptanceRequired: false,
|
|
28
|
+
requireObservedEffort: false, reviewContractRequired: true, maxParallelWorkers: 2 });
|
|
29
|
+
write(".nccgs/requirements.yaml", "schema_version: 1\nrequirements: []\n");
|
|
30
|
+
write("Assets/first.cs", "// SYNTHETIC first file\n"); write("Assets/second.cs", "// SYNTHETIC second file\n");
|
|
31
|
+
const acceptance = ["first", "second"].map(name => ({ id: `AC-${name}`, description: `Synthetic ${name} acceptance`, check: "integration-check",
|
|
32
|
+
assertions: [{ artifact: ".nccgs/evidence/measurements/result.json", pointer: `/${name}`, op: "eq", value: true }] }));
|
|
33
|
+
acceptance.push({ id: "OWNER", description: "Synthetic mandatory owner decision", manual: true });
|
|
34
|
+
write(".nccgs/intake.json", { schemaVersion: 1, mode: "implement", objective: "SYNTHETIC large task", scope: "feature", target: "production-change", readiness: "ready",
|
|
35
|
+
acceptance, requiredChecks: ["integration-check"], analysisArtifacts: [".nccgs/evidence/analysis/review-1.md", ".nccgs/evidence/analysis/review-2.md", ".nccgs/evidence/analysis/review-3.md"] });
|
|
36
|
+
if (parent) initializeStandalone(project, "parent", { intake: ".nccgs/intake.json" });
|
|
37
|
+
initializeStandalone(project, "large", { intake: ".nccgs/intake.json", ...(parent ? { parentId: "parent" } : {}) });
|
|
38
|
+
write(".nccgs/runtime/bindings/synthetic-session.json", { taskId: "large" });
|
|
39
|
+
const task = () => json(path.join(project, ".nccgs/tasks/large.json"));
|
|
40
|
+
const snap = () => snapshot(project, task()).sha256;
|
|
41
|
+
const draft = ".nccgs/evidence/slice-plans/plan.json";
|
|
42
|
+
const plan = () => ({ schemaVersion: 1, taskId: "large", contractHash: task().contractHash, sourceSnapshot: snap(), planId: "plan-one", supersedes: null,
|
|
43
|
+
reason: "SYNTHETIC bounded decomposition", integration: "integrate", slices: [
|
|
44
|
+
{ id: "first", objective: "Implement the first bounded part", agent: "nccgs-gameplay-programmer", dependsOn: [], writePaths: ["Assets/first.cs"], criteria: ["AC-first"] },
|
|
45
|
+
{ id: "integrate", objective: "Implement second part and integrate the full result", agent: "nccgs-gameplay-programmer", dependsOn: ["first"], writePaths: ["Assets/second.cs"], criteria: ["AC-second", "OWNER"] }
|
|
46
|
+
] });
|
|
47
|
+
const register = (p = plan()) => { write(draft, p); return planTaskSlices(project, "large", draft, true); };
|
|
48
|
+
let sequence = 0;
|
|
49
|
+
const start = (request, agentId) => {
|
|
50
|
+
const dispatch = reserve(project, "large", request), agent = agentId ?? `synthetic-agent-${++sequence}`;
|
|
51
|
+
const envelope = { hook_event_name: "PreToolUse", session_id: "synthetic-session", tool_name: "Agent", tool_use_id: `tool-${dispatch.id}`,
|
|
52
|
+
tool_input: { subagent_type: request.agent, prompt: `[NCCGS-DISPATCH:${dispatch.id}] Caller prose cannot replace the slice contract` } };
|
|
53
|
+
const prompt = guardTool(project, envelope)?.updatedInput?.prompt;
|
|
54
|
+
observe(project, { session_id: "synthetic-session", agent_id: agent, agent_type: request.agent }, "agent_start");
|
|
55
|
+
return { dispatch, request, envelope, agentId: agent, prompt };
|
|
56
|
+
};
|
|
57
|
+
const finish = (run, { partial = false, model = run.request.kind === "review" ? "claude-fable-5-1" : "claude-opus-5-5" } = {}) => observe(project, { ...run.envelope, tool_response: {
|
|
58
|
+
agentId: run.agentId, status: "completed", resolvedModel: model, modelsUsed: model ? [model] : [], totalDurationMs: 123, totalTokens: 45,
|
|
59
|
+
totalToolUseCount: 3, content: partial ? "SYNTHETIC partial" : "SYNTHETIC completed" } }, "agent_result");
|
|
60
|
+
const deliver = (request = taskSliceStatus(project, "large").next, options = {}) => {
|
|
61
|
+
const run = start(request);
|
|
62
|
+
if (!options.partial) for (const [index, file] of request.writePaths.entries()) {
|
|
63
|
+
guardTool(project, { session_id: "synthetic-session", agent_id: run.agentId, tool_use_id: `write-${run.dispatch.id}-${index}`, tool_name: "Write", tool_input: { file_path: file, content: "synthetic" } });
|
|
64
|
+
write(file, `// SYNTHETIC change from ${run.dispatch.id}\n`);
|
|
65
|
+
}
|
|
66
|
+
write(request.handoffPaths[0], `NCCGS-HANDOFF: ${options.partial ? "PARTIAL" : "COMPLETE"}\nSynthetic handoff, never acceptance proof.`);
|
|
67
|
+
return { ...run, observed: finish(run, options) };
|
|
68
|
+
};
|
|
69
|
+
const verify = (second = true) => {
|
|
70
|
+
write(".nccgs/verify.mjs", `import fs from 'node:fs'; fs.mkdirSync('.nccgs/evidence/measurements',{recursive:true}); fs.writeFileSync('.nccgs/evidence/measurements/result.json',JSON.stringify({first:true,second:${second}})); console.log('SYNTHETIC measurement fixture only');`);
|
|
71
|
+
return taskRun(["verify", "--project", project, "--id", "large", "--name", "integration-check", "--", process.execPath, ".nccgs/verify.mjs"]);
|
|
72
|
+
};
|
|
73
|
+
const stage = (run, verification) => ({ snapshot: run.observed.snapshot, dispatchId: run.dispatch.id, runtime: artifact(project, run.observed.path),
|
|
74
|
+
checks: [{ name: "integration-check", status: verification.status, evidence: verification.evidence }] });
|
|
75
|
+
const review = (number = 1, agentId, verdict = "PASS", conditions = []) => {
|
|
76
|
+
const file = `.nccgs/evidence/analysis/review-${number}.md`;
|
|
77
|
+
const run = start({ kind: "review", agent: "nccgs-adversarial-reviewer", requestKey: `review-${number}`, reason: "SYNTHETIC independent integrated review", writePaths: [], handoffPaths: [file] }, agentId);
|
|
78
|
+
write(file, `NCCGS-HANDOFF: COMPLETE\n\n\`\`\`nccgs-review\n${JSON.stringify({ schemaVersion: 1, taskId: "large", contractHash: task().contractHash, snapshot: snap(),
|
|
79
|
+
verdict, criteriaMet: verdict === "PASS", findings: [], conditions })}\n\`\`\`\n`);
|
|
80
|
+
const observed = finish(run);
|
|
81
|
+
return { ...run, observed, record: () => taskRun(["record-review", "--project", project, "--id", "large", "--dispatch", run.dispatch.id, "--file", file]) };
|
|
82
|
+
};
|
|
83
|
+
return { project, write, task, snap, draft, plan, register, start, finish, deliver, verify, stage, review,
|
|
84
|
+
dispose() {
|
|
85
|
+
assert.equal(path.dirname(project).toLowerCase(), temporaryRoot.toLowerCase()); assert.ok(path.basename(project).startsWith("nccgs-slices-"));
|
|
86
|
+
assert.equal(fs.lstatSync(project).isSymbolicLink(), false); assert.equal(fs.realpathSync(project).toLowerCase(), project.toLowerCase());
|
|
87
|
+
fs.rmSync(project, { recursive: true, force: true });
|
|
88
|
+
} };
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
test("slice preview/registration preserve the full task and budgets; injected contexts select original criteria", () => {
|
|
92
|
+
const f = fixture();
|
|
93
|
+
try {
|
|
94
|
+
const original = fs.readFileSync(path.join(f.project, ".nccgs/tasks/large.json")), plan = f.plan(); f.write(f.draft, plan);
|
|
95
|
+
const preview = planTaskSlices(f.project, "large", f.draft);
|
|
96
|
+
assert.equal(preview.status, "PREVIEW"); assert.equal(fs.existsSync(path.join(f.project, ".nccgs/orchestration.json")), false);
|
|
97
|
+
assert.equal(preview.budgets[0].minimumRemainingSlices, 2); assert.equal(preview.budgets[0].protectedSlots, 4);
|
|
98
|
+
const saved = f.register(plan), ledger = fs.readFileSync(path.join(f.project, ".nccgs/orchestration.json"));
|
|
99
|
+
assert.equal(f.register(plan).status, "ALREADY_REGISTERED");
|
|
100
|
+
assert.deepEqual(fs.readFileSync(path.join(f.project, ".nccgs/orchestration.json")), ledger);
|
|
101
|
+
assert.deepEqual(fs.readFileSync(path.join(f.project, ".nccgs/tasks/large.json")), original);
|
|
102
|
+
assert.equal(status(f.project, "large").usage.dispatches, 0);
|
|
103
|
+
const run = f.start(taskSliceStatus(f.project, "large").next);
|
|
104
|
+
assert.ok(run.prompt.startsWith(`[NCCGS-DISPATCH:${run.dispatch.id}]`));
|
|
105
|
+
assert.match(run.prompt, /AC-first/); assert.doesNotMatch(run.prompt, /AC-second/);
|
|
106
|
+
assert.match(run.prompt, /same task.*shared budget/i);
|
|
107
|
+
const contextTask = structuredClone(f.task());
|
|
108
|
+
contextTask.contract.design = { document: f.write(".nccgs/evidence/synthetic-design.json", { requirements: [
|
|
109
|
+
{ id: "REQ-A", criteria: ["AC-first"], rules: "Preserve exact fixed rule A" }, { id: "REQ-B", criteria: ["AC-second"], rules: "Fixed rule B" }
|
|
110
|
+
], decisions: [{ resolution: "Keep the global decision" }], outOfScope: ["No product changes"] }) };
|
|
111
|
+
const context = slicePromptContext(f.project, contextTask, run.dispatch);
|
|
112
|
+
assert.match(context, /Preserve exact fixed rule A/); assert.doesNotMatch(context, /Fixed rule B/); assert.match(context, /Keep the global decision/);
|
|
113
|
+
assert.throws(() => guardTool(f.project, { session_id: "synthetic-session", tool_name: "Write", tool_input: { file_path: saved.reference.path } }), /immutable/);
|
|
114
|
+
assert.throws(() => guardTool(f.project, { session_id: "synthetic-session", agent_id: run.agentId, tool_name: "Write", tool_input: { file_path: "Assets/second.cs" } }), /exact planned file/);
|
|
115
|
+
} finally { f.dispose(); }
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
test("slices reject omitted criteria, scope expansion, cycles and unaffordable decomposition without mutation", () => {
|
|
119
|
+
const f = fixture();
|
|
120
|
+
try {
|
|
121
|
+
const base = f.plan();
|
|
122
|
+
const variants = [
|
|
123
|
+
{ ...base, acceptance: [] }, { ...base, budgets: { maxDispatches: 999 } }, { ...base, risk: "FAST" }, { ...base, sourceSnapshot: "stale" },
|
|
124
|
+
{ ...base, slices: [base.slices[0], { ...base.slices[1], criteria: ["AC-second"] }] },
|
|
125
|
+
{ ...base, slices: [base.slices[0], { ...base.slices[1], criteria: ["AC-first", "AC-second", "OWNER"] }] },
|
|
126
|
+
{ ...base, slices: [{ ...base.slices[0], dependsOn: ["integrate"] }, base.slices[1]] },
|
|
127
|
+
{ ...base, slices: [base.slices[0], { ...base.slices[1], dependsOn: [] }] },
|
|
128
|
+
{ ...base, slices: [{ ...base.slices[0], writePaths: ["Assets"] }, base.slices[1]] },
|
|
129
|
+
{ ...base, slices: [{ ...base.slices[0], writePaths: [".nccgs/tasks/large.json"] }, base.slices[1]] },
|
|
130
|
+
{ ...base, slices: [{ ...base.slices[0], agent: "nccgs-adversarial-reviewer" }, base.slices[1]] }
|
|
131
|
+
];
|
|
132
|
+
const many = Array.from({ length: 9 }, (_, i) => ({ id: `part-${i}`, objective: "Synthetic small unit", agent: "nccgs-gameplay-programmer",
|
|
133
|
+
dependsOn: i === 8 ? Array.from({ length: 8 }, (_, j) => `part-${j}`) : [], writePaths: [`Assets/part-${i}.cs`], criteria: i === 0 ? ["AC-first", "AC-second", "OWNER"] : [] }));
|
|
134
|
+
variants.push({ ...base, slices: many, integration: "part-8" });
|
|
135
|
+
for (const plan of variants) {
|
|
136
|
+
f.write(f.draft, plan); assert.throws(() => planTaskSlices(f.project, "large", f.draft, true));
|
|
137
|
+
assert.equal(fs.existsSync(path.join(f.project, ".nccgs/orchestration.json")), false);
|
|
138
|
+
}
|
|
139
|
+
f.register(); const before = fs.readFileSync(path.join(f.project, ".nccgs/orchestration.json"));
|
|
140
|
+
const request = taskSliceStatus(f.project, "large").next;
|
|
141
|
+
for (const changed of [{ ...request, sliceId: "integrate", agent: base.slices[1].agent, writePaths: base.slices[1].writePaths },
|
|
142
|
+
{ ...request, sliceId: undefined }, { ...request, writePaths: ["Assets"] }, { ...request, agent: "nccgs-engine-programmer" }]) {
|
|
143
|
+
assert.throws(() => reserve(f.project, "large", changed));
|
|
144
|
+
assert.deepEqual(fs.readFileSync(path.join(f.project, ".nccgs/orchestration.json")), before);
|
|
145
|
+
}
|
|
146
|
+
} finally { f.dispose(); }
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
test("integrated slices require current full measurements, independent review of every contributor and original manual acceptance", () => {
|
|
150
|
+
const f = fixture();
|
|
151
|
+
try {
|
|
152
|
+
const original = f.task(); f.register();
|
|
153
|
+
const first = f.deliver(), earlyCheck = f.verify();
|
|
154
|
+
assert.throws(() => recordStandalone(f.project, f.task(), "implementation", f.stage(first, earlyCheck)), /All planned slices/);
|
|
155
|
+
assert.throws(() => f.review(), /implementation.*missing|stale/);
|
|
156
|
+
const integrated = f.deliver();
|
|
157
|
+
assert.match(integrated.prompt, /AC-first/); assert.match(integrated.prompt, /AC-second/); assert.match(integrated.prompt, /OWNER/);
|
|
158
|
+
assert.throws(() => recordStandalone(f.project, f.task(), "implementation", f.stage(integrated, earlyCheck)), /Verification execution failed or stale/);
|
|
159
|
+
const failedMeasure = f.verify(false);
|
|
160
|
+
recordStandalone(f.project, f.task(), "implementation", f.stage(integrated, failedMeasure));
|
|
161
|
+
const negative = f.review(1, undefined, "CHANGES_REQUIRED", [{ id: "C1", description: "Second original criterion is still false" }]);
|
|
162
|
+
negative.record(); assert.equal(f.task().stages.review.conditions[0].id, "C1");
|
|
163
|
+
assert.throws(() => recordStandalone(f.project, f.task(), "report", { outcome: "success", summary: "Cannot waive criterion" }), /criteria failed|not passed/);
|
|
164
|
+
// A fresh measured check, without mutating source or historical observations.
|
|
165
|
+
f.verify(true);
|
|
166
|
+
const formerContributor = f.review(2, first.agentId);
|
|
167
|
+
assert.throws(() => formerContributor.record(), /independent of every implementation contributor/);
|
|
168
|
+
const independent = f.review(3); independent.record();
|
|
169
|
+
assert.throws(() => reserve(f.project, "large", { ...first.request, requestKey: "no-extra-review",
|
|
170
|
+
handoffPaths: [".nccgs/evidence/analysis/no-extra-review.md"] }), /independent-review budget exhausted/);
|
|
171
|
+
recordStandalone(f.project, f.task(), "report", { outcome: "success", summary: "Synthetic technical result, manual decision pending" });
|
|
172
|
+
assert.throws(() => taskRun(["close", "--project", f.project, "--id", "large"]), /acceptance: missing/);
|
|
173
|
+
assert.deepEqual(f.task().contract, original.contract);
|
|
174
|
+
assert.equal(f.task().reviewBudget.limit, original.reviewBudget.limit); assert.equal(f.task().reviewBudget.used, 2);
|
|
175
|
+
assert.ok(f.task().history.some(h => h.stages?.review?.verdict === "CHANGES_REQUIRED"));
|
|
176
|
+
assert.equal(status(f.project, "large").usage.dispatches, 5);
|
|
177
|
+
} finally { f.dispose(); }
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
test("slice retries and revisions preserve failed attempts, invalidate dependent work and never reset budget", () => {
|
|
181
|
+
const f = fixture();
|
|
182
|
+
try {
|
|
183
|
+
const plan = f.plan(); f.register(plan);
|
|
184
|
+
const failed = f.deliver(undefined, { partial: true });
|
|
185
|
+
assert.equal(taskSliceStatus(f.project, "large").slices[0].state, "PARTIAL");
|
|
186
|
+
const revision = { ...plan, planId: "plan-two", supersedes: "plan-one", sourceSnapshot: f.snap(), reason: "Refine only unattempted integration", slices: structuredClone(plan.slices) };
|
|
187
|
+
revision.slices[1].objective = "Refined integration assignment";
|
|
188
|
+
f.register(revision); assert.equal(taskSliceStatus(f.project, "large").slices[0].attempts[0].dispatchId, failed.dispatch.id);
|
|
189
|
+
const changed = structuredClone(revision); changed.planId = "plan-three"; changed.supersedes = "plan-two";
|
|
190
|
+
changed.slices[0].id = "hide-first"; changed.slices[1].dependsOn = ["hide-first"];
|
|
191
|
+
assert.throws(() => f.register(changed), /attempted slice first cannot be removed or renamed/);
|
|
192
|
+
const first = f.deliver(), second = f.deliver();
|
|
193
|
+
assert.equal(taskSliceStatus(f.project, "large").slices[1].state, "DELIVERED");
|
|
194
|
+
const retry = { ...first.request, requestKey: "explicit-first-retry", handoffPaths: [".nccgs/evidence/analysis/first-retry.md"] };
|
|
195
|
+
f.deliver(retry);
|
|
196
|
+
const progress = taskSliceStatus(f.project, "large");
|
|
197
|
+
assert.equal(progress.slices[1].state, "STALE"); assert.equal(progress.next.sliceId, "integrate");
|
|
198
|
+
assert.equal(progress.slices[1].dispatchId, second.dispatch.id);
|
|
199
|
+
assert.equal(status(f.project, "large").usage.dispatches, 4);
|
|
200
|
+
const expanded = structuredClone(revision); expanded.planId = "plan-three"; expanded.supersedes = "plan-two"; expanded.sourceSnapshot = f.snap();
|
|
201
|
+
expanded.reason = "Explicitly refine the attempted slice without resetting its history"; expanded.slices[0].writePaths.push("Assets/focused-regression.cs");
|
|
202
|
+
f.register(expanded);
|
|
203
|
+
assert.equal(taskSliceStatus(f.project, "large").slices[0].state, "STALE");
|
|
204
|
+
assert.equal(taskSliceStatus(f.project, "large").slices[0].attempts.length, 3);
|
|
205
|
+
f.write(f.draft, plan); assert.equal(planTaskSlices(f.project, "large", f.draft, true).status, "SUPERSEDED");
|
|
206
|
+
assert.equal(taskSliceStatus(f.project, "large").planId, "plan-three");
|
|
207
|
+
assert.equal(f.task().history.length, 0); // Planning never rewrites lifecycle history/stages.
|
|
208
|
+
} finally { f.dispose(); }
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
test("slice allocation protects ancestor dispatch, cost and duration budgets", () => {
|
|
212
|
+
for (const metric of ["cost", "duration"]) {
|
|
213
|
+
const f = fixture(true);
|
|
214
|
+
try {
|
|
215
|
+
const request = { kind: "analysis", agent: "nccgs-task-scout", requestKey: "prior-parent", reason: "SYNTHETIC existing parent allocation", writePaths: [],
|
|
216
|
+
limits: { maxDispatches: 7, ...(metric === "cost" ? { maxCostUsd: 1 } : { maxDurationMs: 100 }) } };
|
|
217
|
+
const old = reserve(f.project, "parent", request);
|
|
218
|
+
rejectUnclaimed(f.project, old.id, { sessionId: "never-launched", toolUseId: "old", agent: request.agent, reason: "SYNTHETIC rejected launch, no refund" });
|
|
219
|
+
const registered = f.register();
|
|
220
|
+
assert.equal(registered.budgets.find(b => b.taskId === "parent").spareDispatches, 0);
|
|
221
|
+
assert.throws(() => reserve(f.project, "parent", { ...request, requestKey: "steal-integration" }), /cannot fit/);
|
|
222
|
+
assert.equal(status(f.project, "parent").usage.dispatches, 1);
|
|
223
|
+
f.deliver();
|
|
224
|
+
assert.throws(() => reserve(f.project, "large", taskSliceStatus(f.project, "large").next), metric === "cost" ? /missing costUsd telemetry/ : /maxDurationMs budget exhausted/);
|
|
225
|
+
assert.equal(status(f.project, "parent").limits[metric === "cost" ? "maxCostUsd" : "maxDurationMs"], metric === "cost" ? 1 : 100);
|
|
226
|
+
assert.equal(status(f.project, "parent").limits.maxDispatches, 7);
|
|
227
|
+
assert.equal(status(f.project, "parent").usage.dispatches, 2);
|
|
228
|
+
} finally { f.dispose(); }
|
|
229
|
+
}
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
test("integrated task slices retain the existing bounded repair and independent re-review path", () => {
|
|
233
|
+
const f = fixture();
|
|
234
|
+
try {
|
|
235
|
+
const original = f.task(); f.register(); f.deliver(); const integrated = f.deliver();
|
|
236
|
+
recordStandalone(f.project, f.task(), "implementation", f.stage(integrated, f.verify(false)));
|
|
237
|
+
f.review(1, undefined, "CHANGES_REQUIRED", [{ id: "FIX", description: "Correct second behavior without changing its criterion" }]).record();
|
|
238
|
+
const workPacket = ".nccgs/evidence/work-packets/repair.json";
|
|
239
|
+
f.write(workPacket, { schemaVersion: 1, taskId: "large", contractHash: f.task().contractHash,
|
|
240
|
+
sourceSnapshot: f.snap(), problem: "Synthetic second behavior fails the original criterion", solution: "Correct only second behavior",
|
|
241
|
+
changes: [{ path: "Assets/second.cs", instruction: "Apply the chosen correction" }], context: [], verification: ["integration-check"],
|
|
242
|
+
budget: { workCalls: 12, readCalls: 6, handoffCalls: 3, durationMs: 600000 } });
|
|
243
|
+
const repaired = f.deliver({ kind: "implementation", purpose: "repair", agent: "nccgs-gameplay-programmer", requestKey: "bounded-repair",
|
|
244
|
+
reason: "SYNTHETIC repair against recorded feedback", writePaths: ["Assets/second.cs"], workPacket,
|
|
245
|
+
handoffPaths: [".nccgs/evidence/analysis/repair-result.md"] });
|
|
246
|
+
recordStandalone(f.project, f.task(), "implementation", f.stage(repaired, f.verify(true)));
|
|
247
|
+
f.review(2).record();
|
|
248
|
+
assert.equal(f.task().stages.implementation.dispatchId, repaired.dispatch.id);
|
|
249
|
+
assert.equal(f.task().stages.review.verdict, "PASS");
|
|
250
|
+
assert.equal(f.task().reviewBudget.used, 2); assert.equal(f.task().reviewBudget.limit, original.reviewBudget.limit);
|
|
251
|
+
assert.deepEqual(f.task().contract, original.contract);
|
|
252
|
+
assert.ok(f.task().history.some(h => h.stages?.review?.verdict === "CHANGES_REQUIRED"));
|
|
253
|
+
assert.equal(taskSliceStatus(f.project, "large").next, null);
|
|
254
|
+
assert.equal(status(f.project, "large").usage.dispatches, 5);
|
|
255
|
+
} finally { f.dispose(); }
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
test("slice closure rejects missing native model, tampered handoffs or deleted plan history", () => {
|
|
259
|
+
for (const variant of ["model", "handoff", "plan", "history"]) {
|
|
260
|
+
const f = fixture();
|
|
261
|
+
try {
|
|
262
|
+
const registration = f.register(), delivered = f.deliver(undefined, { model: variant === "model" ? null : "claude-opus-5-5" });
|
|
263
|
+
if (variant === "handoff") f.write(delivered.request.handoffPaths[0], "SYNTHETIC tampering");
|
|
264
|
+
if (variant === "plan") f.write(registration.reference.path, "{}");
|
|
265
|
+
if (variant === "history") {
|
|
266
|
+
const ledger = json(path.join(f.project, ".nccgs/orchestration.json")); delete ledger.tasks.large.slicePlan; f.write(".nccgs/orchestration.json", ledger);
|
|
267
|
+
}
|
|
268
|
+
if (["model", "handoff"].includes(variant)) {
|
|
269
|
+
const state = taskSliceStatus(f.project, "large"); assert.equal(state.slices[0].state, "BLOCKED");
|
|
270
|
+
assert.equal(state.slices[1].ready, false);
|
|
271
|
+
} else assert.throws(() => taskSliceStatus(f.project, "large"), /stale artifact|history missing/);
|
|
272
|
+
} finally { f.dispose(); }
|
|
273
|
+
}
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
test("slice commands reject budget overrides and workers cannot register plans", () => {
|
|
277
|
+
const f = fixture();
|
|
278
|
+
try {
|
|
279
|
+
f.register(); const run = f.start(taskSliceStatus(f.project, "large").next);
|
|
280
|
+
for (const extra of [["--file", "second.json"], ["--budget", "999"], ["--", "node"]]) assert.throws(() => taskRun([
|
|
281
|
+
"slice-preview", "--project", f.project, "--id", "large", "--file", f.draft, ...extra]), /unique/);
|
|
282
|
+
assert.throws(() => guardTool(f.project, { session_id: "synthetic-session", agent_id: run.agentId, tool_name: "Bash", tool_input: {
|
|
283
|
+
command: `node .claude/nccgs/tools/task.mjs slice-plan --id large --file ${f.draft}` } }), /Coordinator owns lifecycle/);
|
|
284
|
+
} finally { f.dispose(); }
|
|
285
|
+
});
|