nccgs 2.0.0-rc.2 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (71) hide show
  1. package/.claude/nccgs/MEMORY.md +3 -2
  2. package/.claude/nccgs/VERSION +1 -1
  3. package/.claude/nccgs/packages/ponytail/README.md +2 -2
  4. package/.claude/nccgs/protocols/agent-contract.md +15 -2
  5. package/.claude/nccgs/protocols/bounded-repair.md +135 -7
  6. package/.claude/nccgs/protocols/editor-session-evidence.md +52 -0
  7. package/.claude/nccgs/protocols/orchestration.md +7 -5
  8. package/.claude/nccgs/protocols/review-handoff.md +17 -1
  9. package/.claude/nccgs/protocols/standalone-gates.md +64 -8
  10. package/.claude/nccgs/protocols/standalone.md +33 -13
  11. package/.claude/nccgs/protocols/task-gates.md +13 -1
  12. package/.claude/nccgs/protocols/task-slices.md +106 -0
  13. package/.claude/nccgs/routing.generated.md +20 -0
  14. package/.claude/nccgs/tools/command-process.mjs +10 -4
  15. package/.claude/nccgs/tools/compile-policy.mjs +4 -0
  16. package/.claude/nccgs/tools/editor-session-evidence.mjs +66 -0
  17. package/.claude/nccgs/tools/gates.mjs +7 -4
  18. package/.claude/nccgs/tools/handoff.mjs +51 -0
  19. package/.claude/nccgs/tools/inputs.mjs +36 -1
  20. package/.claude/nccgs/tools/lifecycle.mjs +21 -0
  21. package/.claude/nccgs/tools/orchestration.mjs +264 -14
  22. package/.claude/nccgs/tools/repair-packet.mjs +29 -10
  23. package/.claude/nccgs/tools/returned-handoff.mjs +38 -0
  24. package/.claude/nccgs/tools/review-budget.mjs +171 -0
  25. package/.claude/nccgs/tools/review-handoff.mjs +42 -8
  26. package/.claude/nccgs/tools/runtime.mjs +7 -2
  27. package/.claude/nccgs/tools/session-budget.mjs +67 -0
  28. package/.claude/nccgs/tools/task-slices.mjs +147 -0
  29. package/.claude/nccgs/tools/task.mjs +39 -2
  30. package/.claude/nccgs/tools/tool-guard.mjs +19 -5
  31. package/.claude/nccgs/tools/unity-repair.mjs +128 -0
  32. package/.claude/nccgs/tools/verification-jobs.mjs +13 -3
  33. package/.claude/nccgs/tools/windows-compiler-server.cs +163 -0
  34. package/.claude/nccgs/tools/windows-job.cs +8 -1
  35. package/CHANGELOG.md +123 -0
  36. package/MEMORY.md +3 -2
  37. package/README.md +35 -27
  38. package/UPGRADING.md +26 -22
  39. package/VERSION +1 -1
  40. package/docs/ARCHITECTURE.md +45 -18
  41. package/docs/BOUNDED-REPAIR.md +76 -7
  42. package/docs/EDITOR-SESSION-EVIDENCE.md +52 -0
  43. package/docs/HANDOFF-AUTOMATION.md +144 -0
  44. package/docs/HUONG-DAN-MIGRATE-VA-SU-DUNG.md +40 -11
  45. package/docs/PHASE-1-OPERATIONS.md +142 -1
  46. package/docs/PHASE-2-OPERATIONS.md +1 -1
  47. package/docs/RELEASE-STATUS.md +92 -9
  48. package/docs/TASK-SLICING.md +57 -0
  49. package/docs/UPGRADE-QUALIFICATION.md +45 -0
  50. package/docs/WORKFLOWS.md +1 -1
  51. package/package.json +2 -2
  52. package/scaffold/.nccgs/templates/repair-unity-code.json +18 -0
  53. package/scaffold/.nccgs/templates/repair-unity-editor.json +21 -0
  54. package/scaffold/.nccgs/templates/task-slices.json +28 -0
  55. package/scripts/cli.mjs +4 -0
  56. package/scripts/install.mjs +10 -0
  57. package/scripts/run.mjs +4 -3
  58. package/scripts/supervise.mjs +30 -6
  59. package/scripts/validate.mjs +4 -0
  60. package/scripts/work-handoff.mjs +38 -0
  61. package/tests/editor-session-evidence.test.mjs +45 -0
  62. package/tests/fixtures/compiler-shutdown.cs +64 -0
  63. package/tests/framework.test.mjs +38 -0
  64. package/tests/orchestration-v15.test.mjs +52 -1
  65. package/tests/phase1.test.mjs +92 -0
  66. package/tests/policy-v15.test.mjs +14 -0
  67. package/tests/review-budget.test.mjs +177 -0
  68. package/tests/runtime-v15.test.mjs +572 -4
  69. package/tests/session-budget.test.mjs +34 -0
  70. package/tests/task-slices.test.mjs +285 -0
  71. package/tests/unity-generated.test.mjs +140 -0
@@ -0,0 +1,177 @@
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 crypto from "node:crypto";
6
+ import test from "node:test";
7
+ import { artifact, atomicJson } from "../.claude/nccgs/tools/policy.mjs";
8
+ import { acquireLock } from "../.claude/nccgs/tools/process-lock.mjs";
9
+ import { sessionBudgetLock } from "../.claude/nccgs/tools/session-budget.mjs";
10
+ import { allocateReviewBudget, findReviewBudget, plannedDispatchBudget, registerReviewBudget, reviewBudgetStatus } from "../.claude/nccgs/tools/review-budget.mjs";
11
+
12
+ // Synthetic accounting fixtures, never provider/game qualification evidence.
13
+ function fixture() {
14
+ const temp = fs.realpathSync(os.tmpdir()), project = fs.mkdtempSync(path.join(temp, "nccgs-review-reserve-"));
15
+ const write = (file, value) => atomicJson(path.join(project, file), value);
16
+ write('.nccgs/tasks/root.json', { id: 'root', schemaVersion: 2, mode: 'plan', parentId: null });
17
+ for (const id of ['delivery', 'sibling']) write(`.nccgs/tasks/${id}.json`, { id, schemaVersion: 2, mode: 'implement', parentId: 'root' });
18
+ const request = '.nccgs/evidence/reserve.json';
19
+ write(request, { schemaVersion: 1, authority: 'Synthetic external test allocation', totalUsd: 12, finalReviewUsd: 4, totalDurationMs: 4500000, finalReviewDurationMs: 900000 });
20
+ const plan = () => registerReviewBudget(project, 'root', request);
21
+ const save = run => write(`.nccgs/runs/${run.id}/run.json`, run);
22
+ const run = (taskId = 'root', requested = 12) => {
23
+ const state = { schemaVersion: 1, id: crypto.randomUUID(), project, taskId, sessionId: crypto.randomUUID(), budgetUsd: requested,
24
+ deadline: Date.now() + 4500000, status: 'ready', costCoverage: 'complete', cumulativeCostUsd: 0, invocations: [] }; save(state); return state;
25
+ };
26
+ const locked = (state, action) => { const lock = acquireLock(path.join(project, sessionBudgetLock), { runId: state.id, sessionId: state.sessionId }); try { return action(); } finally { lock.release(); } };
27
+ const allocate = (state, phase = 'work') => locked(state, () => { const result = allocateReviewBudget(project, state, phase); save(state); return result; });
28
+ const finish = (state, cost) => {
29
+ const log = `native-${state.invocations.length}.jsonl`, file = `.nccgs/runs/${state.id}/${log}`;
30
+ fs.writeFileSync(path.join(project, file), JSON.stringify({ type: 'result', session_id: state.sessionId, total_cost_usd: cost }) + '\n');
31
+ state.invocations.push({ status: 'finished', log, evidence: artifact(project, file) });
32
+ Object.assign(state, { status: 'settled', costCoverage: 'complete', cumulativeCostUsd: cost }); save(state);
33
+ };
34
+ return { project, write, request, plan, run, save, locked, allocate, finish, dispose() {
35
+ assert.equal(path.dirname(project), temp); assert.ok(path.basename(project).startsWith('nccgs-review-reserve-'));
36
+ assert.equal(fs.realpathSync(project), project); fs.rmSync(project, { recursive: true, force: true });
37
+ } };
38
+ }
39
+
40
+ test('adopting a legacy task cannot ignore a started worker session when maxCostUsd was omitted', () => {
41
+ const f = fixture(); try {
42
+ f.write('.nccgs/orchestration.json', { tasks: { root: { limits: { maxCostUsd: null } } }, dispatches: {
43
+ old: { taskId: 'delivery', sessionId: 'uncovered-old-session', startedAt: new Date().toISOString(), status: 'completed', telemetry: { costUsd: null } }
44
+ } });
45
+ assert.throws(() => f.plan(), /worker session.*supervised cost coverage/);
46
+ assert.equal(fs.existsSync(path.join(f.project, '.nccgs/budgets/root.initialized.json')), false);
47
+ } finally { f.dispose(); }
48
+ });
49
+
50
+ test('a reserve refuses unexplained historical workers before allocating another session', () => {
51
+ const f = fixture(); try {
52
+ f.plan(); const old = f.run(); f.allocate(old); f.finish(old, 2);
53
+ f.write('.nccgs/orchestration.json', { tasks: { root: { limits: { maxCostUsd: null } } }, dispatches: {
54
+ known: { taskId: 'root', sessionId: old.sessionId, startedAt: new Date().toISOString(), status: 'completed' },
55
+ unknown: { taskId: 'delivery', sessionId: 'missing-old-session', startedAt: new Date().toISOString(), status: 'completed' }
56
+ } });
57
+ const next = f.run('delivery');
58
+ assert.throws(() => f.allocate(next), /worker session.*supervised cost coverage/);
59
+ assert.equal(next.budgetAllocation, undefined);
60
+ } finally { f.dispose(); }
61
+ });
62
+ test('budget terms are pinned externally and children cannot create new money', () => {
63
+ const f = fixture(); try {
64
+ const first = f.plan(); assert.deepEqual(f.plan(), first);
65
+ assert.equal(findReviewBudget(f.project, 'delivery').taskId, 'root');
66
+ assert.throws(() => registerReviewBudget(f.project, 'delivery', f.request), /already pinned/);
67
+ f.write('.nccgs/evidence/replacement.json', { schemaVersion: 1, authority: 'changed', totalUsd: 20, finalReviewUsd: 4 });
68
+ assert.throws(() => registerReviewBudget(f.project, 'root', '.nccgs/evidence/replacement.json'), /already pinned/);
69
+ f.write('.nccgs/budgets/root.json', { ...first, limits: { ...first.limits, totalUsd: 20 } });
70
+ assert.throws(() => findReviewBudget(f.project, 'root'), /pinned budget terms/);
71
+ } finally { f.dispose(); }
72
+ });
73
+ test('successive design/delivery sessions share eight dollars while four and fifteen minutes stay protected', () => {
74
+ const f = fixture(); try {
75
+ const plan = f.plan(), design = f.run(); f.allocate(design);
76
+ assert.equal(design.budgetUsd, 8); assert.equal(design.deadline, plan.deadline - 900000);
77
+ f.finish(design, 4.5);
78
+ const delivery = f.run('delivery'); f.allocate(delivery); assert.equal(delivery.budgetUsd, 3.5); f.finish(delivery, 3.5);
79
+ const review = f.run('delivery'); f.allocate(review, 'final-review'); assert.equal(review.budgetUsd, 4); assert.equal(review.deadline, plan.deadline); f.finish(review, 2.25);
80
+ const status = reviewBudgetStatus(f.project, 'root'); assert.equal(status.settledCostUsd, 10.25); assert.equal(status.protectedReviewUsd, 1.75);
81
+ const repair = f.run('sibling'); assert.throws(() => f.allocate(repair), /exhausted/);
82
+ } finally { f.dispose(); }
83
+ });
84
+ test('review may use unused work funds, but spent review money cannot be refunded to a later repair', () => {
85
+ const f = fixture(); try {
86
+ f.plan(); const work = f.run('delivery'); f.allocate(work); f.finish(work, 6);
87
+ const review = f.run('delivery'); f.allocate(review, 'final-review'); assert.equal(review.budgetUsd, 6); f.finish(review, 5);
88
+ const repair = f.run('delivery'); f.allocate(repair); assert.equal(repair.budgetUsd, 1);
89
+ } finally { f.dispose(); }
90
+ });
91
+ test('missing native usage or an allocated run blocks further spending, even when workers supplied no cost', () => {
92
+ const f = fixture(); try {
93
+ f.plan(); const old = f.run(); f.allocate(old); f.finish(old, 2);
94
+ const next = f.run('delivery'); old.costCoverage = 'unknown-current-invocation'; f.save(old);
95
+ assert.throws(() => f.allocate(next), /unknown/); old.costCoverage = 'complete'; f.save(old);
96
+ fs.renameSync(path.join(f.project, `.nccgs/runs/${old.id}/run.json`), path.join(f.project, `.nccgs/runs/${old.id}/missing.json`));
97
+ assert.throws(() => f.allocate(next), /ENOENT|missing/);
98
+ } finally { f.dispose(); }
99
+ });
100
+ test('recovery retains the original allocation and cumulative resumed totals are never added twice', () => {
101
+ const f = fixture(); try {
102
+ f.plan(); const first = f.run(); f.allocate(first); f.finish(first, 1); f.finish(first, 2);
103
+ f.allocate(first); assert.equal(first.budgetUsd, 8);
104
+ first.budgetUsd = 12; assert.throws(() => f.allocate(first), /recovery cannot alter/); first.budgetUsd = 8;
105
+ const next = f.run('delivery'); f.allocate(next); assert.equal(next.budgetUsd, 6);
106
+ assert.throws(() => f.allocate(next, 'final-review'), /recovery cannot alter/);
107
+ } finally { f.dispose(); }
108
+ });
109
+ test('live work/review dispatch admissions enforce phase and keep final delivery review independent', () => {
110
+ const f = fixture(), names = ['NCCGS_SUPERVISED_RUN', 'NCCGS_SUPERVISED_SESSION'], saved = names.map(n => process.env[n]);
111
+ try {
112
+ f.plan(); const work = f.run('delivery'); f.allocate(work);
113
+ Object.assign(work, { status: 'running', invocations: [{ status: 'running', pid: process.pid }] }); f.save(work);
114
+ process.env.NCCGS_SUPERVISED_RUN = work.id; process.env.NCCGS_SUPERVISED_SESSION = work.sessionId;
115
+ f.locked(work, () => {
116
+ assert.equal(plannedDispatchBudget(f.project, 'delivery', { kind: 'implementation' }).phase, 'work');
117
+ assert.throws(() => plannedDispatchBudget(f.project, 'delivery', { kind: 'review' }), /protected final-review/);
118
+ });
119
+ work.invocations = []; f.finish(work, 2);
120
+ const review = f.run('delivery'); f.allocate(review, 'final-review');
121
+ Object.assign(review, { status: 'running', invocations: [{ status: 'running', pid: process.pid }] }); f.save(review);
122
+ process.env.NCCGS_SUPERVISED_RUN = review.id; process.env.NCCGS_SUPERVISED_SESSION = review.sessionId;
123
+ f.locked(review, () => {
124
+ assert.equal(plannedDispatchBudget(f.project, 'delivery', { kind: 'review' }).phase, 'final-review');
125
+ for (const kind of ['analysis', 'implementation']) assert.throws(() => plannedDispatchBudget(f.project, 'delivery', { kind }), /cannot dispatch/);
126
+ });
127
+ } finally { names.forEach((n,i) => { if(saved[i]===undefined) delete process.env[n]; else process.env[n]=saved[i]; }); f.dispose(); }
128
+ });
129
+ test('audit protects independent final review and never spends its reserve on another analysis', () => {
130
+ const f = fixture(), names = ['NCCGS_SUPERVISED_RUN', 'NCCGS_SUPERVISED_SESSION'], saved = names.map(n => process.env[n]);
131
+ try {
132
+ f.write('.nccgs/tasks/root.json', { id: 'root', schemaVersion: 2, mode: 'audit', parentId: null });
133
+ const plan = f.plan(), work = f.run(); f.allocate(work); assert.equal(work.budgetUsd, 8);
134
+ Object.assign(work, { status: 'running', invocations: [{ status: 'running', pid: process.pid }] }); f.save(work);
135
+ process.env.NCCGS_SUPERVISED_RUN = work.id; process.env.NCCGS_SUPERVISED_SESSION = work.sessionId;
136
+ f.locked(work, () => {
137
+ assert.equal(plannedDispatchBudget(f.project, 'root', { kind: 'analysis' }).phase, 'work');
138
+ assert.throws(() => plannedDispatchBudget(f.project, 'root', { kind: 'review' }), /protected final-review/);
139
+ });
140
+ work.invocations = []; f.finish(work, 3);
141
+ const review = f.run(); f.allocate(review, 'final-review'); assert.equal(review.budgetUsd, 9); assert.equal(review.deadline, plan.deadline);
142
+ Object.assign(review, { status: 'running', invocations: [{ status: 'running', pid: process.pid }] }); f.save(review);
143
+ process.env.NCCGS_SUPERVISED_RUN = review.id; process.env.NCCGS_SUPERVISED_SESSION = review.sessionId;
144
+ f.locked(review, () => {
145
+ assert.equal(plannedDispatchBudget(f.project, 'root', { kind: 'review' }).phase, 'final-review');
146
+ for (const kind of ['analysis', 'implementation']) assert.throws(() => plannedDispatchBudget(f.project, 'root', { kind }), /cannot dispatch/);
147
+ });
148
+ review.invocations = []; f.finish(review, 2);
149
+ delete process.env.NCCGS_SUPERVISED_RUN; delete process.env.NCCGS_SUPERVISED_SESSION;
150
+ assert.equal(reviewBudgetStatus(f.project, 'root').settledCostUsd, 5);
151
+ } finally { names.forEach((n,i) => { if(saved[i]===undefined) delete process.env[n]; else process.env[n]=saved[i]; }); f.dispose(); }
152
+ });
153
+
154
+ test('plan design review remains unable to consume the final delivery reserve', () => {
155
+ const f = fixture(); try { f.plan(); const design = f.run(); assert.throws(() => f.allocate(design, 'final-review'), /design review/); }
156
+ finally { f.dispose(); }
157
+ });
158
+
159
+ test('the work time window stops before the protected review window without extending the original deadline', () => {
160
+ const f = fixture(), realNow = Date.now; try {
161
+ const plan = f.plan(); Date.now = () => plan.workDeadline + 1;
162
+ const work = f.run('delivery'); assert.throws(() => f.allocate(work), /exhausted/);
163
+ Object.assign(work, { status: 'blocked', costCoverage: 'complete', cumulativeCostUsd: 0 }); f.save(work);
164
+ const review = f.run('delivery'); f.allocate(review, 'final-review'); assert.equal(review.deadline, plan.deadline);
165
+ Date.now = () => plan.deadline + 1; assert.throws(() => f.allocate(review, 'final-review'), /exhausted/);
166
+ } finally { Date.now = realNow; f.dispose(); }
167
+ });
168
+ test('spent money cannot be retroactively reserved and initialized budget history cannot be erased', () => {
169
+ const f = fixture(); try {
170
+ const old = f.run(); f.finish(old, 11.589259);
171
+ assert.throws(() => f.plan(), /retroactively/);
172
+ } finally { f.dispose(); }
173
+ const g = fixture(); try {
174
+ g.plan(); const file = path.join(g.project, '.nccgs/budgets/root.json'); fs.renameSync(file, file + '.archived');
175
+ assert.throws(() => g.plan(), /history is missing/);
176
+ } finally { g.dispose(); }
177
+ });