nccgs 1.3.0 → 2.0.0-rc.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (180) hide show
  1. package/.claude/agents/nccgs-adversarial-reviewer.md +17 -6
  2. package/.claude/agents/nccgs-engine-programmer.md +2 -2
  3. package/.claude/agents/nccgs-fast-implementer.md +30 -0
  4. package/.claude/agents/nccgs-game-designer.md +26 -0
  5. package/.claude/agents/nccgs-gameplay-programmer.md +3 -4
  6. package/.claude/agents/nccgs-qa-engineer.md +2 -2
  7. package/.claude/agents/nccgs-task-scout.md +24 -0
  8. package/.claude/agents/nccgs-ui-programmer.md +2 -2
  9. package/.claude/agents/nccgs-unity-build-specialist.md +2 -2
  10. package/.claude/agents/nccgs-unity-implementer.md +7 -6
  11. package/.claude/agents/nccgs-unity-systems-specialist.md +2 -2
  12. package/.claude/agents/nccgs-unity-ui-specialist.md +2 -2
  13. package/.claude/agents/nccgs-verification-engineer.md +2 -2
  14. package/.claude/nccgs/MEMORY.md +59 -0
  15. package/.claude/nccgs/THIRD_PARTY_NOTICES.md +16 -0
  16. package/.claude/nccgs/VERSION +1 -1
  17. package/.claude/nccgs/agent-library/nccgs-adversarial-reviewer.md +16 -5
  18. package/.claude/nccgs/agent-library/nccgs-fast-implementer.md +28 -0
  19. package/.claude/nccgs/agent-library/nccgs-game-designer.md +1 -2
  20. package/.claude/nccgs/agent-library/nccgs-gameplay-programmer.md +1 -2
  21. package/.claude/nccgs/agent-library/nccgs-task-scout.md +22 -0
  22. package/.claude/nccgs/agent-library/nccgs-unity-implementer.md +5 -4
  23. package/.claude/nccgs/constitution.md +44 -12
  24. package/.claude/nccgs/hooks/agent-audit.mjs +2 -2
  25. package/.claude/nccgs/hooks/closure-guard.mjs +31 -8
  26. package/.claude/nccgs/hooks/common.mjs +2 -1
  27. package/.claude/nccgs/hooks/runtime-observation.mjs +2 -2
  28. package/.claude/nccgs/hooks/session-start.mjs +11 -4
  29. package/.claude/nccgs/packages/ponytail/LICENSE +21 -0
  30. package/.claude/nccgs/packages/ponytail/NOTICE.md +32 -0
  31. package/.claude/nccgs/packages/ponytail/README.md +46 -0
  32. package/.claude/nccgs/packages/ponytail/complexity-review.md +47 -0
  33. package/.claude/nccgs/packages/ponytail/engineering.md +68 -0
  34. package/.claude/nccgs/packages/ponytail/manifest.json +24 -0
  35. package/.claude/nccgs/pipeline.json +92 -8
  36. package/.claude/nccgs/protocols/agent-contract.md +50 -1
  37. package/.claude/nccgs/protocols/astra-claude.md +46 -12
  38. package/.claude/nccgs/protocols/bounded-repair.md +118 -0
  39. package/.claude/nccgs/protocols/context-packets.md +5 -0
  40. package/.claude/nccgs/protocols/delivery-acceptance.md +105 -0
  41. package/.claude/nccgs/protocols/design-lifecycle.md +139 -0
  42. package/.claude/nccgs/protocols/evidence.md +11 -1
  43. package/.claude/nccgs/protocols/model-routing.md +44 -42
  44. package/.claude/nccgs/protocols/orchestration.md +46 -5
  45. package/.claude/nccgs/protocols/review-handoff.md +65 -0
  46. package/.claude/nccgs/protocols/standalone-gates.md +273 -0
  47. package/.claude/nccgs/protocols/standalone-pilot.md +144 -0
  48. package/.claude/nccgs/protocols/standalone.md +187 -0
  49. package/.claude/nccgs/protocols/task-autonomy.md +55 -0
  50. package/.claude/nccgs/protocols/task-gates.md +73 -7
  51. package/.claude/nccgs/routing.generated.md +25 -7
  52. package/.claude/nccgs/settings.fragment.json +153 -141
  53. package/.claude/nccgs/studio.json +135 -101
  54. package/.claude/nccgs/tools/autonomy.mjs +27 -0
  55. package/.claude/nccgs/tools/command-process.mjs +44 -0
  56. package/.claude/nccgs/tools/compile-policy.mjs +34 -4
  57. package/.claude/nccgs/tools/configure-models.mjs +13 -5
  58. package/.claude/nccgs/tools/criteria.mjs +121 -0
  59. package/.claude/nccgs/tools/delivery-budget.mjs +52 -0
  60. package/.claude/nccgs/tools/design-readiness.mjs +158 -0
  61. package/.claude/nccgs/tools/evidence.mjs +153 -0
  62. package/.claude/nccgs/tools/gates.mjs +151 -24
  63. package/.claude/nccgs/tools/inputs.mjs +73 -0
  64. package/.claude/nccgs/tools/lifecycle.mjs +521 -0
  65. package/.claude/nccgs/tools/native-completion.mjs +21 -0
  66. package/.claude/nccgs/tools/orchestration.mjs +430 -0
  67. package/.claude/nccgs/tools/paths.mjs +60 -0
  68. package/.claude/nccgs/tools/policy.mjs +58 -13
  69. package/.claude/nccgs/tools/process-lock.mjs +74 -0
  70. package/.claude/nccgs/tools/repair-packet.mjs +87 -0
  71. package/.claude/nccgs/tools/review-handoff.mjs +32 -0
  72. package/.claude/nccgs/tools/runtime.mjs +33 -7
  73. package/.claude/nccgs/tools/task.mjs +62 -31
  74. package/.claude/nccgs/tools/tool-guard.mjs +117 -0
  75. package/.claude/nccgs/tools/verification-jobs.mjs +161 -0
  76. package/.claude/nccgs/tools/verification-worker.mjs +71 -0
  77. package/.claude/nccgs/tools/windows-job.cs +96 -0
  78. package/.claude/nccgs/workflow-catalog.json +286 -26
  79. package/.claude/skills/accessibility-review/SKILL.md +12 -5
  80. package/.claude/skills/architecture-decision/SKILL.md +12 -5
  81. package/.claude/skills/asset-audit/SKILL.md +12 -5
  82. package/.claude/skills/audit/SKILL.md +12 -5
  83. package/.claude/skills/balance-review/SKILL.md +12 -5
  84. package/.claude/skills/bug-triage/SKILL.md +12 -5
  85. package/.claude/skills/bug-triage/references/astra.md +12 -5
  86. package/.claude/skills/closure/SKILL.md +12 -5
  87. package/.claude/skills/compatibility-review/SKILL.md +12 -5
  88. package/.claude/skills/context-pack/SKILL.md +12 -5
  89. package/.claude/skills/dependency-review/SKILL.md +12 -5
  90. package/.claude/skills/design/SKILL.md +14 -5
  91. package/.claude/skills/design-review/SKILL.md +14 -5
  92. package/.claude/skills/evidence-review/SKILL.md +12 -5
  93. package/.claude/skills/hotfix/SKILL.md +12 -5
  94. package/.claude/skills/hotfix/references/astra.md +12 -5
  95. package/.claude/skills/incident-recovery/SKILL.md +12 -5
  96. package/.claude/skills/incident-recovery/references/astra.md +12 -5
  97. package/.claude/skills/localize-game/SKILL.md +12 -5
  98. package/.claude/skills/localize-game/references/astra.md +12 -5
  99. package/.claude/skills/migrate-project/SKILL.md +12 -5
  100. package/.claude/skills/migrate-project/references/astra.md +12 -5
  101. package/.claude/skills/migrate-project/references/procedure.md +2 -1
  102. package/.claude/skills/milestone-review/SKILL.md +12 -5
  103. package/.claude/skills/nccgs-code-review/SKILL.md +12 -5
  104. package/.claude/skills/nccgs-code-review/references/astra.md +8 -2
  105. package/.claude/skills/performance-audit/SKILL.md +12 -5
  106. package/.claude/skills/plan-feature/SKILL.md +12 -5
  107. package/.claude/skills/playtest/SKILL.md +12 -5
  108. package/.claude/skills/playtest/references/astra.md +16 -5
  109. package/.claude/skills/project-stage/SKILL.md +12 -5
  110. package/.claude/skills/prototype-feature/SKILL.md +12 -5
  111. package/.claude/skills/prototype-feature/references/astra.md +16 -5
  112. package/.claude/skills/qa-plan/SKILL.md +12 -5
  113. package/.claude/skills/release/SKILL.md +12 -5
  114. package/.claude/skills/release/references/astra.md +12 -5
  115. package/.claude/skills/release-readiness/SKILL.md +12 -5
  116. package/.claude/skills/retrospective/SKILL.md +12 -5
  117. package/.claude/skills/review/SKILL.md +12 -5
  118. package/.claude/skills/review/references/astra.md +8 -2
  119. package/.claude/skills/security-audit/SKILL.md +12 -5
  120. package/.claude/skills/sprint-plan/SKILL.md +12 -5
  121. package/.claude/skills/status/SKILL.md +12 -5
  122. package/.claude/skills/status/references/astra.md +6 -0
  123. package/.claude/skills/story-readiness/SKILL.md +14 -5
  124. package/.claude/skills/test/SKILL.md +12 -5
  125. package/.claude/skills/test/references/astra.md +12 -5
  126. package/.claude/skills/ui-review/SKILL.md +12 -5
  127. package/.claude/skills/work/SKILL.md +12 -5
  128. package/.claude/skills/work/references/astra.md +17 -5
  129. package/CHANGELOG.md +42 -0
  130. package/CLAUDE.md +2 -0
  131. package/MEMORY.md +55 -0
  132. package/README.md +132 -180
  133. package/THIRD_PARTY_NOTICES.md +15 -0
  134. package/UPGRADING.md +91 -69
  135. package/VERSION +1 -1
  136. package/docs/ARCHITECTURE.md +19 -5
  137. package/docs/BOUNDED-REPAIR.md +11 -0
  138. package/docs/HUONG-DAN-MIGRATE-VA-SU-DUNG.md +46 -34
  139. package/docs/NCCGS-1.3.md +10 -122
  140. package/docs/NCCGS-1.4.1.md +149 -0
  141. package/docs/NCCGS-1.4.md +184 -0
  142. package/docs/NCCGS-1.5-IMPLEMENTATION-PLAN.md +378 -0
  143. package/docs/NCCGS-1.5-PILOT.md +130 -0
  144. package/docs/NCCGS-1.5.md +237 -0
  145. package/docs/PHASE-1-OPERATIONS.md +137 -0
  146. package/docs/PHASE-2-OPERATIONS.md +183 -0
  147. package/docs/PROJECT-POLICY.md +21 -9
  148. package/docs/RELEASE-STATUS.md +42 -0
  149. package/docs/WORKFLOWS.md +42 -24
  150. package/package.json +7 -3
  151. package/scaffold/.nccgs/autonomy.json +4 -0
  152. package/scaffold/.nccgs/execution.json +10 -0
  153. package/scaffold/.nccgs/inputs.json +6 -0
  154. package/scaffold/.nccgs/project.yaml +4 -2
  155. package/scaffold/.nccgs/templates/agent-handoff.md +2 -0
  156. package/scaffold/.nccgs/templates/design-contract.json +34 -0
  157. package/scaffold/.nccgs/templates/feature-contract.md +21 -5
  158. package/scaffold/.nccgs/templates/implementation-report.md +4 -1
  159. package/scaffold/.nccgs/templates/repair-packet.json +14 -0
  160. package/scripts/benchmark-snapshot.mjs +205 -0
  161. package/scripts/benchmark-v15.mjs +173 -0
  162. package/scripts/cli.mjs +129 -2
  163. package/scripts/doctor.mjs +128 -22
  164. package/scripts/install.mjs +96 -34
  165. package/scripts/run.mjs +72 -0
  166. package/scripts/supervise.mjs +124 -0
  167. package/scripts/validate.mjs +45 -4
  168. package/tests/autonomy.test.mjs +113 -0
  169. package/tests/benchmark-v15.test.mjs +65 -0
  170. package/tests/design-readiness.test.mjs +200 -0
  171. package/tests/evidence.test.mjs +186 -0
  172. package/tests/framework.test.mjs +333 -16
  173. package/tests/gates.test.mjs +313 -4
  174. package/tests/lifecycle-v15.test.mjs +300 -0
  175. package/tests/orchestration-v15.test.mjs +545 -0
  176. package/tests/phase1.test.mjs +324 -0
  177. package/tests/phase2.test.mjs +250 -0
  178. package/tests/policy-v15.test.mjs +220 -0
  179. package/tests/ponytail-package.test.mjs +75 -0
  180. package/tests/runtime-v15.test.mjs +624 -0
@@ -0,0 +1,545 @@
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 { spawn } from "node:child_process";
6
+ import { fileURLToPath, pathToFileURL } from "node:url";
7
+ import test from "node:test";
8
+ import { reserve, claim, complete, reconcile, status, checkpoint, resume, telemetry, validateStageDispatch, extendDispatch } from "../.claude/nccgs/tools/orchestration.mjs";
9
+ import { snapshot } from "../.claude/nccgs/tools/gates.mjs";
10
+ import { pinInputs } from "../.claude/nccgs/tools/inputs.mjs";
11
+ import { artifact, atomicJson, hash, json } from "../.claude/nccgs/tools/policy.mjs";
12
+ import { persistRuntimeObservation } from "../.claude/nccgs/tools/evidence.mjs";
13
+ import { observe } from "../.claude/nccgs/tools/runtime.mjs";
14
+ import { validateStandaloneRuntime, initializeStandalone, recordStandalone } from "../.claude/nccgs/tools/lifecycle.mjs";
15
+ import { guardTool } from "../.claude/nccgs/tools/tool-guard.mjs";
16
+
17
+ // These are synthetic framework fixtures, never Claude Code execution evidence.
18
+ const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
19
+ const moduleUrl = pathToFileURL(path.join(root, ".claude/nccgs/tools/orchestration.mjs")).href;
20
+ const agents = { implementation: "nccgs-gameplay-programmer", analysis: "nccgs-task-scout", review: "nccgs-adversarial-reviewer" };
21
+ function fixture() {
22
+ const temporaryRoot = fs.realpathSync(os.tmpdir());
23
+ const project = fs.mkdtempSync(path.join(temporaryRoot, "nccgs-orchestration-v15-"));
24
+ const write = (relative, value) => {
25
+ const file = path.join(project, relative);
26
+ fs.mkdirSync(path.dirname(file), { recursive: true });
27
+ fs.writeFileSync(file, Buffer.isBuffer(value) || typeof value === "string" ? value : `${JSON.stringify(value, null, 2)}\n`);
28
+ return artifact(project, relative);
29
+ };
30
+ for (const name of ["pipeline.json", "studio.json"]) write(`.claude/nccgs/${name}`, fs.readFileSync(path.join(root, ".claude/nccgs", name)));
31
+ write(".nccgs/project.yaml", "schema_version: 2\nmodels:\n profile: claude-standalone\n");
32
+ write(".nccgs/execution.json", { schemaVersion: 1, name: "standalone", externalReview: { mode: "off" }, acceptanceRequired: false, requireObservedEffort: false, maxParallelWorkers: 2 });
33
+ write("Assets/game.cs", "// TEST FIXTURE source, not a game implementation\n");
34
+ write(".nccgs/requirements.yaml", "schema_version: 1\nrequirements: []\n");
35
+ function task(id, overrides = {}) {
36
+ const mode = overrides.mode ?? "implement";
37
+ const contract = { schemaVersion: 1, revision: 1, mode, scope: "local", target: "production-change", acceptance: [], analysisArtifacts: [".nccgs/evidence/analysis/plan.md"] };
38
+ const value = { schemaVersion: 2, id, mode, risk: "STANDARD", profile: "claude-standalone", createdAt: new Date().toISOString(),
39
+ contract, contractHash: hash(JSON.stringify(contract)), reviewBudget: { used: 0, limit: 3, extensions: [] },
40
+ implementationRoute: { class: "standard", model: "claude-opus-5-5", effort: "medium" },
41
+ canonicalInputs: pinInputs(project), stages: {}, history: [], parentId: null, ...overrides };
42
+ write(`.nccgs/tasks/${id}.json`, value);
43
+ return value;
44
+ }
45
+ let serial = 0;
46
+ function request(kind, overrides = {}) {
47
+ return { requestKey: `fixture-request-${++serial}`, kind, agent: agents[kind], writePaths: kind === "implementation" ? ["Assets/game.cs"] : [],
48
+ reason: "TEST FIXTURE bounded work", ...overrides };
49
+ }
50
+ function start(id, req = request("analysis")) {
51
+ const reservation = reserve(project, id, req);
52
+ const identity = { sessionId: `session-${++serial}`, toolUseId: `tool-${serial}`, agent: req.agent, kind: req.kind };
53
+ claim(project, reservation.id, identity);
54
+ return { reservation, identity, request: req, taskId: id };
55
+ }
56
+ function observation(run, overrides = {}, rawOverrides = {}) {
57
+ const current = json(path.join(project, `.nccgs/tasks/${run.taskId}.json`));
58
+ const data = { schemaVersion: 1, origin: "runtime-observation", observedProvider: "claude-code",
59
+ dispatchId: run.reservation.id, taskId: run.taskId, contractHash: current.contractHash,
60
+ snapshot: snapshot(project).sha256, capturedAt: new Date().toISOString(),
61
+ sessionId: run.identity.sessionId, toolUseId: run.identity.toolUseId, agentId: `agent-${run.reservation.id}`,
62
+ agentType: run.request.agent, observedModel: "claude-opus-5-5", observedEffort: null, modelsUsed: ["claude-opus-5-5"],
63
+ status: "completed", durationMs: null, toolUseCount: null, totalTokens: null, costUsd: null,
64
+ provenance: { kind: "provider-log", reference: "SYNTHETIC TEST FIXTURE ONLY; not a real provider run" }, ...overrides };
65
+ const source = write(`.nccgs/evidence/runtime/sources/fixture-${++serial}.json`, { ...data, ...rawOverrides });
66
+ return persistRuntimeObservation(project, { ...data, observationSource: source }, source);
67
+ }
68
+ return { project, write, task, request, start, observation,
69
+ dispose() {
70
+ const resolved = path.resolve(project);
71
+ assert.equal(path.dirname(resolved).toLowerCase(), temporaryRoot.toLowerCase());
72
+ assert.ok(path.basename(resolved).startsWith("nccgs-orchestration-v15-"));
73
+ assert.equal(fs.lstatSync(resolved).isSymbolicLink(), false);
74
+ assert.equal(fs.realpathSync(resolved).toLowerCase(), resolved.toLowerCase());
75
+ fs.rmSync(resolved, { recursive: true, force: true });
76
+ } };
77
+ }
78
+ function external(action) {
79
+ const saved = [process.env.CLAUDECODE, process.env.CLAUDE_CODE_ENTRYPOINT];
80
+ delete process.env.CLAUDECODE; delete process.env.CLAUDE_CODE_ENTRYPOINT;
81
+ try { return action(); } finally {
82
+ for (const [index, name] of ["CLAUDECODE", "CLAUDE_CODE_ENTRYPOINT"].entries()) {
83
+ if (saved[index] === undefined) delete process.env[name]; else process.env[name] = saved[index];
84
+ }
85
+ }
86
+ }
87
+ function failRun(f, run) {
88
+ return external(() => reconcile(f.project, run.reservation.id, { status: "failed", reason: "TEST FIXTURE process confirmed terminated", reference: "TEST FIXTURE external process-exit receipt" }));
89
+ }
90
+ test("external dispatch extension keeps spent attempts and parent cap, and rejects live or native changes", () => {
91
+ const f = fixture();
92
+ try {
93
+ f.task("parent"); f.task("child", { parentId: "parent" });
94
+ const parent = f.start("parent", f.request("analysis", { limits: { maxDispatches: 2 } }));
95
+ failRun(f, parent);
96
+ const child = f.start("child", f.request("analysis", { limits: { maxDispatches: 1 } }));
97
+ const decision = { previousLimit: 1, maxDispatches: 2, reason: "External fixture allocation", reference: f.write(".nccgs/evidence/authority.json", { fixture: true }) };
98
+ assert.throws(() => guardTool(f.project, { session_id: "coordinator", tool_name: "Bash", tool_input: {
99
+ command: "node .claude/nccgs/tools/task.mjs extend-dispatch --id child --request .nccgs/evidence/authority.json"
100
+ } }), /External decisions/);
101
+ assert.throws(() => external(() => extendDispatch(f.project, "child", decision)), /active dispatches/);
102
+ failRun(f, child);
103
+ external(() => {
104
+ process.env.CLAUDECODE = "1";
105
+ try { assert.throws(() => extendDispatch(f.project, "child", decision), /external operator/); }
106
+ finally { delete process.env.CLAUDECODE; }
107
+ assert.throws(() => extendDispatch(f.project, "child", { ...decision, previousLimit: 0 }), /stale/);
108
+ assert.throws(() => extendDispatch(f.project, "child", { ...decision, maxDispatches: 1 }), /increase/);
109
+ extendDispatch(f.project, "child", decision);
110
+ });
111
+ assert.equal(status(f.project, "child").usage.dispatches, 1);
112
+ assert.equal(status(f.project, "child").limits.maxDispatches, 2);
113
+ assert.throws(() => reserve(f.project, "child", f.request("analysis")), /budget exhausted for parent/);
114
+ assert.equal(json(path.join(f.project, ".nccgs/orchestration.json")).events.filter(e => e.action === "extend-dispatch").length, 1);
115
+ } finally { f.dispose(); }
116
+ });
117
+
118
+ test("native invocation completion cannot replace a declared missing handoff", () => {
119
+ const f = fixture();
120
+ try {
121
+ const task = f.task("handoff"), request = f.request("analysis", { handoffPaths: [".nccgs/evidence/analysis/plan.md"] });
122
+ const run = f.start("handoff", request), runtime = f.observation(run);
123
+ const completed = complete(f.project, run.reservation.id, { runtime });
124
+ assert.equal(completed.status, "failed"); assert.equal(completed.workStatus, "incomplete"); assert.equal(completed.invocationStatus, "completed");
125
+ assert.equal(completed.handoff.status, "missing"); assert.equal(status(f.project, "handoff").usage.dispatches, 1);
126
+ const trace = json(path.join(f.project, runtime.path));
127
+ assert.throws(() => validateStageDispatch(f.project, task, { dispatchId: completed.id, snapshot: completed.completedSnapshot, runtime }, trace, "analysis"), /handoff artifacts are missing/);
128
+ f.write(".nccgs/evidence/analysis/plan.md", "Late text is not the original worker handoff");
129
+ assert.equal(reserve(f.project, "handoff", request).id, completed.id);
130
+ assert.throws(() => validateStageDispatch(f.project, task, { dispatchId: completed.id, snapshot: completed.completedSnapshot, runtime }, trace, "analysis"), /handoff artifacts are missing/);
131
+ } finally { f.dispose(); }
132
+ });
133
+ async function simultaneousReservations(project, id, requests) {
134
+ const code = `import { reserve } from ${JSON.stringify(moduleUrl)};
135
+ process.on('message', message => {
136
+ if (message !== 'go') return;
137
+ try { process.send({ result: { ok: true, value: reserve(process.argv[1], process.argv[2], JSON.parse(process.argv[3])) } }); }
138
+ catch (error) { process.send({ result: { ok: false, error: error.message } }); }
139
+ process.disconnect();
140
+ });
141
+ process.send({ ready: true });`;
142
+ const children = [];
143
+ try {
144
+ const workers = requests.map(item => {
145
+ const taskId = item.taskId ?? id, request = item.request ?? item;
146
+ const child = spawn(process.execPath, ["--input-type=module", "-e", code, project, taskId, JSON.stringify(request)], { windowsHide: true, stdio: ["ignore", "pipe", "pipe", "ipc"] });
147
+ children.push(child);
148
+ let stderr = "";
149
+ child.stderr.on("data", chunk => { stderr += chunk; });
150
+ let received;
151
+ let readyResolve;
152
+ const ready = new Promise(resolve => { readyResolve = resolve; });
153
+ const result = new Promise((resolve, reject) => {
154
+ const timer = setTimeout(() => { child.kill(); reject(new Error("Race subprocess timeout")); }, 20000);
155
+ child.on("message", message => { if (message.ready) readyResolve(); if (message.result) received = message.result; });
156
+ child.on("error", error => { clearTimeout(timer); readyResolve(); reject(error); });
157
+ child.on("exit", code => {
158
+ clearTimeout(timer); readyResolve();
159
+ if (code !== 0 || !received) reject(new Error(`Race subprocess failed (${code}): ${stderr}`)); else resolve(received);
160
+ });
161
+ });
162
+ return { child, ready, result };
163
+ });
164
+ await Promise.all(workers.map(worker => worker.ready));
165
+ for (const worker of workers) if (worker.child.connected) worker.child.send("go");
166
+ return await Promise.all(workers.map(worker => worker.result));
167
+ } finally { for (const child of children) if (child.exitCode === null) child.kill(); }
168
+ }
169
+
170
+ test("reservation retry is idempotent but cannot replace an existing request", () => {
171
+ const f = fixture();
172
+ try {
173
+ f.task("task");
174
+ const request = f.request("analysis");
175
+ const first = reserve(f.project, "task", request);
176
+ assert.equal(reserve(f.project, "task", structuredClone(request)).id, first.id);
177
+ assert.equal(status(f.project, "task").reservations.length, 1);
178
+ assert.throws(() => reserve(f.project, "task", { ...request, reason: "Changed request under reused key" }), /key|request|duplicate|different/i);
179
+ assert.throws(() => reserve(f.project, "task", { ...request, requestKey: "" }), /key|request/i);
180
+ } finally { f.dispose(); }
181
+ });
182
+
183
+ test("project path ownership covers descendants and Windows case aliases", () => {
184
+ const f = fixture();
185
+ try {
186
+ for (const id of ["owner", "other"]) f.task(id);
187
+ reserve(f.project, "owner", f.request("implementation", { writePaths: ["Assets/Actors"] }));
188
+ assert.throws(() => reserve(f.project, "other", f.request("implementation", { writePaths: ["Assets/Actors/player.cs"] })), /path|writer|overlap|conflict/i);
189
+ if (process.platform === "win32") assert.throws(() => reserve(f.project, "other", f.request("implementation", { writePaths: ["assets/actors/player.cs"] })), /path|writer|overlap|conflict/i);
190
+ assert.throws(() => reserve(f.project, "other", f.request("implementation", { writePaths: ["../outside.cs"] })), /path|relative|outside|travers/i);
191
+ assert.equal(status(f.project, "other").reservations.length, 0);
192
+ } finally { f.dispose(); }
193
+ });
194
+
195
+ test("no more than two implementation workers reserve across separate tasks", () => {
196
+ const f = fixture();
197
+ try {
198
+ for (const id of ["one", "two", "three"]) f.task(id);
199
+ reserve(f.project, "one", f.request("implementation", { writePaths: ["Assets/One"] }));
200
+ reserve(f.project, "two", f.request("implementation", { writePaths: ["Assets/Two"] }));
201
+ assert.throws(() => reserve(f.project, "three", f.request("implementation", { writePaths: ["Assets/Three"] })), /parallel|worker|concurr|limit/i);
202
+ } finally { f.dispose(); }
203
+ });
204
+
205
+ test("audit and plan cannot reserve source writers; read-only roles cannot become implementers", () => {
206
+ const f = fixture();
207
+ try {
208
+ // Check role capability before reserving any other read-only work, so a
209
+ // concurrency conflict cannot accidentally satisfy the role assertion.
210
+ f.task("code");
211
+ assert.throws(() => reserve(f.project, "code", f.request("implementation", { agent: agents.analysis })), /read.only|role|agent|implement/i);
212
+ for (const mode of ["audit", "plan"]) {
213
+ f.task(mode, { mode });
214
+ assert.throws(() => reserve(f.project, mode, f.request("implementation")), /mode|read.only|implement|audit|plan/i);
215
+ assert.throws(() => reserve(f.project, mode, f.request("analysis", { writePaths: ["Assets/game.cs"] })), /path|read.only|analysis|writ/i);
216
+ assert.ok(reserve(f.project, mode, f.request("analysis")).id);
217
+ }
218
+ } finally { f.dispose(); }
219
+ });
220
+
221
+ test("an already exhausted recorded review budget blocks a first managed reviewer", () => {
222
+ const f = fixture();
223
+ try {
224
+ f.task("task", { reviewBudget: { used: 3, limit: 3, extensions: [] } });
225
+ assert.throws(() => reserve(f.project, "task", f.request("review")), /review|budget|limit|exhaust/i);
226
+ assert.equal(status(f.project, "task").reservations.length, 0);
227
+ } finally { f.dispose(); }
228
+ });
229
+
230
+ test("two subprocess reviewers racing for the last round cannot both reserve", async () => {
231
+ const f = fixture();
232
+ try {
233
+ f.write(".nccgs/intake.json", { mode: "implement", objective: "Synthetic concurrency fixture", scope: "feature", readiness: "ready", requiredChecks: [] });
234
+ let task = initializeStandalone(f.project, "task", { intake: ".nccgs/intake.json" });
235
+ const run = f.start("task", f.request("implementation")), runtime = f.observation(run);
236
+ const done = complete(f.project, run.reservation.id, { runtime });
237
+ const evidence = f.write(".nccgs/evidence/synthetic-check.txt", "SYNTHETIC verification fixture");
238
+ task = recordStandalone(f.project, task, "implementation", { dispatchId: done.id, snapshot: done.completedSnapshot, runtime, checks: [{ name: "synthetic-check", status: "PASS", evidence }] });
239
+ task.reviewBudget.used = task.reviewBudget.limit - 1;
240
+ f.write(".nccgs/tasks/task.json", task);
241
+ const results = await simultaneousReservations(f.project, "task", [f.request("review"), f.request("review")]);
242
+ assert.equal(results.filter(result => result.ok).length, 1, JSON.stringify(results));
243
+ assert.equal(status(f.project, "task").active.filter(item => item.kind === "review").length, 1);
244
+ } finally { f.dispose(); }
245
+ });
246
+
247
+ test("claim identities and exact completion retries cannot duplicate accounting", () => {
248
+ const f = fixture();
249
+ try {
250
+ f.task("task");
251
+ const run = f.start("task");
252
+ claim(f.project, run.reservation.id, run.identity);
253
+ assert.throws(() => claim(f.project, run.reservation.id, { ...run.identity, toolUseId: "different-tool" }), /claim|identity|running|bound|dispatch/i);
254
+ const runtime = f.observation(run);
255
+ complete(f.project, run.reservation.id, { runtime });
256
+ const before = status(f.project, "task").usage;
257
+ complete(f.project, run.reservation.id, { runtime });
258
+ assert.deepEqual(status(f.project, "task").usage, before);
259
+ assert.equal(status(f.project, "task").active.length, 0);
260
+ assert.equal(reserve(f.project, "task", run.request).id, run.reservation.id);
261
+ assert.equal(telemetry(f.project, "task").attempts, 1);
262
+ } finally { f.dispose(); }
263
+ });
264
+
265
+ test("child dispatches consume the ancestor budget and new children cannot mint quota", () => {
266
+ const f = fixture();
267
+ try {
268
+ f.task("parent"); f.task("child", { parentId: "parent" }); f.task("new-child", { parentId: "parent" });
269
+ const parent = f.start("parent", f.request("analysis", { limits: { maxDispatches: 2 } }));
270
+ complete(f.project, parent.reservation.id, { runtime: f.observation(parent) });
271
+ const child = f.start("child");
272
+ failRun(f, child);
273
+ assert.equal(status(f.project, "parent").usage.dispatches, 2);
274
+ assert.equal(telemetry(f.project, "parent").attempts, 2);
275
+ assert.equal(telemetry(f.project, "parent").metrics.durationMs.expected, 2);
276
+ assert.throws(() => reserve(f.project, "new-child", f.request("analysis")), /budget|quota|dispatch|limit/i);
277
+ assert.throws(() => reserve(f.project, "parent", f.request("analysis", { limits: { maxDispatches: 20 } })), /budget|quota|limit|extend|chang/i);
278
+ } finally { f.dispose(); }
279
+ });
280
+
281
+ test("separate child processes racing for the last ancestor dispatch share one quota", async () => {
282
+ const f = fixture();
283
+ try {
284
+ f.task("parent"); f.task("first", { parentId: "parent" }); f.task("second", { parentId: "parent" });
285
+ const parent = f.start("parent", f.request("analysis", { limits: { maxDispatches: 2 } }));
286
+ complete(f.project, parent.reservation.id, { runtime: f.observation(parent) });
287
+ const results = await simultaneousReservations(f.project, "parent", [
288
+ { taskId: "first", request: f.request("analysis") }, { taskId: "second", request: f.request("analysis") }
289
+ ]);
290
+ assert.equal(results.filter(result => result.ok).length, 1, JSON.stringify(results));
291
+ assert.equal(status(f.project, "parent").usage.dispatches, 2);
292
+ } finally { f.dispose(); }
293
+ });
294
+
295
+ test("checkpoint and resume do not free an uncertain run or reset consumed budget", () => {
296
+ const f = fixture();
297
+ try {
298
+ f.task("task");
299
+ const run = f.start("task", f.request("implementation", { limits: { maxDispatches: 1 } }));
300
+ const before = status(f.project, "task");
301
+ checkpoint(f.project, "task", { reason: "TEST FIXTURE interrupted coordinator" });
302
+ resume(f.project, "task");
303
+ const after = status(f.project, "task");
304
+ assert.deepEqual(after.usage, before.usage);
305
+ assert.ok(after.active.some(item => item.id === run.reservation.id));
306
+ assert.throws(() => reserve(f.project, "task", f.request("implementation")), /budget|quota|dispatch|limit|path|writer/i);
307
+ failRun(f, run);
308
+ assert.equal(status(f.project, "task").active.length, 0);
309
+ assert.throws(() => reserve(f.project, "task", f.request("analysis")), /budget|quota|dispatch|limit/i);
310
+ } finally { f.dispose(); }
311
+ });
312
+
313
+ test("external reconciliation needs provenance and is rejected inside Claude", () => {
314
+ const f = fixture();
315
+ try {
316
+ f.task("task");
317
+ const run = f.start("task");
318
+ external(() => assert.throws(() => reconcile(f.project, run.reservation.id, { status: "failed", reason: "", reference: "" }), /reason|reference|provenance/i));
319
+ const original = process.env.CLAUDECODE;
320
+ process.env.CLAUDECODE = "1";
321
+ try { assert.throws(() => reconcile(f.project, run.reservation.id, { status: "failed", reason: "Self-declared stop", reference: "self" }), /external|Claude|operator/i); }
322
+ finally { if (original === undefined) delete process.env.CLAUDECODE; else process.env.CLAUDECODE = original; }
323
+ assert.equal(status(f.project, "task").active.length, 1);
324
+ } finally { f.dispose(); }
325
+ });
326
+
327
+ test("source or contract changes before claim make the reservation stale", () => {
328
+ const f = fixture();
329
+ try {
330
+ f.task("source");
331
+ const source = reserve(f.project, "source", f.request("analysis"));
332
+ f.write("Assets/game.cs", "// changed after reserve\n");
333
+ assert.throws(() => claim(f.project, source.id, { sessionId: "s", toolUseId: "u", agent: agents.analysis }), /stale|snapshot|changed/i);
334
+ f.task("contract");
335
+ const contract = reserve(f.project, "contract", f.request("analysis"));
336
+ const changed = json(path.join(f.project, ".nccgs/tasks/contract.json"));
337
+ changed.contractHash = hash("changed contract");
338
+ atomicJson(path.join(f.project, ".nccgs/tasks/contract.json"), changed);
339
+ assert.throws(() => claim(f.project, contract.id, { sessionId: "s", toolUseId: "v", agent: agents.analysis }), /contract|stale|changed/i);
340
+ } finally { f.dispose(); }
341
+ });
342
+
343
+ test("completion rejects stale, mismatched or corrupt durable evidence without releasing the slot", () => {
344
+ const f = fixture();
345
+ try {
346
+ f.task("task");
347
+ const run = f.start("task");
348
+ for (const override of [{ sessionId: "foreign-session" }, { toolUseId: "foreign-tool" }, { dispatchId: "foreign-dispatch" }, { taskId: "foreign-task" }, { contractHash: "foreign-contract" }, { snapshot: "stale-snapshot" }, { observedProvider: "codex" }]) {
349
+ assert.throws(() => complete(f.project, run.reservation.id, { runtime: f.observation(run, override) }), /identity|session|tool|dispatch|task|contract|snapshot|provider|Claude|stale|match/i);
350
+ assert.equal(status(f.project, "task").active.length, 1);
351
+ }
352
+ assert.throws(() => complete(f.project, run.reservation.id, { runtime: f.observation(run, { durationMs: 0 }, { durationMs: 100 }) }), /source|observation|duration|telemetry|match/i);
353
+ const runtime = f.observation(run);
354
+ const observation = json(path.join(f.project, runtime.path));
355
+ fs.appendFileSync(path.join(f.project, observation.observationSource.path), "\ncorruption");
356
+ assert.throws(() => complete(f.project, run.reservation.id, { runtime }), /artifact|hash|source|stale|JSON/i);
357
+ assert.equal(status(f.project, "task").active.length, 1);
358
+ } finally { f.dispose(); }
359
+ });
360
+
361
+ test("an analysis worker source mutation is retained as failure, never successful completion", () => {
362
+ const f = fixture();
363
+ try {
364
+ f.task("task", { mode: "audit" });
365
+ const run = f.start("task");
366
+ f.write("Assets/game.cs", "// forbidden analysis mutation\n");
367
+ complete(f.project, run.reservation.id, { runtime: f.observation(run) });
368
+ const entry = status(f.project, "task").reservations.find(item => item.id === run.reservation.id);
369
+ assert.equal(entry.status, "failed");
370
+ assert.equal(status(f.project, "task").usage.dispatches, 1);
371
+ } finally { f.dispose(); }
372
+ });
373
+
374
+ test("a failed provider observation releases concurrency but still consumes its dispatch", () => {
375
+ const f = fixture();
376
+ try {
377
+ f.task("task");
378
+ const run = f.start("task", f.request("implementation", { limits: { maxDispatches: 1 } }));
379
+ complete(f.project, run.reservation.id, { runtime: f.observation(run, { status: "failed", durationMs: 17 }) });
380
+ const result = status(f.project, "task");
381
+ assert.equal(result.active.length, 0);
382
+ assert.equal(result.reservations[0].status, "failed");
383
+ assert.equal(result.usage.dispatches, 1);
384
+ assert.equal(telemetry(f.project, "task").metrics.durationMs.total, 17);
385
+ assert.throws(() => reserve(f.project, "task", f.request("implementation")), /budget|quota|dispatch|limit/i);
386
+ } finally { f.dispose(); }
387
+ });
388
+
389
+ test("unknown telemetry remains null while observed zero stays measured zero", () => {
390
+ const f = fixture();
391
+ try {
392
+ f.task("unknown");
393
+ const unknown = f.start("unknown");
394
+ complete(f.project, unknown.reservation.id, { runtime: f.observation(unknown) });
395
+ const missing = telemetry(f.project, "unknown");
396
+ for (const key of ["durationMs", "toolUseCount", "totalTokens", "costUsd"]) {
397
+ assert.equal(missing.metrics[key].total, null, key);
398
+ assert.equal(missing.metrics[key].measured, 0, key);
399
+ assert.equal(missing.metrics[key].expected, 1, key);
400
+ assert.equal(missing.metrics[key].coverage, 0, key);
401
+ }
402
+ f.task("zero");
403
+ const zero = f.start("zero");
404
+ complete(f.project, zero.reservation.id, { runtime: f.observation(zero, { durationMs: 0, toolUseCount: 0, totalTokens: 0, costUsd: 0 }) });
405
+ for (const key of ["durationMs", "toolUseCount", "totalTokens", "costUsd"]) {
406
+ assert.equal(telemetry(f.project, "zero").metrics[key].total, 0, key);
407
+ assert.equal(telemetry(f.project, "zero").metrics[key].measured, 1, key);
408
+ }
409
+ } finally { f.dispose(); }
410
+ });
411
+
412
+ test("corrupt orchestration ledger fails closed instead of resetting quotas", () => {
413
+ const f = fixture();
414
+ try {
415
+ f.task("task");
416
+ reserve(f.project, "task", f.request("analysis", { limits: { maxDispatches: 1 } }));
417
+ f.write(".nccgs/orchestration.json", "{ broken durable ledger");
418
+ assert.throws(() => status(f.project, "task"), /JSON|corrupt|ledger|Unexpected|property/i);
419
+ assert.throws(() => reserve(f.project, "task", f.request("analysis")), /JSON|corrupt|ledger|Unexpected|property/i);
420
+ assert.equal(fs.readFileSync(path.join(f.project, ".nccgs/orchestration.json"), "utf8"), "{ broken durable ledger");
421
+ } finally { f.dispose(); }
422
+ });
423
+
424
+ test("losing an initialized ledger cannot silently restore exhausted dispatch quota", () => {
425
+ const f = fixture();
426
+ try {
427
+ f.task("task");
428
+ const run = f.start("task", f.request("analysis", { limits: { maxDispatches: 1 } }));
429
+ complete(f.project, run.reservation.id, { runtime: f.observation(run) });
430
+ fs.unlinkSync(path.join(f.project, ".nccgs/orchestration.json"));
431
+ assert.throws(() => reserve(f.project, "task", f.request("analysis")), /ledger|missing|history|recover|initial/i);
432
+ } finally { f.dispose(); }
433
+ });
434
+
435
+ test("a lost dispatch entry with surviving history cannot refund spent quota", () => {
436
+ const f = fixture();
437
+ try {
438
+ f.task("task");
439
+ const run = f.start("task", f.request("analysis", { limits: { maxDispatches: 1 } }));
440
+ complete(f.project, run.reservation.id, { runtime: f.observation(run) });
441
+ const file = path.join(f.project, ".nccgs/orchestration.json");
442
+ const ledger = json(file);
443
+ delete ledger.dispatches[run.reservation.id];
444
+ atomicJson(file, ledger);
445
+ assert.throws(() => reserve(f.project, "task", f.request("analysis")), /ledger|history|corrupt|dispatch|event/i);
446
+ } finally { f.dispose(); }
447
+ });
448
+
449
+ test("durable observations cannot complete against an ignored raw source", () => {
450
+ const f = fixture();
451
+ try {
452
+ f.task("task");
453
+ const run = f.start("task");
454
+ const good = f.observation(run);
455
+ const observation = json(path.join(f.project, good.path));
456
+ const sourceBytes = fs.readFileSync(path.join(f.project, observation.observationSource.path));
457
+ observation.observationSource = f.write(".nccgs/runtime/ignored-source.json", sourceBytes);
458
+ const runtime = f.write(".nccgs/evidence/runtime/observations/ignored-source-ref.json", observation);
459
+ assert.throws(() => complete(f.project, run.reservation.id, { runtime }), /durable|source|runtime/i);
460
+ assert.equal(status(f.project, "task").active.length, 1);
461
+ } finally { f.dispose(); }
462
+ });
463
+
464
+ test("valid task IDs that match object prototype properties can reserve normally", () => {
465
+ const f = fixture();
466
+ try {
467
+ for (const id of ["constructor", "toString"]) {
468
+ f.task(id);
469
+ assert.ok(reserve(f.project, id, f.request("analysis")).id);
470
+ assert.equal(status(f.project, id).reservations.length, 1);
471
+ }
472
+ } finally { f.dispose(); }
473
+ });
474
+
475
+ test("telemetry fails closed when the durable evidence behind measured totals is lost or corrupt", () => {
476
+ const f = fixture();
477
+ try {
478
+ f.task("task");
479
+ const run = f.start("task");
480
+ const runtime = f.observation(run, { durationMs: 31 });
481
+ complete(f.project, run.reservation.id, { runtime });
482
+ assert.equal(telemetry(f.project, "task").metrics.durationMs.total, 31);
483
+ const observation = json(path.join(f.project, runtime.path));
484
+ const sourcePath = path.join(f.project, observation.observationSource.path);
485
+ const originalBytes = fs.readFileSync(sourcePath);
486
+ fs.unlinkSync(sourcePath);
487
+ assert.throws(() => telemetry(f.project, "task"), /ENOENT|artifact|evidence|source|missing/i);
488
+ fs.writeFileSync(sourcePath, Buffer.concat([originalBytes, Buffer.from("\ncorrupt")]));
489
+ assert.throws(() => telemetry(f.project, "task"), /artifact|evidence|source|hash|stale/i);
490
+ } finally { f.dispose(); }
491
+ });
492
+
493
+ function nativeFixtureEvent(run) {
494
+ return { session_id: run.identity.sessionId, tool_use_id: run.identity.toolUseId,
495
+ tool_input: { subagent_type: run.request.agent, prompt: `[NCCGS-DISPATCH:${run.reservation.id}] SYNTHETIC TEST FIXTURE ONLY` },
496
+ tool_response: { status: "completed", agentId: `agent-${run.reservation.id}`, resolvedModel: "claude-opus-5-5",
497
+ modelsUsed: ["claude-opus-5-5"], totalDurationMs: 23, totalToolUseCount: 2, totalTokens: 10 } };
498
+ }
499
+
500
+ test("native standalone observation produces provenance accepted by runtime validation", () => {
501
+ const f = fixture();
502
+ try {
503
+ const task = f.task("task", { execution: { requireObservedEffort: false } });
504
+ const run = f.start("task", f.request("analysis", { agent: agents.implementation }));
505
+ const observation = observe(f.project, nativeFixtureEvent(run), "agent_result");
506
+ const stage = { runtime: artifact(f.project, observation.path) };
507
+ const verified = validateStandaloneRuntime(f.project, task, stage, task.implementationRoute, snapshot(f.project).sha256);
508
+ assert.equal(verified.observedProvider, "claude-code");
509
+ assert.equal(verified.observedEffort, null);
510
+ assert.equal(verified.effortEvidence, "UNKNOWN");
511
+ } finally { f.dispose(); }
512
+ });
513
+
514
+ test("duplicate native terminal callback is idempotent and keeps the original dispatch evidence", () => {
515
+ const f = fixture();
516
+ try {
517
+ f.task("task");
518
+ const run = f.start("task", f.request("analysis", { agent: agents.implementation }));
519
+ const event = nativeFixtureEvent(run);
520
+ observe(f.project, event, "agent_result");
521
+ const before = status(f.project, "task").reservations[0];
522
+ assert.doesNotThrow(() => observe(f.project, structuredClone(event), "agent_result"));
523
+ const after = status(f.project, "task").reservations[0];
524
+ assert.deepEqual(after.runtime, before.runtime);
525
+ assert.equal(telemetry(f.project, "task").attempts, 1);
526
+ assert.equal(telemetry(f.project, "task").metrics.durationMs.total, 23);
527
+ } finally { f.dispose(); }
528
+ });
529
+
530
+ test("coordinator verification allowance cannot hide trailing shell commands", () => {
531
+ const f = fixture();
532
+ const savedTask = process.env.NCCGS_TASK_ID;
533
+ try {
534
+ f.task("task");
535
+ process.env.NCCGS_TASK_ID = "task";
536
+ for (const suffix of ["; echo injected", " && echo injected", " | echo injected", "\necho injected"]) {
537
+ assert.throws(() => guardTool(f.project, { session_id: "coordinator", tool_name: "Bash", tool_input: {
538
+ command: `node .claude/nccgs/tools/task.mjs verify --id task --name regression -- node -e 0${suffix}`
539
+ } }), /shell|compos|verif|command|correlat|worker|blocked|unsafe/i);
540
+ }
541
+ } finally {
542
+ if (savedTask === undefined) delete process.env.NCCGS_TASK_ID; else process.env.NCCGS_TASK_ID = savedTask;
543
+ f.dispose();
544
+ }
545
+ });