nccgs 2.1.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 +3 -1
- 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/hook-transport.mjs +34 -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 -200
- package/CLAUDE.md +10 -8
- package/MEMORY.md +109 -60
- package/README.md +206 -191
- package/UPGRADING.md +130 -118
- 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 -116
- package/docs/NCCGS-1.5.md +237 -237
- package/docs/PROJECT-POLICY.md +94 -94
- package/docs/RELEASE-STATUS.md +17 -9
- package/docs/UNINSTALL.md +3 -3
- 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/doctor.mjs +261 -261
- package/scripts/install.mjs +530 -529
- package/scripts/run.mjs +5 -4
- package/scripts/supervise.mjs +4 -0
- package/scripts/uninstall.mjs +292 -292
- package/scripts/validate.mjs +285 -280
- 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/unity-transport.test.mjs +348 -0
|
@@ -1,1192 +1,1198 @@
|
|
|
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, spawnSync } from "node:child_process";
|
|
6
|
-
import { fileURLToPath } from "node:url";
|
|
7
|
-
import test from "node:test";
|
|
8
|
-
import { initializeStandalone, recordStandalone } from "../.claude/nccgs/tools/lifecycle.mjs";
|
|
9
|
-
import { reserve, status, rejectUnclaimed, dispatchPrompt, runRepairEditor, reconcile } from "../.claude/nccgs/tools/orchestration.mjs";
|
|
10
|
-
import { repairEditorCommand, unityEditorInvocation } from "../.claude/nccgs/tools/unity-repair.mjs";
|
|
11
|
-
import { settleWorkHandoff } from "../scripts/work-handoff.mjs";
|
|
12
|
-
import { supervise } from "../scripts/supervise.mjs";
|
|
13
|
-
import { registerReviewBudget, reviewBudgetStatus } from "../.claude/nccgs/tools/review-budget.mjs";
|
|
14
|
-
import { startVerification, waitVerification } from "../.claude/nccgs/tools/verification-jobs.mjs";
|
|
15
|
-
import { snapshot } from "../.claude/nccgs/tools/gates.mjs";
|
|
16
|
-
import { observe } from "../.claude/nccgs/tools/runtime.mjs";
|
|
17
|
-
import { captureReturnedHandoff } from "../.claude/nccgs/tools/returned-handoff.mjs";
|
|
18
|
-
import { guardTool } from "../.claude/nccgs/tools/tool-guard.mjs";
|
|
19
|
-
import { artifact, json } from "../.claude/nccgs/tools/policy.mjs";
|
|
20
|
-
import { run as taskRun } from "../.claude/nccgs/tools/task.mjs";
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
//
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
const
|
|
28
|
-
const
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
write(".nccgs/
|
|
36
|
-
write(".nccgs/
|
|
37
|
-
write("
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
const
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
const
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
assert.
|
|
74
|
-
assert.
|
|
75
|
-
assert.equal(fs.
|
|
76
|
-
fs.
|
|
77
|
-
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
f.
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
const
|
|
90
|
-
const
|
|
91
|
-
|
|
92
|
-
assert.equal(
|
|
93
|
-
assert.
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
assert.ok(
|
|
97
|
-
|
|
98
|
-
const
|
|
99
|
-
f.
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
const
|
|
104
|
-
|
|
105
|
-
assert.equal(
|
|
106
|
-
assert.
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
});
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
const
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
if (scenario === "
|
|
120
|
-
if (scenario === "
|
|
121
|
-
if (scenario === "
|
|
122
|
-
if (scenario === "
|
|
123
|
-
if (scenario === "
|
|
124
|
-
if (scenario === "
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
assert.ok(!
|
|
130
|
-
|
|
131
|
-
});
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
assert.throws(() => f.dispatch({ handoffTransport: "
|
|
138
|
-
assert.
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
assert.
|
|
144
|
-
|
|
145
|
-
});
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
const
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
});
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
assert.
|
|
170
|
-
|
|
171
|
-
});
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
const
|
|
178
|
-
|
|
179
|
-
assert.equal(
|
|
180
|
-
assert.equal(observation.
|
|
181
|
-
assert.
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
assert.
|
|
185
|
-
assert.
|
|
186
|
-
assert.equal(raw.
|
|
187
|
-
assert.equal(raw.
|
|
188
|
-
assert.equal(raw.
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
});
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
assert.
|
|
199
|
-
assert.equal(f.state().
|
|
200
|
-
|
|
201
|
-
});
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
const
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
const
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
assert.
|
|
218
|
-
|
|
219
|
-
});
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
f.
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
assert.equal(state.
|
|
230
|
-
assert.equal(state.reservations[0].
|
|
231
|
-
assert.
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
assert.
|
|
235
|
-
|
|
236
|
-
});
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
const
|
|
243
|
-
f.
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
assert.equal(f.state().reservations[0].
|
|
247
|
-
assert.
|
|
248
|
-
|
|
249
|
-
f.
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
assert.equal(f.state().
|
|
254
|
-
assert.equal(f.state().reservations[
|
|
255
|
-
assert.equal(f.state().
|
|
256
|
-
|
|
257
|
-
});
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
const
|
|
264
|
-
|
|
265
|
-
assert.equal(
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
assert.
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
assert.equal(
|
|
275
|
-
|
|
276
|
-
});
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
const
|
|
283
|
-
|
|
284
|
-
assert.equal(
|
|
285
|
-
assert.equal(f.state().
|
|
286
|
-
assert.equal(f.state().
|
|
287
|
-
|
|
288
|
-
});
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
assert.
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
assert.
|
|
302
|
-
assert.
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
});
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
const
|
|
313
|
-
|
|
314
|
-
assert.
|
|
315
|
-
assert.throws(() => guardTool(f.project,
|
|
316
|
-
assert.throws(() => guardTool(f.project, { ...write("Assets/fixture.cs"), agent_id:
|
|
317
|
-
assert.throws(() => guardTool(f.project, { ...
|
|
318
|
-
assert.throws(() => guardTool(f.project,
|
|
319
|
-
assert.throws(() => guardTool(f.project, write(".nccgs/
|
|
320
|
-
|
|
321
|
-
});
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
const
|
|
328
|
-
const
|
|
329
|
-
|
|
330
|
-
assert.
|
|
331
|
-
assert.throws(() => guardTool(f.project, edit("
|
|
332
|
-
assert.
|
|
333
|
-
|
|
334
|
-
assert.throws(() => guardTool(f.project, { ...input, tool_name:
|
|
335
|
-
assert.throws(() => guardTool(f.project, { ...input, tool_name: "
|
|
336
|
-
assert.throws(() => guardTool(f.project, {
|
|
337
|
-
|
|
338
|
-
});
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
const
|
|
345
|
-
|
|
346
|
-
assert.
|
|
347
|
-
assert.
|
|
348
|
-
|
|
349
|
-
});
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
const
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
assert.
|
|
360
|
-
assert.
|
|
361
|
-
|
|
362
|
-
});
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
f.
|
|
370
|
-
|
|
371
|
-
const
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
assert.equal(report.stages.report.
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
});
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
const
|
|
389
|
-
f.
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
});
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
const
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
const
|
|
400
|
-
const
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
}
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
}
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
f.write("
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
f.write(
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
const
|
|
424
|
-
const
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
}
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
}
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
const
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
assert.
|
|
448
|
-
assert.
|
|
449
|
-
assert.
|
|
450
|
-
assert.
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
assert.equal(f.state().
|
|
455
|
-
assert.equal(f.
|
|
456
|
-
|
|
457
|
-
});
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
const
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
assert.equal(
|
|
472
|
-
assert.
|
|
473
|
-
|
|
474
|
-
});
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
if (variant === "
|
|
488
|
-
|
|
489
|
-
task
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
if (variant === "
|
|
494
|
-
if (variant === "
|
|
495
|
-
if (variant === "
|
|
496
|
-
if (variant === "
|
|
497
|
-
|
|
498
|
-
const
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
assert.equal(f.task().
|
|
502
|
-
assert.equal(f.task().stages.
|
|
503
|
-
assert.
|
|
504
|
-
assert.deepEqual(f.task().
|
|
505
|
-
assert.
|
|
506
|
-
assert.
|
|
507
|
-
|
|
508
|
-
assert.
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
}
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
f.
|
|
520
|
-
import {
|
|
521
|
-
import {
|
|
522
|
-
import {
|
|
523
|
-
import {
|
|
524
|
-
|
|
525
|
-
const
|
|
526
|
-
const
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
const
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
const
|
|
538
|
-
|
|
539
|
-
assert.equal(run.
|
|
540
|
-
assert.equal(
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
assert.
|
|
544
|
-
|
|
545
|
-
});
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
const
|
|
555
|
-
const
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
});
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
const
|
|
569
|
-
const
|
|
570
|
-
|
|
571
|
-
assert.equal(run.reservation.workPacket.budget.
|
|
572
|
-
assert.
|
|
573
|
-
assert.
|
|
574
|
-
|
|
575
|
-
f.
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
assert.
|
|
581
|
-
assert.
|
|
582
|
-
assert.
|
|
583
|
-
assert.
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
assert.equal(
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
assert.
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
assert.
|
|
597
|
-
assert.
|
|
598
|
-
assert.
|
|
599
|
-
assert.
|
|
600
|
-
|
|
601
|
-
const
|
|
602
|
-
const
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
assert.
|
|
608
|
-
|
|
609
|
-
});
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
f.
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
repairCall(f, run, "
|
|
624
|
-
repairCall(f, run, "
|
|
625
|
-
repairCall(f, run, "
|
|
626
|
-
|
|
627
|
-
{ ...editorCall(f, run, packet.unity.steps[0], "
|
|
628
|
-
{ ...editorCall(f, run, packet.unity.steps[0], "
|
|
629
|
-
|
|
630
|
-
assert.throws(() =>
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
const
|
|
635
|
-
|
|
636
|
-
assert.
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
f.
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
f.
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
});
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
const
|
|
654
|
-
|
|
655
|
-
{ ...packet,
|
|
656
|
-
{ ...packet, changes: [{ ...packet.changes[0], operation: "
|
|
657
|
-
{ ...packet, changes: [{ ...packet.changes[0],
|
|
658
|
-
{ ...packet, changes: [{ ...packet.changes[0], path: "
|
|
659
|
-
{ ...packet, changes: [{ ...packet.changes[0], path: "
|
|
660
|
-
{ ...packet,
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
const
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
});
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
if (variant === "
|
|
689
|
-
if (variant === "
|
|
690
|
-
if (variant === "
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
assert.
|
|
699
|
-
assert.
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
}
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
const
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
assert.
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
f.
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
const
|
|
722
|
-
|
|
723
|
-
assert.equal(retained.
|
|
724
|
-
assert.equal(retained.
|
|
725
|
-
assert.equal(
|
|
726
|
-
assert.
|
|
727
|
-
assert.throws(() => f.
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
assert.equal(
|
|
732
|
-
assert.
|
|
733
|
-
assert.
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
}
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
});
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
const
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
env
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
assert.
|
|
761
|
-
assert.
|
|
762
|
-
assert.equal(f.state().
|
|
763
|
-
|
|
764
|
-
});
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
const
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
f.
|
|
777
|
-
f.write(
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
assert.equal(f.state().reservations.at(-1).
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
assert.
|
|
784
|
-
|
|
785
|
-
});
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
{ ...packet,
|
|
795
|
-
{ ...packet,
|
|
796
|
-
{ ...packet, changes: [{ path: "
|
|
797
|
-
{ ...packet, changes: [
|
|
798
|
-
{ ...packet,
|
|
799
|
-
{ ...packet,
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
assert.
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
assert.throws(() => f.dispatch({ ...request,
|
|
809
|
-
assert.
|
|
810
|
-
assert.
|
|
811
|
-
|
|
812
|
-
});
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
const
|
|
819
|
-
|
|
820
|
-
assert.
|
|
821
|
-
assert.
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
assert.equal(output.
|
|
827
|
-
assert.equal(output.
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
});
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
const
|
|
838
|
-
|
|
839
|
-
guardTool(f.project, read);
|
|
840
|
-
|
|
841
|
-
assert.
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
assert.throws(() => guardTool(f.project, sweep), /only project reads/);
|
|
845
|
-
assert.
|
|
846
|
-
assert.
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
f.
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
assert.
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
assert.equal(f.state().
|
|
856
|
-
assert.
|
|
857
|
-
|
|
858
|
-
});
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
const
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
["
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
assert.throws(() => guardTool(f.project,
|
|
880
|
-
|
|
881
|
-
assert.
|
|
882
|
-
|
|
883
|
-
});
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
});
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
});
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
}
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
f.
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
const
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
assert.equal(
|
|
1013
|
-
assert.
|
|
1014
|
-
assert.
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
]
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
f.start(
|
|
1043
|
-
|
|
1044
|
-
const
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
}
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
const
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
assert.
|
|
1064
|
-
|
|
1065
|
-
});
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
assert.
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
assert.equal(
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
assert.
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
}
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
assert.
|
|
1120
|
-
|
|
1121
|
-
assert.
|
|
1122
|
-
assert.
|
|
1123
|
-
assert.
|
|
1124
|
-
assert.
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
const
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
}
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
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, spawnSync } from "node:child_process";
|
|
6
|
+
import { fileURLToPath } from "node:url";
|
|
7
|
+
import test from "node:test";
|
|
8
|
+
import { initializeStandalone, recordStandalone } from "../.claude/nccgs/tools/lifecycle.mjs";
|
|
9
|
+
import { reserve, status, rejectUnclaimed, dispatchPrompt, runRepairEditor, reconcile } from "../.claude/nccgs/tools/orchestration.mjs";
|
|
10
|
+
import { repairEditorCommand, unityEditorInvocation } from "../.claude/nccgs/tools/unity-repair.mjs";
|
|
11
|
+
import { settleWorkHandoff } from "../scripts/work-handoff.mjs";
|
|
12
|
+
import { supervise } from "../scripts/supervise.mjs";
|
|
13
|
+
import { registerReviewBudget, reviewBudgetStatus } from "../.claude/nccgs/tools/review-budget.mjs";
|
|
14
|
+
import { startVerification, waitVerification } from "../.claude/nccgs/tools/verification-jobs.mjs";
|
|
15
|
+
import { snapshot } from "../.claude/nccgs/tools/gates.mjs";
|
|
16
|
+
import { observe } from "../.claude/nccgs/tools/runtime.mjs";
|
|
17
|
+
import { captureReturnedHandoff } from "../.claude/nccgs/tools/returned-handoff.mjs";
|
|
18
|
+
import { guardTool } from "../.claude/nccgs/tools/tool-guard.mjs";
|
|
19
|
+
import { artifact, json } from "../.claude/nccgs/tools/policy.mjs";
|
|
20
|
+
import { run as taskRun } from "../.claude/nccgs/tools/task.mjs";
|
|
21
|
+
import { nativeProvider } from "./fixtures/native-provider.mjs";
|
|
22
|
+
|
|
23
|
+
// All envelopes, source files, model identifiers and measurements below are
|
|
24
|
+
// SYNTHETIC TEST FIXTURES. They do not establish actual Claude Code execution.
|
|
25
|
+
const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
|
26
|
+
function fixture(mode = "implement", reviewContractRequired = false, intakeOverrides = {}) {
|
|
27
|
+
const temporaryRoot = fs.realpathSync(os.tmpdir());
|
|
28
|
+
const project = fs.mkdtempSync(path.join(temporaryRoot, "nccgs-runtime-v15-"));
|
|
29
|
+
const write = (relative, value) => {
|
|
30
|
+
const file = path.join(project, relative); fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
31
|
+
fs.writeFileSync(file, typeof value === "string" || Buffer.isBuffer(value) ? value : JSON.stringify(value));
|
|
32
|
+
return artifact(project, relative);
|
|
33
|
+
};
|
|
34
|
+
for (const file of ["pipeline.json", "studio.json"]) write(`.claude/nccgs/${file}`, fs.readFileSync(path.join(root, ".claude/nccgs", file)));
|
|
35
|
+
write(".nccgs/project.yaml", "schema_version: 2\nmodels:\n profile: claude-standalone\n");
|
|
36
|
+
write(".nccgs/execution.json", { schemaVersion: 1, name: "standalone", externalReview: { mode: "off" }, acceptanceRequired: false, requireObservedEffort: false, maxParallelWorkers: 2, ...(reviewContractRequired ? { reviewContractRequired } : {}) });
|
|
37
|
+
write(".nccgs/requirements.yaml", "schema_version: 1\nrequirements: []\n");
|
|
38
|
+
write("Assets/fixture.cs", "// SYNTHETIC TEST SOURCE; no game implementation.\n");
|
|
39
|
+
const analysis = ".nccgs/evidence/analysis/synthetic-result.md";
|
|
40
|
+
write(".nccgs/intake.json", { schemaVersion: 1, mode, objective: "SYNTHETIC TEST: inspect bounded fixture behavior", scope: "feature", target: "production-change", readiness: "ready",
|
|
41
|
+
requiredChecks: ["synthetic-check"], analysisArtifacts: [analysis, ".nccgs/evidence/analysis/synthetic-review.md"], acceptance: [], ...intakeOverrides });
|
|
42
|
+
initializeStandalone(project, "synthetic", { intake: ".nccgs/intake.json" });
|
|
43
|
+
write(".nccgs/runtime/bindings/synthetic-session.json", { taskId: "synthetic" });
|
|
44
|
+
const verification = write(".nccgs/evidence/synthetic-check.txt", "SYNTHETIC TEST: assertion fixture, not an actual game or provider check.\n");
|
|
45
|
+
let sequence = 0;
|
|
46
|
+
const dispatch = (overrides = {}) => {
|
|
47
|
+
const kind = mode === "implement" ? "implementation" : "analysis";
|
|
48
|
+
const request = { kind, agent: kind === "implementation" ? "nccgs-gameplay-programmer" : "nccgs-task-scout", requestKey: `synthetic-${++sequence}`,
|
|
49
|
+
reason: "SYNTHETIC TEST dispatch", writePaths: kind === "implementation" ? ["Assets/fixture.cs"] : [], ...overrides };
|
|
50
|
+
const reservation = reserve(project, "synthetic", request);
|
|
51
|
+
const envelope = { hook_event_name: "PreToolUse", session_id: "synthetic-session", tool_name: "Agent", tool_use_id: `synthetic-tool-${sequence}`,
|
|
52
|
+
tool_input: { subagent_type: request.agent, prompt: `[NCCGS-DISPATCH:${reservation.id}] SYNTHETIC TEST prompt SECRET_PROMPT` } };
|
|
53
|
+
return { reservation, request, envelope, agentId: `synthetic-agent-${sequence}` };
|
|
54
|
+
};
|
|
55
|
+
const start = run => {
|
|
56
|
+
guardTool(project, run.envelope);
|
|
57
|
+
return observe(project, { hook_event_name: "SubagentStart", session_id: "synthetic-session", agent_id: run.agentId, agent_type: run.request.agent }, "agent_start");
|
|
58
|
+
};
|
|
59
|
+
const result = (run, overrides = {}) => ({ ...run.envelope, hook_event_name: "PostToolUse", tool_response: {
|
|
60
|
+
agentId: run.agentId, status: "completed", resolvedModel: run.request.kind === "review" ? "claude-fable-5-1" : run.request.agent === "nccgs-task-scout" ? "claude-sonnet-4-6" : "claude-opus-5-5",
|
|
61
|
+
modelsUsed: [run.request.kind === "review" ? "claude-fable-5-1" : run.request.agent === "nccgs-task-scout" ? "claude-sonnet-4-6" : "claude-opus-5-5"], totalDurationMs: 123, totalToolUseCount: 4, totalTokens: 456,
|
|
62
|
+
content: "SECRET_RESULT", apiKey: "SECRET_KEY", effort: "high", observedEffort: "high", costUsd: 987, ...overrides } });
|
|
63
|
+
const stage = (run, observation) => ({ snapshot: observation.snapshot, dispatchId: run.reservation.id, runtime: artifact(project, observation.path),
|
|
64
|
+
checks: [{ name: "synthetic-check", status: "PASS", evidence: verification }] });
|
|
65
|
+
const hook = (file, arg, input) => {
|
|
66
|
+
const env = { ...process.env }; delete env.NCCGS_TASK_ID;
|
|
67
|
+
return spawnSync(process.execPath, [path.join(root, ".claude/nccgs/hooks", file), arg].filter(Boolean), { cwd: project, input: JSON.stringify(input), encoding: "utf8", env });
|
|
68
|
+
};
|
|
69
|
+
return { project, write, analysis, dispatch, start, result, stage, hook,
|
|
70
|
+
task: () => json(path.join(project, ".nccgs/tasks/synthetic.json")),
|
|
71
|
+
state: () => status(project, "synthetic"),
|
|
72
|
+
dispose() {
|
|
73
|
+
assert.equal(path.dirname(project).toLowerCase(), temporaryRoot.toLowerCase());
|
|
74
|
+
assert.ok(path.basename(project).startsWith("nccgs-runtime-v15-"));
|
|
75
|
+
assert.equal(fs.lstatSync(project).isSymbolicLink(), false);
|
|
76
|
+
assert.equal(fs.realpathSync(project).toLowerCase(), project.toLowerCase());
|
|
77
|
+
fs.rmSync(project, { recursive: true, force: true });
|
|
78
|
+
} };
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
test("opt-in returned analysis is captured verbatim and pins native identity without closing audit", () => {
|
|
82
|
+
const f = fixture("audit", true);
|
|
83
|
+
try {
|
|
84
|
+
const run = f.dispatch({ agent: "nccgs-engine-programmer", handoffPaths: [f.analysis], handoffTransport: "native-result" });
|
|
85
|
+
assert.match(dispatchPrompt(f.project, "synthetic", run.reservation.id).prompt, /sole final text response/);
|
|
86
|
+
f.start(run);
|
|
87
|
+
assert.throws(() => guardTool(f.project, { session_id: "synthetic-session", agent_id: run.agentId, tool_name: "Write",
|
|
88
|
+
tool_input: { file_path: path.join(f.project, f.analysis), content: "forged" } }), /captured only/);
|
|
89
|
+
const text = "NCCGS-HANDOFF: COMPLETE\n\nSYNTHETIC factual analysis. No source edits; not independent review.\n";
|
|
90
|
+
const envelope = f.result(run, { content: [{ type: "text", text }] });
|
|
91
|
+
const observation = observe(f.project, envelope, "agent_result");
|
|
92
|
+
assert.equal(fs.readFileSync(path.join(f.project, f.analysis), "utf8"), text);
|
|
93
|
+
assert.equal(f.state().reservations[0].status, "completed");
|
|
94
|
+
assert.deepEqual(observation.returnedHandoff.evidence, artifact(f.project, f.analysis));
|
|
95
|
+
recordStandalone(f.project, f.task(), "analysis", { ...f.stage(run, observation), evidence: artifact(f.project, f.analysis) });
|
|
96
|
+
assert.ok(!f.task().closed);
|
|
97
|
+
assert.ok(observe(f.project, envelope, "agent_result").duplicate);
|
|
98
|
+
const reviewPath = ".nccgs/evidence/analysis/synthetic-review.md";
|
|
99
|
+
const review = f.dispatch({ kind: "review", agent: "nccgs-adversarial-reviewer", writePaths: [], handoffPaths: [reviewPath], handoffTransport: "native-result" });
|
|
100
|
+
f.start(review);
|
|
101
|
+
const verdict = { schemaVersion: 1, taskId: "synthetic", contractHash: f.task().contractHash, snapshot: review.reservation.snapshot,
|
|
102
|
+
verdict: "BLOCKED", criteriaMet: false, findings: [], conditions: [{ id: "R1", description: "Synthetic required check remains unresolved." }] };
|
|
103
|
+
const reviewText = "NCCGS-HANDOFF: COMPLETE\n\n```nccgs-review\n" + JSON.stringify(verdict) + "\n```\n";
|
|
104
|
+
const observedReview = observe(f.project, f.result(review, { content: [{ type: "text", text: reviewText }] }), "agent_result");
|
|
105
|
+
assert.equal(fs.readFileSync(path.join(f.project, reviewPath), "utf8"), reviewText);
|
|
106
|
+
assert.equal(f.state().reservations[1].status, "completed");
|
|
107
|
+
assert.throws(() => recordStandalone(f.project, f.task(), "review", { ...f.stage(review, observedReview), evidence: artifact(f.project, reviewPath),
|
|
108
|
+
...verdict, verdict: "PASS", criteriaMet: true, conditions: [] }), /differs from.*handoff/);
|
|
109
|
+
assert.ok(!f.task().closed);
|
|
110
|
+
} finally { f.dispose(); }
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
for (const scenario of ["partial", "missing", "multiple", "oversized", "incomplete", "existing", "source-change"]) test(`returned handoff fails closed: ${scenario}`, () => {
|
|
114
|
+
const f = fixture("audit", true);
|
|
115
|
+
try {
|
|
116
|
+
const run = f.dispatch({ agent: "nccgs-engine-programmer", handoffPaths: [f.analysis], handoffTransport: "native-result" }); f.start(run);
|
|
117
|
+
const text = "NCCGS-HANDOFF: COMPLETE\nSYNTHETIC returned content.\n";
|
|
118
|
+
let content = [{ type: "text", text }], extra = {};
|
|
119
|
+
if (scenario === "partial") content[0].text = text.replace("COMPLETE", "PARTIAL");
|
|
120
|
+
if (scenario === "missing") content = "not a supported native content envelope";
|
|
121
|
+
if (scenario === "multiple") content.push({ type: "text", text: "extra" });
|
|
122
|
+
if (scenario === "oversized") content[0].text += "x".repeat(262144);
|
|
123
|
+
if (scenario === "incomplete") extra = { harnessNoteCount: 1 };
|
|
124
|
+
if (scenario === "existing") f.write(f.analysis, "NCCGS-HANDOFF: COMPLETE\nPreserved existing bytes.\n");
|
|
125
|
+
if (scenario === "source-change") f.write("Assets/fixture.cs", "// SYNTHETIC forbidden mutation.\n");
|
|
126
|
+
observe(f.project, f.result(run, { content, ...extra }), "agent_result");
|
|
127
|
+
assert.equal(f.state().reservations[0].status, "failed");
|
|
128
|
+
if (scenario === "existing") assert.match(fs.readFileSync(path.join(f.project, f.analysis), "utf8"), /Preserved existing/);
|
|
129
|
+
else if (scenario !== "partial") assert.ok(!fs.existsSync(path.join(f.project, f.analysis)));
|
|
130
|
+
assert.ok(!f.task().closed);
|
|
131
|
+
} finally { f.dispose(); }
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
test("returned handoff cannot be requested for source writers, multiple paths or unknown transport", () => {
|
|
135
|
+
const f = fixture();
|
|
136
|
+
try {
|
|
137
|
+
assert.throws(() => f.dispatch({ handoffPaths: [f.analysis], handoffTransport: "native-result" }), /read-only artifact/);
|
|
138
|
+
assert.throws(() => f.dispatch({ handoffTransport: "invented" }), /Unknown handoff transport/);
|
|
139
|
+
assert.equal(f.state().usage.dispatches, 0);
|
|
140
|
+
} finally { f.dispose(); }
|
|
141
|
+
const a = fixture("audit");
|
|
142
|
+
try {
|
|
143
|
+
assert.throws(() => a.dispatch({ handoffTransport: "native-result", handoffPaths: [a.analysis, ".nccgs/evidence/analysis/synthetic-review.md"] }), /one declared/);
|
|
144
|
+
assert.equal(a.state().usage.dispatches, 0);
|
|
145
|
+
} finally { a.dispose(); }
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
test("native-result transport rejects mismatched hook identities without publishing", () => {
|
|
149
|
+
const f = fixture("audit");
|
|
150
|
+
try {
|
|
151
|
+
const run = f.dispatch({ handoffPaths: [f.analysis], handoffTransport: "native-result" }); f.start(run);
|
|
152
|
+
const dispatch = f.state().reservations[0], base = f.result(run, { content: [{ type: "text", text: "NCCGS-HANDOFF: COMPLETE\nSYNTHETIC" }] });
|
|
153
|
+
for (const field of ["session_id", "tool_use_id", "hook_event_name", "tool_name"]) {
|
|
154
|
+
const result = captureReturnedHandoff(f.project, dispatch, { ...base, [field]: "incorrect" }, { status: "completed" }, dispatch.snapshot);
|
|
155
|
+
assert.equal(result.status, "rejected");
|
|
156
|
+
}
|
|
157
|
+
for (const envelope of [{ ...base, tool_response: { ...base.tool_response, agentId: "wrong" } },
|
|
158
|
+
{ ...base, tool_input: { ...base.tool_input, subagent_type: "wrong" } }])
|
|
159
|
+
assert.equal(captureReturnedHandoff(f.project, dispatch, envelope, { status: "completed" }, dispatch.snapshot).status, "rejected");
|
|
160
|
+
assert.ok(!fs.existsSync(path.join(f.project, f.analysis)));
|
|
161
|
+
} finally { f.dispose(); }
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
test("legacy file transport never captures arbitrary native response content", () => {
|
|
165
|
+
const f = fixture("audit");
|
|
166
|
+
try {
|
|
167
|
+
const run = f.dispatch({ handoffPaths: [f.analysis] }); f.start(run);
|
|
168
|
+
observe(f.project, f.result(run, { content: [{ type: "text", text: "NCCGS-HANDOFF: COMPLETE\nSYNTHETIC" }] }), "agent_result");
|
|
169
|
+
assert.equal(f.state().reservations[0].status, "failed");
|
|
170
|
+
assert.ok(!fs.existsSync(path.join(f.project, f.analysis)));
|
|
171
|
+
} finally { f.dispose(); }
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
test("synthetic foreground native result completes a reserved dispatch and records lifecycle output", () => {
|
|
175
|
+
const f = fixture();
|
|
176
|
+
try {
|
|
177
|
+
const run = f.dispatch(); f.start(run);
|
|
178
|
+
const observation = observe(f.project, f.result(run), "agent_result");
|
|
179
|
+
assert.equal(f.state().reservations[0].status, "completed");
|
|
180
|
+
assert.equal(observation.requestedEffort, "high");
|
|
181
|
+
assert.equal(observation.observedEffort, null);
|
|
182
|
+
assert.ok(observation.missing.includes("effort"));
|
|
183
|
+
const raw = json(path.join(f.project, observation.observationSource.path));
|
|
184
|
+
assert.equal(raw.provenance.kind, "provider-log");
|
|
185
|
+
assert.doesNotMatch(JSON.stringify(raw), /SECRET_PROMPT|SECRET_RESULT|SECRET_KEY/);
|
|
186
|
+
assert.equal(raw.durationMs, 123);
|
|
187
|
+
assert.equal(raw.toolUseCount, 4);
|
|
188
|
+
assert.equal(raw.totalTokens, 456);
|
|
189
|
+
assert.equal(raw.costUsd, null);
|
|
190
|
+
const recorded = recordStandalone(f.project, f.task(), "implementation", f.stage(run, observation));
|
|
191
|
+
assert.equal(recorded.stages.implementation.dispatchId, run.reservation.id);
|
|
192
|
+
} finally { f.dispose(); }
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
test("review readiness refuses reservation without consuming quota", () => {
|
|
196
|
+
const f = fixture();
|
|
197
|
+
try {
|
|
198
|
+
assert.throws(() => f.dispatch({ kind: "review", agent: "nccgs-adversarial-reviewer", writePaths: [] }), /implementation.*missing|stale/i);
|
|
199
|
+
assert.equal(f.state().usage.dispatches, 0);
|
|
200
|
+
assert.equal(f.state().active.length, 0);
|
|
201
|
+
} finally { f.dispose(); }
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
test("reviewer writes its declared handoff but cannot edit source or evidence outside it", () => {
|
|
205
|
+
const f = fixture();
|
|
206
|
+
try {
|
|
207
|
+
const impl = f.dispatch(); f.start(impl);
|
|
208
|
+
const observation = observe(f.project, f.result(impl), "agent_result");
|
|
209
|
+
recordStandalone(f.project, f.task(), "implementation", f.stage(impl, observation));
|
|
210
|
+
const review = f.dispatch({ kind: "review", agent: "nccgs-adversarial-reviewer", writePaths: [], handoffPaths: [f.analysis] }); f.start(review);
|
|
211
|
+
const edit = relative => ({ session_id: "synthetic-session", agent_id: review.agentId, tool_name: "Write", tool_input: { file_path: path.join(f.project, relative), content: "synthetic" } });
|
|
212
|
+
assert.doesNotThrow(() => guardTool(f.project, edit(f.analysis)));
|
|
213
|
+
for (const relative of ["Assets/fixture.cs", ".nccgs/evidence/analysis/other.md", ".nccgs/evidence/runtime/forged.json"])
|
|
214
|
+
assert.throws(() => guardTool(f.project, edit(relative)), /cannot edit source|declared analysis|immutable/i);
|
|
215
|
+
f.write(f.analysis, "NCCGS-HANDOFF: COMPLETE\nSynthetic negative review is a complete handoff, not product PASS.");
|
|
216
|
+
observe(f.project, f.result(review), "agent_result");
|
|
217
|
+
assert.equal(f.state().reservations[1].status, "completed");
|
|
218
|
+
assert.match(fs.readFileSync(path.join(root, ".claude/agents/nccgs-adversarial-reviewer.md"), "utf8"), /^tools:.*\bWrite\b/m);
|
|
219
|
+
} finally { f.dispose(); }
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
test("a stale unclaimed launch is released by its hook, never refunded or relaunched", () => {
|
|
223
|
+
const f = fixture();
|
|
224
|
+
try {
|
|
225
|
+
const run = f.dispatch();
|
|
226
|
+
f.write("Assets/fixture.cs", "// source changed after reservation");
|
|
227
|
+
assert.throws(() => guardTool(f.project, run.envelope), /snapshot stale/i);
|
|
228
|
+
const state = f.state();
|
|
229
|
+
assert.equal(state.active.length, 0); assert.equal(state.usage.dispatches, 1);
|
|
230
|
+
assert.equal(state.reservations[0].invocationStatus, "not-started");
|
|
231
|
+
assert.equal(state.reservations[0].launchRejection.toolUseId, run.envelope.tool_use_id);
|
|
232
|
+
assert.throws(() => guardTool(f.project, run.envelope), /available to claim/i);
|
|
233
|
+
const next = f.dispatch(); f.start(next);
|
|
234
|
+
assert.throws(() => rejectUnclaimed(f.project, next.reservation.id, { sessionId: "synthetic-session", toolUseId: "denied", agent: next.request.agent, reason: "synthetic" }), /never-claimed/i);
|
|
235
|
+
assert.equal(f.state().active.length, 1);
|
|
236
|
+
} finally { f.dispose(); }
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
test("partial handoff survives turn exhaustion and recovery consumes a fresh attempt", () => {
|
|
240
|
+
const f = fixture();
|
|
241
|
+
try {
|
|
242
|
+
const firstPath = ".nccgs/evidence/analysis/first.md", nextPath = ".nccgs/evidence/analysis/next.md";
|
|
243
|
+
const first = f.dispatch({ handoffPaths: [firstPath] }); f.start(first);
|
|
244
|
+
f.write(firstPath, "NCCGS-HANDOFF: PARTIAL\nFinished analysis; implementation remains.");
|
|
245
|
+
const partial = observe(f.project, f.result(first), "agent_result");
|
|
246
|
+
assert.equal(f.state().reservations[0].status, "failed");
|
|
247
|
+
assert.equal(f.state().reservations[0].invocationStatus, "completed");
|
|
248
|
+
assert.throws(() => recordStandalone(f.project, f.task(), "implementation", f.stage(first, partial)), /handoff is partial/i);
|
|
249
|
+
const next = f.dispatch({ handoffPaths: [nextPath], reason: "Continue bounded unfinished work from first attempt" }); f.start(next);
|
|
250
|
+
f.write(nextPath, "NCCGS-HANDOFF: COMPLETE\nSynthetic bounded work finished; references first attempt.");
|
|
251
|
+
const complete = observe(f.project, f.result(next), "agent_result");
|
|
252
|
+
recordStandalone(f.project, f.task(), "implementation", f.stage(next, complete));
|
|
253
|
+
assert.equal(f.state().usage.dispatches, 2);
|
|
254
|
+
assert.equal(f.state().reservations[0].status, "failed");
|
|
255
|
+
assert.equal(f.state().reservations[1].status, "completed");
|
|
256
|
+
assert.equal(f.state().active.length, 0);
|
|
257
|
+
} finally { f.dispose(); }
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
test("synthetic start, stop and async launch cannot manufacture completion", () => {
|
|
261
|
+
const f = fixture();
|
|
262
|
+
try {
|
|
263
|
+
const run = f.dispatch();
|
|
264
|
+
const first = f.start(run);
|
|
265
|
+
assert.equal(first.status, "agent_start");
|
|
266
|
+
assert.equal(f.state().reservations[0].status, "running");
|
|
267
|
+
observe(f.project, { session_id: "synthetic-session", agent_id: run.agentId, agent_type: run.request.agent,
|
|
268
|
+
tool_response: { status: "completed", resolvedModel: "claude-opus-5-5" } }, "agent_stop");
|
|
269
|
+
assert.equal(f.state().reservations[0].status, "running");
|
|
270
|
+
const background = observe(f.project, f.result(run, { status: "async_launched" }), "agent_result");
|
|
271
|
+
assert.equal(f.state().reservations[0].status, "uncertain");
|
|
272
|
+
assert.throws(() => recordStandalone(f.project, f.task(), "implementation", f.stage(run, background)), /incomplete|completed|dispatch/i);
|
|
273
|
+
const foreground = observe(f.project, f.result(run), "agent_result");
|
|
274
|
+
assert.equal(f.state().reservations[0].status, "completed");
|
|
275
|
+
assert.equal(foreground.observedEffort, null);
|
|
276
|
+
} finally { f.dispose(); }
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
test("native tool failure preserves an uncertain attempt instead of a completed result", () => {
|
|
280
|
+
const f = fixture();
|
|
281
|
+
try {
|
|
282
|
+
const run = f.dispatch(); f.start(run);
|
|
283
|
+
const result = f.hook("runtime-observation.mjs", "failure", { ...run.envelope, hook_event_name: "PostToolUseFailure", error: "SYNTHETIC provider interruption" });
|
|
284
|
+
assert.equal(result.status, 0, result.stderr);
|
|
285
|
+
assert.equal(f.state().reservations[0].status, "uncertain");
|
|
286
|
+
assert.equal(f.state().usage.dispatches, 1);
|
|
287
|
+
assert.equal(f.state().telemetry.metrics.costUsd.total, null);
|
|
288
|
+
} finally { f.dispose(); }
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
test("native hook executables enforce reserved dispatch and capture foreground result", () => {
|
|
292
|
+
const f = fixture();
|
|
293
|
+
try {
|
|
294
|
+
const unreserved = f.hook("closure-guard.mjs", "pre", { session_id: "synthetic-session", tool_name: "Agent", tool_use_id: "unreserved", tool_input: { subagent_type: "nccgs-gameplay-programmer", prompt: "SYNTHETIC unreserved request" } });
|
|
295
|
+
assert.equal(unreserved.status, 2);
|
|
296
|
+
assert.match(unreserved.stderr, /Reserve|reservation/i);
|
|
297
|
+
const run = f.dispatch();
|
|
298
|
+
let outcome = f.hook("closure-guard.mjs", "pre", run.envelope);
|
|
299
|
+
assert.equal(outcome.status, 0, outcome.stderr);
|
|
300
|
+
outcome = f.hook("runtime-observation.mjs", undefined, f.result(run));
|
|
301
|
+
assert.equal(outcome.status, 0, outcome.stderr);
|
|
302
|
+
assert.match(outcome.stdout, /runtime evidence/);
|
|
303
|
+
assert.equal(f.state().reservations[0].status, "completed");
|
|
304
|
+
const stopped = f.hook("closure-guard.mjs", "stop", { session_id: "synthetic-session", last_assistant_message: "Status: DONE" });
|
|
305
|
+
assert.equal(JSON.parse(stopped.stdout).decision, "block");
|
|
306
|
+
} finally { f.dispose(); }
|
|
307
|
+
});
|
|
308
|
+
|
|
309
|
+
test("implementation writes need the correlated worker and owned path; nested delegation is rejected", () => {
|
|
310
|
+
const f = fixture();
|
|
311
|
+
try {
|
|
312
|
+
const run = f.dispatch(); f.start(run);
|
|
313
|
+
const write = relative => ({ session_id: "synthetic-session", agent_id: run.agentId, tool_name: "Edit", tool_input: { file_path: path.join(f.project, relative), old_string: "fixture", new_string: "changed" } });
|
|
314
|
+
assert.doesNotThrow(() => guardTool(f.project, write("Assets/fixture.cs")));
|
|
315
|
+
assert.throws(() => guardTool(f.project, write("Assets/other.cs")), /owned paths/i);
|
|
316
|
+
assert.throws(() => guardTool(f.project, { ...write("Assets/fixture.cs"), agent_id: "unrelated-agent" }), /correlated|running/i);
|
|
317
|
+
assert.throws(() => guardTool(f.project, { ...write("Assets/fixture.cs"), agent_id: undefined }), /correlated|running/i);
|
|
318
|
+
assert.throws(() => guardTool(f.project, { ...run.envelope, agent_id: run.agentId }), /recursively delegate/i);
|
|
319
|
+
assert.throws(() => guardTool(f.project, write(".nccgs/tasks/synthetic.json")), /governance/i);
|
|
320
|
+
assert.throws(() => guardTool(f.project, write(".nccgs/evidence/runtime/sources/forged.json")), /never authored/i);
|
|
321
|
+
} finally { f.dispose(); }
|
|
322
|
+
});
|
|
323
|
+
|
|
324
|
+
for (const mode of ["audit", "plan"]) test(`${mode} workers can write declared analysis artifacts but cannot mutate source or run shell/MCP`, () => {
|
|
325
|
+
const f = fixture(mode);
|
|
326
|
+
try {
|
|
327
|
+
const run = f.dispatch(); f.start(run);
|
|
328
|
+
const input = { session_id: "synthetic-session", agent_id: run.agentId };
|
|
329
|
+
const edit = relative => ({ ...input, tool_name: "Write", tool_input: { file_path: path.join(f.project, relative), content: "SYNTHETIC analysis" } });
|
|
330
|
+
assert.doesNotThrow(() => guardTool(f.project, edit(f.analysis)));
|
|
331
|
+
assert.throws(() => guardTool(f.project, edit(".nccgs/evidence/analysis/undeclared.md")), /declared analysis/i);
|
|
332
|
+
assert.throws(() => guardTool(f.project, edit("Assets/fixture.cs")), /cannot edit source/i);
|
|
333
|
+
assert.doesNotThrow(() => guardTool(f.project, { ...input, tool_name: "Read", tool_input: { file_path: path.join(f.project, "Assets/fixture.cs") } }));
|
|
334
|
+
for (const name of ["Bash", "PowerShell"]) assert.throws(() => guardTool(f.project, { ...input, tool_name: name, tool_input: { command: "echo synthetic" } }), /shell execution is blocked/i);
|
|
335
|
+
assert.throws(() => guardTool(f.project, { ...input, tool_name: "mcp__unity__execute", tool_input: { code: "synthetic mutation" } }), /read-only allowlist|Unity CLI-only/i);
|
|
336
|
+
assert.throws(() => guardTool(f.project, { ...input, tool_name: "Bash", tool_input: { command: "node .claude/nccgs/tools/task.mjs snapshot" } }), /Coordinator owns/i);
|
|
337
|
+
assert.throws(() => guardTool(f.project, { session_id: "synthetic-session", tool_name: "Bash", tool_input: { command: "node .claude/nccgs/tools/task.mjs inspect --id synthetic; echo bypass" } }), /shell execution is blocked/i);
|
|
338
|
+
} finally { f.dispose(); }
|
|
339
|
+
});
|
|
340
|
+
|
|
341
|
+
test("missing observed model cannot satisfy lifecycle despite foreground completion", () => {
|
|
342
|
+
const f = fixture();
|
|
343
|
+
try {
|
|
344
|
+
const run = f.dispatch(); f.start(run);
|
|
345
|
+
const observation = observe(f.project, f.result(run, { resolvedModel: undefined, modelsUsed: [] }), "agent_result");
|
|
346
|
+
assert.equal(observation.observedModel, null);
|
|
347
|
+
assert.throws(() => recordStandalone(f.project, f.task(), "implementation", f.stage(run, observation)), /Observed model|route/i);
|
|
348
|
+
assert.equal(f.task().stages.implementation, undefined);
|
|
349
|
+
} finally { f.dispose(); }
|
|
350
|
+
});
|
|
351
|
+
|
|
352
|
+
test("ambiguous same-role SubagentStart never guesses a worker reservation", () => {
|
|
353
|
+
const f = fixture();
|
|
354
|
+
try {
|
|
355
|
+
const first = f.dispatch();
|
|
356
|
+
const second = f.dispatch({ writePaths: ["Assets/second.cs"] });
|
|
357
|
+
guardTool(f.project, first.envelope); guardTool(f.project, second.envelope);
|
|
358
|
+
const observed = observe(f.project, { session_id: "synthetic-session", agent_type: first.request.agent, agent_id: "ambiguous-worker" }, "agent_start");
|
|
359
|
+
assert.equal(observed.dispatchId, null);
|
|
360
|
+
assert.ok(f.state().reservations.every(item => item.agentId === null));
|
|
361
|
+
assert.throws(() => guardTool(f.project, { session_id: "synthetic-session", agent_id: "ambiguous-worker", tool_name: "Write", tool_input: { file_path: path.join(f.project, "Assets/fixture.cs"), content: "SYNTHETIC" } }), /correlated|running/i);
|
|
362
|
+
} finally { f.dispose(); }
|
|
363
|
+
});
|
|
364
|
+
|
|
365
|
+
test("changed execution policy blocks new provider dispatch and recovery report still works through hooks", () => {
|
|
366
|
+
const f = fixture();
|
|
367
|
+
try {
|
|
368
|
+
f.write(".nccgs/execution.json", { schemaVersion: 1, name: "standalone", externalReview: { mode: "required" } });
|
|
369
|
+
assert.throws(() => f.dispatch(), /execution policy changed/i);
|
|
370
|
+
f.write(".nccgs/execution.json", "malformed");
|
|
371
|
+
const input = { session_id: "synthetic-session", tool_name: "Bash", tool_input: { command: 'node .claude/nccgs/tools/task.mjs report --id synthetic --outcome blocked --summary "SYNTHETIC TEST invalid policy"' } };
|
|
372
|
+
const allowed = f.hook("closure-guard.mjs", "pre", input);
|
|
373
|
+
assert.equal(allowed.status, 0, allowed.stderr);
|
|
374
|
+
const report = recordStandalone(f.project, f.task(), "report", { outcome: "blocked", summary: "SYNTHETIC TEST invalid policy" });
|
|
375
|
+
assert.equal(report.stages.report.verificationState.verified, false);
|
|
376
|
+
assert.equal(report.stages.report.executionSummary.telemetry.metrics.costUsd.total, null);
|
|
377
|
+
const composed = f.hook("closure-guard.mjs", "pre", { ...input, tool_input: { command: input.tool_input.command + "; echo injected" } });
|
|
378
|
+
assert.equal(composed.status, 2);
|
|
379
|
+
} finally { f.dispose(); }
|
|
380
|
+
});
|
|
381
|
+
|
|
382
|
+
test("immutable external request and runtime files cannot be rewritten through structured tools", () => {
|
|
383
|
+
const f = fixture();
|
|
384
|
+
try {
|
|
385
|
+
for (const relative of [".nccgs/evidence/external/requests/export.json", ".nccgs/evidence/runtime/sources/raw.json"]) {
|
|
386
|
+
assert.throws(() => guardTool(f.project, { session_id: "synthetic-session", tool_name: "Write", tool_input: { file_path: path.join(f.project, relative), content: "SYNTHETIC ATTEMPT" } }), /immutable/i);
|
|
387
|
+
}
|
|
388
|
+
const run = f.dispatch(); f.start(run);
|
|
389
|
+
const observation = observe(f.project, f.result(run), "agent_result");
|
|
390
|
+
f.write(observation.observationSource.path, "SYNTHETIC CORRUPTION");
|
|
391
|
+
assert.throws(() => observe(f.project, f.result(run), "agent_result"), /changed|hash|mismatch|stale/i);
|
|
392
|
+
} finally { f.dispose(); }
|
|
393
|
+
});
|
|
394
|
+
|
|
395
|
+
function prepareRepair(f, budget) {
|
|
396
|
+
const impl = f.dispatch(); f.start(impl);
|
|
397
|
+
const implementation = observe(f.project, f.result(impl), "agent_result");
|
|
398
|
+
recordStandalone(f.project, f.task(), "implementation", f.stage(impl, implementation));
|
|
399
|
+
const review = f.dispatch({ kind: "review", agent: "nccgs-adversarial-reviewer", writePaths: [] }); f.start(review);
|
|
400
|
+
const observed = observe(f.project, f.result(review), "agent_result");
|
|
401
|
+
const feedback = f.write(f.analysis, "Synthetic negative review: replace fixture baseline; never actual provider evidence.");
|
|
402
|
+
recordStandalone(f.project, f.task(), "review", { snapshot: observed.snapshot, dispatchId: review.reservation.id,
|
|
403
|
+
runtime: artifact(f.project, observed.path), verdict: "CHANGES_REQUIRED", criteriaMet: false, findings: [], evidence: feedback });
|
|
404
|
+
const packet = { schemaVersion: 1, taskId: "synthetic", contractHash: f.task().contractHash, sourceSnapshot: snapshot(f.project, f.task()).sha256,
|
|
405
|
+
problem: "Synthetic baseline correction needed", solution: "Replace the fixture comment with the chosen correction",
|
|
406
|
+
changes: [{ path: "Assets/fixture.cs", instruction: "Replace the baseline comment with a synthetic correction" }], context: [feedback], verification: ["synthetic-check"], ...(budget ? { budget } : {}) };
|
|
407
|
+
const request = { purpose: "repair", workPacket: ".nccgs/evidence/work-packets/repair.json", handoffPaths: [".nccgs/evidence/analysis/repair.md"] };
|
|
408
|
+
f.write(request.workPacket, packet);
|
|
409
|
+
return { packet, request };
|
|
410
|
+
}
|
|
411
|
+
function repairCall(f, run, id, name = "Read", tool = { file_path: "Assets/fixture.cs" }) {
|
|
412
|
+
return { hook_event_name: "PreToolUse", session_id: "synthetic-session", agent_id: run.agentId, tool_use_id: id, tool_name: name, tool_input: tool };
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
function prepareUnityRepair(f, type = "unity-code", create = false, steps = 1) {
|
|
416
|
+
f.write("ProjectSettings/ProjectVersion.txt", "m_EditorVersion: SYNTHETIC-FIXTURE\n");
|
|
417
|
+
f.write("Assets/fixture.cs.meta", "fileFormatVersion: 2\nguid: 11111111111111111111111111111111\n");
|
|
418
|
+
const target = type === "unity-editor" ? "Assets/Fixture.prefab" : create ? "Assets/Regression.cs" : "Assets/fixture.cs";
|
|
419
|
+
if (type === "unity-editor" && !create) {
|
|
420
|
+
f.write(target, "%YAML SYNTHETIC FIXTURE; not a real Unity prefab\n");
|
|
421
|
+
f.write(`${target}.meta`, "fileFormatVersion: 2\nguid: 22222222222222222222222222222222\n");
|
|
422
|
+
}
|
|
423
|
+
const capabilities = f.write(".nccgs/evidence/unity-capabilities.txt", "SYNTHETIC CLI catalog fixture; no Unity or provider execution. synthetic_operation(positionals)\n");
|
|
424
|
+
const old = prepareRepair(f);
|
|
425
|
+
const packet = { ...old.packet, schemaVersion: 2, repairType: type,
|
|
426
|
+
changes: [{ path: target, operation: create ? "create" : "edit", instruction: "Apply only the synthetic fixture change" }],
|
|
427
|
+
unity: { capabilities, steps: Array.from({ length: steps }, (_, i) => ({ id: `step-${i + 1}`, command: "eval", arguments: ['return "SYNTHETIC ONLY";'], timeoutMs: 1000 })) } };
|
|
428
|
+
f.write(old.request.workPacket, packet);
|
|
429
|
+
return { packet, target, request: { ...old.request, writePaths: [target] } };
|
|
430
|
+
}
|
|
431
|
+
function editorCall(f, run, step, id = step.id) {
|
|
432
|
+
return repairCall(f, run, id, "Bash", { command: repairEditorCommand(run.reservation, step), timeout: step.timeoutMs + 5000 });
|
|
433
|
+
}
|
|
434
|
+
const syntheticEditorSuccess = () => ({ status: 0, stdout: '{"synthetic":true}', stderr: "" });
|
|
435
|
+
|
|
436
|
+
test("dirty-scene refusal is a retained failed attempt, with no automatic retry, source change or review waiver", () => {
|
|
437
|
+
const f = fixture();
|
|
438
|
+
try {
|
|
439
|
+
const { packet, request, target } = prepareUnityRepair(f, "unity-editor"), run = f.dispatch(request); f.start(run);
|
|
440
|
+
const before = fs.readFileSync(path.join(f.project, target));
|
|
441
|
+
guardTool(f.project, editorCall(f, run, packet.unity.steps[0]));
|
|
442
|
+
const receipt = runRepairEditor(f.project, "synthetic", run.reservation.id, "step-1", (exe, args) => {
|
|
443
|
+
const token = args[2].match(/token = "([a-f0-9-]+)"/)[1];
|
|
444
|
+
return { status: 0, stdout: JSON.stringify({ success: true, data: { result: { success: true,
|
|
445
|
+
result: { nccgsSceneGuard: "loaded-scenes-clean-v1", token, blockedScene: "<untitled>" } } } }), stderr: "" };
|
|
446
|
+
});
|
|
447
|
+
assert.equal(receipt.status, "FAIL"); assert.equal(receipt.operationStatus, "failed");
|
|
448
|
+
assert.match(receipt.message, /Unsaved scene/);
|
|
449
|
+
assert.equal(json(path.join(f.project, receipt.evidence.path)).blockedScene, "<untitled>");
|
|
450
|
+
assert.deepEqual(fs.readFileSync(path.join(f.project, target)), before);
|
|
451
|
+
assert.throws(() => runRepairEditor(f.project, "synthetic", run.reservation.id, "step-1", syntheticEditorSuccess), /already attempted/);
|
|
452
|
+
f.write(request.handoffPaths[0], "NCCGS-HANDOFF: PARTIAL\nUnsaved scene; owner action needed.");
|
|
453
|
+
observe(f.project, f.result(run), "agent_result");
|
|
454
|
+
assert.equal(f.state().usage.dispatches, 3);
|
|
455
|
+
assert.equal(f.state().active.length, 0);
|
|
456
|
+
assert.equal(f.task().stages.review.verdict, "CHANGES_REQUIRED");
|
|
457
|
+
} finally { f.dispose(); }
|
|
458
|
+
});
|
|
459
|
+
|
|
460
|
+
test("previously pinned Unity reservations retain their original operation without retroactive guard injection", () => {
|
|
461
|
+
const f = fixture();
|
|
462
|
+
try {
|
|
463
|
+
const { packet, request } = prepareUnityRepair(f), run = f.dispatch(request);
|
|
464
|
+
const file = path.join(f.project, ".nccgs/orchestration.json"), ledger = json(file);
|
|
465
|
+
delete ledger.dispatches[run.reservation.id].workPacket.sceneSafety; // Pre-RC4 reservation fixture.
|
|
466
|
+
fs.writeFileSync(file, JSON.stringify(ledger)); f.start(run);
|
|
467
|
+
guardTool(f.project, editorCall(f, run, packet.unity.steps[0]));
|
|
468
|
+
const receipt = runRepairEditor(f.project, "synthetic", run.reservation.id, "step-1", (exe, args) => {
|
|
469
|
+
assert.equal(args[2], packet.unity.steps[0].arguments[0]); return syntheticEditorSuccess();
|
|
470
|
+
});
|
|
471
|
+
assert.equal(receipt.operationStatus, "completed");
|
|
472
|
+
assert.equal(json(path.join(f.project, receipt.evidence.path)).sceneGuard, undefined);
|
|
473
|
+
assert.throws(() => unityEditorInvocation(f.project, packet.unity.steps[0], "unknown-policy"), /unknown pinned/);
|
|
474
|
+
} finally { f.dispose(); }
|
|
475
|
+
});
|
|
476
|
+
|
|
477
|
+
test("work handoff records PARTIAL only with current output and never changes review, acceptance, history or spent quota", () => {
|
|
478
|
+
for (const variant of ["ready", "stale", "failed-check", "unmeasured", "reported", "no-allocation", "final-review", "unknown-cost", "active"]) {
|
|
479
|
+
const f = fixture("implement", false, { manualGates: ["OWNER-01"], ...(variant === "unmeasured" ? {
|
|
480
|
+
acceptance: [{ id: "UNMEASURED", check: "synthetic-check", description: "Missing measured evidence" }]
|
|
481
|
+
} : {}) });
|
|
482
|
+
try {
|
|
483
|
+
const work = f.dispatch(); f.start(work); const observed = observe(f.project, f.result(work), "agent_result");
|
|
484
|
+
recordStandalone(f.project, f.task(), "implementation", f.stage(work, observed));
|
|
485
|
+
const state = { id: "synthetic-run", taskId: "synthetic", contractHash: f.task().contractHash, createdAt: f.task().createdAt,
|
|
486
|
+
budgetAllocation: { synthetic: true }, budgetPhase: "work", costCoverage: "complete", invocations: [{ status: "finished", code: 0, terminal: { type: "result" } }] };
|
|
487
|
+
if (variant === "stale") f.write("Assets/fixture.cs", "// changed after verification");
|
|
488
|
+
if (variant === "failed-check") {
|
|
489
|
+
const task = f.task();
|
|
490
|
+
task.stages.implementation.checks[0].status = "FAIL";
|
|
491
|
+
f.write(".nccgs/tasks/synthetic.json", task);
|
|
492
|
+
}
|
|
493
|
+
if (variant === "reported") recordStandalone(f.project, f.task(), "report", { outcome: "blocked", summary: "Preserve explicit coordinator assessment" });
|
|
494
|
+
if (variant === "no-allocation") delete state.budgetAllocation;
|
|
495
|
+
if (variant === "final-review") state.budgetPhase = "final-review";
|
|
496
|
+
if (variant === "unknown-cost") state.costCoverage = "unknown-current-invocation";
|
|
497
|
+
if (variant === "active") f.dispatch({ kind: "review", agent: "nccgs-adversarial-reviewer", writePaths: [] });
|
|
498
|
+
const before = f.task(), ledger = fs.readFileSync(path.join(f.project, ".nccgs/orchestration.json"));
|
|
499
|
+
const result = settleWorkHandoff(f.project, state);
|
|
500
|
+
if (variant === "ready") {
|
|
501
|
+
assert.ok(result); assert.equal(f.task().outcome, "partial");
|
|
502
|
+
assert.equal(f.task().stages.report.origin, "supervisor-work-handoff");
|
|
503
|
+
assert.equal(f.task().stages.review, undefined); assert.equal(f.task().closed, undefined);
|
|
504
|
+
assert.deepEqual(f.task().contract, before.contract); assert.deepEqual(f.task().reviewBudget, before.reviewBudget);
|
|
505
|
+
assert.deepEqual(f.task().history.slice(0, before.history.length), before.history);
|
|
506
|
+
assert.equal(settleWorkHandoff(f.project, state), null);
|
|
507
|
+
assert.throws(() => taskRun(["close", "--project", f.project, "--id", "synthetic"]), /review.*missing|stale/i);
|
|
508
|
+
} else { assert.equal(result, null, variant); assert.deepEqual(f.task(), before, variant); }
|
|
509
|
+
assert.deepEqual(fs.readFileSync(path.join(f.project, ".nccgs/orchestration.json")), ledger);
|
|
510
|
+
} finally { f.dispose(); }
|
|
511
|
+
}
|
|
512
|
+
});
|
|
513
|
+
|
|
514
|
+
test("protected work supervisor settles a prose-only PARTIAL without spending a continuation or final-review reserve", async () => {
|
|
515
|
+
const f = fixture();
|
|
516
|
+
try {
|
|
517
|
+
fs.cpSync(path.join(root, ".claude/nccgs/tools"), path.join(f.project, ".claude/nccgs/tools"), { recursive: true });
|
|
518
|
+
f.write(".nccgs/evidence/budget.json", { schemaVersion: 1, authority: "Synthetic fixture only", totalUsd: 1, finalReviewUsd: 0.4, totalDurationMs: 60000, finalReviewDurationMs: 10000 });
|
|
519
|
+
registerReviewBudget(f.project, "synthetic", ".nccgs/evidence/budget.json");
|
|
520
|
+
f.write(".nccgs/native-fixture.mjs", `import {reserve} from '../.claude/nccgs/tools/orchestration.mjs';
|
|
521
|
+
import {guardTool} from '../.claude/nccgs/tools/tool-guard.mjs';
|
|
522
|
+
import {observe} from '../.claude/nccgs/tools/runtime.mjs';
|
|
523
|
+
import {record} from '../.claude/nccgs/tools/gates.mjs';
|
|
524
|
+
import {artifact} from '../.claude/nccgs/tools/policy.mjs';
|
|
525
|
+
const project=process.cwd(), session=process.env.NCCGS_SUPERVISED_SESSION;
|
|
526
|
+
const d=reserve(project,'synthetic',{kind:'implementation',agent:'nccgs-gameplay-programmer',requestKey:'once',reason:'SYNTHETIC',writePaths:['Assets/fixture.cs']});
|
|
527
|
+
const input={session_id:session,tool_name:'Agent',tool_use_id:'synthetic-tool',tool_input:{subagent_type:d.agent,prompt:'[NCCGS-DISPATCH:'+d.id+'] SYNTHETIC'}};
|
|
528
|
+
guardTool(project,input);observe(project,{session_id:session,agent_id:'synthetic-worker',agent_type:d.agent},'agent_start');
|
|
529
|
+
const o=observe(project,{...input,tool_response:{agentId:'synthetic-worker',status:'completed',resolvedModel:'claude-opus-5-5',totalDurationMs:123,totalToolUseCount:2,totalTokens:456}},'agent_result');
|
|
530
|
+
record(project,'synthetic','implementation',{snapshot:o.snapshot,dispatchId:d.id,runtime:artifact(project,o.path),checks:[{name:'synthetic-check',status:'PASS',evidence:artifact(project,'.nccgs/evidence/synthetic-check.txt')}]});
|
|
531
|
+
console.log(JSON.stringify({type:'result',session_id:session,total_cost_usd:0.2,result:'PARTIAL: ready for independent review. Synthetic provider fixture only.'}));`);
|
|
532
|
+
f.write(".nccgs/prompt.txt", "SYNTHETIC provider integration test; no Claude execution");
|
|
533
|
+
const env = { ...process.env }; for (const key of ["NCCGS_SUPERVISED_RUN", "NCCGS_SUPERVISED_SESSION", "NCCGS_TASK_ID", "CLAUDECODE"]) delete env[key];
|
|
534
|
+
const plan = { project: f.project, ...nativeProvider(f.project, path.join(f.project, ".nccgs/native-fixture.mjs"), { env }),
|
|
535
|
+
supervision: { taskId: "synthetic", promptFile: path.join(f.project, ".nccgs/prompt.txt"), budgetUsd: 1, maxResumes: 0, timeoutMs: 20000, budgetPhase: "work" } };
|
|
536
|
+
assert.equal(await supervise(plan), 0);
|
|
537
|
+
const id = fs.readdirSync(path.join(f.project, ".nccgs/runs"), { withFileTypes: true }).find(e => e.isDirectory()).name;
|
|
538
|
+
const run = json(path.join(f.project, `.nccgs/runs/${id}/run.json`));
|
|
539
|
+
assert.equal(run.invocations.length, 1); assert.equal(run.cumulativeCostUsd, 0.2); assert.equal(run.taskOutcome, "partial");
|
|
540
|
+
assert.equal(run.workHandoff.origin, "supervisor-work-handoff"); assert.equal(f.task().stages.review, undefined);
|
|
541
|
+
assert.equal(reviewBudgetStatus(f.project, "synthetic").protectedReviewUsd, 0.4);
|
|
542
|
+
const history = f.task().history;
|
|
543
|
+
assert.equal(await supervise({ ...plan, supervision: { recoverRun: id } }), 0);
|
|
544
|
+
assert.deepEqual(f.task().history, history); assert.equal(f.state().usage.dispatches, 1);
|
|
545
|
+
} finally { f.dispose(); }
|
|
546
|
+
});
|
|
547
|
+
|
|
548
|
+
test("work handoff rejects the latest failed check even when an older PASS was recorded again", () => {
|
|
549
|
+
const f = fixture();
|
|
550
|
+
try {
|
|
551
|
+
const state = { id: "synthetic-run", taskId: "synthetic", contractHash: f.task().contractHash, createdAt: f.task().createdAt,
|
|
552
|
+
budgetAllocation: { synthetic: true }, budgetPhase: "work", costCoverage: "complete", invocations: [{ status: "finished", code: 0, terminal: { type: "result" } }] };
|
|
553
|
+
for (const exitCode of [1, 0]) {
|
|
554
|
+
const work = f.dispatch(); f.start(work); const observed = observe(f.project, f.result(work), "agent_result");
|
|
555
|
+
const started = startVerification(f.project, "synthetic", { name: "synthetic-check", executable: process.execPath, arguments: ["-e", `process.exit(${exitCode})`] });
|
|
556
|
+
const receipt = waitVerification(f.project, started.jobId, "synthetic", 10000);
|
|
557
|
+
assert.equal(receipt.status, exitCode ? "FAIL" : "PASS");
|
|
558
|
+
recordStandalone(f.project, f.task(), "implementation", f.stage(work, observed));
|
|
559
|
+
const report = settleWorkHandoff(f.project, state);
|
|
560
|
+
if (exitCode) assert.equal(report, null); else assert.ok(report);
|
|
561
|
+
}
|
|
562
|
+
} finally { f.dispose(); }
|
|
563
|
+
});
|
|
564
|
+
|
|
565
|
+
test("Unity repair creates code and imports metadata before completion, retaining negative history and manual gates", () => {
|
|
566
|
+
const f = fixture("implement", false, { manualGates: ["OWNER-01"] });
|
|
567
|
+
try {
|
|
568
|
+
const { packet, request, target } = prepareUnityRepair(f, "unity-code", true);
|
|
569
|
+
const original = f.task(), feedback = fs.readFileSync(path.join(f.project, f.analysis));
|
|
570
|
+
const run = f.dispatch(request); f.start(run);
|
|
571
|
+
assert.equal(run.reservation.workPacket.budget.workCalls, 24);
|
|
572
|
+
assert.equal(run.reservation.workPacket.budget.readCalls, 12);
|
|
573
|
+
assert.match(dispatchPrompt(f.project, "synthetic", run.reservation.id).prompt, /Never hand-write \.meta/);
|
|
574
|
+
assert.throws(() => runRepairEditor(f.project, "synthetic", run.reservation.id, "step-1", syntheticEditorSuccess), /tool admission/);
|
|
575
|
+
guardTool(f.project, repairCall(f, run, "new-code", "Write", { file_path: target }));
|
|
576
|
+
f.write(target, "// SYNTHETIC new test source; no Unity execution\n");
|
|
577
|
+
const call = editorCall(f, run, packet.unity.steps[0]);
|
|
578
|
+
guardTool(f.project, call);
|
|
579
|
+
const result = runRepairEditor(f.project, "synthetic", run.reservation.id, "step-1", (executable, args, options) => {
|
|
580
|
+
assert.match(executable, /^unity(?:\.exe)?$/); assert.equal(options.shell, false); assert.equal(options.windowsHide, true);
|
|
581
|
+
assert.equal(args[args.indexOf("--project-path") + 1], fs.realpathSync(f.project));
|
|
582
|
+
assert.deepEqual(args.slice(0, 2), ["command", "eval"]);
|
|
583
|
+
assert.match(args[2], /GetSceneAt.*isDirty/s);
|
|
584
|
+
assert.ok(args[2].endsWith('return "SYNTHETIC ONLY";'));
|
|
585
|
+
f.write(`${target}.meta`, "fileFormatVersion: 2\nguid: 33333333333333333333333333333333\n");
|
|
586
|
+
return syntheticEditorSuccess();
|
|
587
|
+
});
|
|
588
|
+
assert.equal(result.status, "EXECUTED");
|
|
589
|
+
assert.equal(json(path.join(f.project, result.evidence.path)).origin, "unity-repair-operation");
|
|
590
|
+
f.write(request.handoffPaths[0], "NCCGS-HANDOFF: COMPLETE\nSynthetic edit/import completed; verification remains separate.");
|
|
591
|
+
const observed = observe(f.project, f.result(run), "agent_result");
|
|
592
|
+
assert.equal(f.state().reservations.at(-1).status, "completed");
|
|
593
|
+
assert.throws(() => recordStandalone(f.project, f.task(), "implementation", { ...f.stage(run, observed),
|
|
594
|
+
checks: [{ name: "synthetic-check", status: "PASS", evidence: result.evidence }] }), /operation receipts are not verification/);
|
|
595
|
+
recordStandalone(f.project, f.task(), "implementation", f.stage(run, observed));
|
|
596
|
+
assert.deepEqual(f.task().contract, original.contract);
|
|
597
|
+
assert.equal(f.task().reviewBudget.used, original.reviewBudget.used);
|
|
598
|
+
assert.ok(f.task().history.some(h => h.stages?.review?.verdict === "CHANGES_REQUIRED"));
|
|
599
|
+
assert.deepEqual(fs.readFileSync(path.join(f.project, f.analysis)), feedback);
|
|
600
|
+
assert.throws(() => recordStandalone(f.project, f.task(), "report", { outcome: "success", summary: "Cannot skip fresh review" }), /review.*missing|stale/i);
|
|
601
|
+
const review = f.dispatch({ kind: "review", agent: "nccgs-adversarial-reviewer", writePaths: [] }); f.start(review);
|
|
602
|
+
const reviewed = observe(f.project, f.result(review), "agent_result");
|
|
603
|
+
const evidence = f.write(".nccgs/evidence/analysis/synthetic-review.md", "SYNTHETIC independent review, not actual provider evidence");
|
|
604
|
+
recordStandalone(f.project, f.task(), "review", { snapshot: reviewed.snapshot, dispatchId: review.reservation.id,
|
|
605
|
+
runtime: artifact(f.project, reviewed.path), verdict: "PASS", criteriaMet: true, findings: [], evidence });
|
|
606
|
+
recordStandalone(f.project, f.task(), "report", { outcome: "success", summary: "Synthetic technical pass; manual decision pending" });
|
|
607
|
+
assert.throws(() => taskRun(["close", "--project", f.project, "--id", "synthetic"]), /acceptance: missing/);
|
|
608
|
+
assert.equal(f.state().usage.dispatches, 4); assert.equal(f.task().reviewBudget.used, original.reviewBudget.used + 1);
|
|
609
|
+
} finally { f.dispose(); }
|
|
610
|
+
});
|
|
611
|
+
|
|
612
|
+
test("Unity repair Editor operations are pinned, ordered, exclusive and never replayed", () => {
|
|
613
|
+
const f = fixture();
|
|
614
|
+
try {
|
|
615
|
+
const { packet, request, target } = prepareUnityRepair(f, "unity-editor", false, 2);
|
|
616
|
+
packet.changes.push({ path: "Assets/fixture.cs", operation: "edit", instruction: "SYNTHETIC paired code correction for the prefab" });
|
|
617
|
+
request.writePaths.push("Assets/fixture.cs"); f.write(request.workPacket, packet);
|
|
618
|
+
const run = f.dispatch(request); f.start(run);
|
|
619
|
+
guardTool(f.project, repairCall(f, run, "paired-code", "Edit", { file_path: "Assets/fixture.cs" }));
|
|
620
|
+
f.write("Assets/fixture.cs", "// SYNTHETIC code correction paired with the prefab\n");
|
|
621
|
+
assert.throws(() => f.dispatch({ writePaths: ["Assets/elsewhere.cs"] }), /owns the project/);
|
|
622
|
+
for (const input of [
|
|
623
|
+
repairCall(f, run, "raw", "Write", { file_path: target }),
|
|
624
|
+
repairCall(f, run, "meta", "Edit", { file_path: `${target}.meta` }),
|
|
625
|
+
repairCall(f, run, "shell", "Bash", { command: "unity command eval evil", timeout: 6000 }),
|
|
626
|
+
repairCall(f, run, "mcp", "mcp__unityMCP__execute_code", { code: "unbounded" }),
|
|
627
|
+
{ ...editorCall(f, run, packet.unity.steps[0], "background"), tool_input: { ...editorCall(f, run, packet.unity.steps[0]).tool_input, run_in_background: true } },
|
|
628
|
+
{ ...editorCall(f, run, packet.unity.steps[0], "chain"), tool_input: { command: repairEditorCommand(run.reservation, packet.unity.steps[0]) + "; echo bad", timeout: 6000 } },
|
|
629
|
+
{ ...editorCall(f, run, packet.unity.steps[0], "short"), tool_input: { command: repairEditorCommand(run.reservation, packet.unity.steps[0]), timeout: 1000 } }
|
|
630
|
+
]) assert.throws(() => guardTool(f.project, input));
|
|
631
|
+
assert.throws(() => runRepairEditor(f.project, "synthetic", run.reservation.id, "step-2", syntheticEditorSuccess), /packet order/);
|
|
632
|
+
guardTool(f.project, editorCall(f, run, packet.unity.steps[0]));
|
|
633
|
+
let executions = 0;
|
|
634
|
+
const executor = () => { executions++; f.write(target, "%YAML SYNTHETIC changed through fake Editor\n"); return syntheticEditorSuccess(); };
|
|
635
|
+
const receipt = runRepairEditor(f.project, "synthetic", run.reservation.id, "step-1", executor);
|
|
636
|
+
assert.throws(() => runRepairEditor(f.project, "synthetic", run.reservation.id, "step-1", executor), /already attempted/);
|
|
637
|
+
assert.equal(executions, 1);
|
|
638
|
+
for (const file_path of [receipt.evidence.path, run.reservation.workPacket.scope.path, packet.unity.capabilities.path])
|
|
639
|
+
assert.throws(() => guardTool(f.project, { session_id: "synthetic-session", tool_name: "Write", tool_input: { file_path } }), /immutable/);
|
|
640
|
+
guardTool(f.project, editorCall(f, run, packet.unity.steps[1]));
|
|
641
|
+
runRepairEditor(f.project, "synthetic", run.reservation.id, "step-2", syntheticEditorSuccess);
|
|
642
|
+
f.write(request.handoffPaths[0], "NCCGS-HANDOFF: COMPLETE\nSYNTHETIC saved asset");
|
|
643
|
+
const observed = observe(f.project, f.result(run), "agent_result");
|
|
644
|
+
assert.equal(f.state().reservations.at(-1).status, "completed");
|
|
645
|
+
f.write(receipt.evidence.path, "SYNTHETIC tampering");
|
|
646
|
+
assert.throws(() => recordStandalone(f.project, f.task(), "implementation", f.stage(run, observed)), /stale artifact/);
|
|
647
|
+
} finally { f.dispose(); }
|
|
648
|
+
});
|
|
649
|
+
|
|
650
|
+
test("Unity repair rejects malformed profiles and scope before consuming another dispatch", () => {
|
|
651
|
+
const f = fixture();
|
|
652
|
+
try {
|
|
653
|
+
const { packet, request } = prepareUnityRepair(f);
|
|
654
|
+
const invalid = [
|
|
655
|
+
{ ...packet, repairType: "unrestricted" }, { ...packet, budget: { workCalls: 25 } },
|
|
656
|
+
{ ...packet, changes: [{ ...packet.changes[0], operation: "delete" }] },
|
|
657
|
+
{ ...packet, changes: [{ ...packet.changes[0], operation: "create" }] },
|
|
658
|
+
{ ...packet, changes: [{ ...packet.changes[0], path: "Assets/fixture.cs.meta" }] },
|
|
659
|
+
{ ...packet, changes: [{ ...packet.changes[0], path: "Packages/manifest.json" }] },
|
|
660
|
+
{ ...packet, changes: [{ ...packet.changes[0], path: "Assets/missing/new.cs", operation: "create" }] },
|
|
661
|
+
{ ...packet, unity: { ...packet.unity, capabilities: { ...packet.unity.capabilities, sha256: "changed" } } },
|
|
662
|
+
...[{ arguments: ["--project-path", "/other"] }, { command: "eval;evil" }, { command: "scene_open" }, { arguments: ["one", "two"] },
|
|
663
|
+
{ arguments: ['using UnityEngine;\nreturn "bad";'] }, { timeoutMs: 60001 }, { id: "../x" }, { unknown: true }]
|
|
664
|
+
.map(override => ({ ...packet, unity: { ...packet.unity, steps: [{ ...packet.unity.steps[0], ...override }] } }))
|
|
665
|
+
];
|
|
666
|
+
const before = fs.readFileSync(path.join(f.project, ".nccgs/orchestration.json"));
|
|
667
|
+
for (const bad of invalid) {
|
|
668
|
+
f.write(request.workPacket, bad); assert.throws(() => f.dispatch(request));
|
|
669
|
+
assert.deepEqual(fs.readFileSync(path.join(f.project, ".nccgs/orchestration.json")), before);
|
|
670
|
+
}
|
|
671
|
+
f.write(request.workPacket, packet);
|
|
672
|
+
const run = f.dispatch(request);
|
|
673
|
+
const same = reserve(f.project, "synthetic", run.request);
|
|
674
|
+
assert.equal(same.id, run.reservation.id); assert.equal(f.state().usage.dispatches, 3);
|
|
675
|
+
} finally { f.dispose(); }
|
|
676
|
+
});
|
|
677
|
+
|
|
678
|
+
test("Unity repair refuses out-of-scope side effects, changed GUIDs and unfinished import", () => {
|
|
679
|
+
for (const variant of ["outside", "deleted", "guid", "missing-meta", "missing-step"]) {
|
|
680
|
+
const f = fixture();
|
|
681
|
+
try {
|
|
682
|
+
const { packet, request, target } = prepareUnityRepair(f, "unity-code", variant === "missing-meta"), run = f.dispatch(request); f.start(run);
|
|
683
|
+
if (variant === "missing-step") {
|
|
684
|
+
f.write(request.handoffPaths[0], "NCCGS-HANDOFF: COMPLETE\nSYNTHETIC false completion");
|
|
685
|
+
} else {
|
|
686
|
+
guardTool(f.project, editorCall(f, run, packet.unity.steps[0]));
|
|
687
|
+
const result = runRepairEditor(f.project, "synthetic", run.reservation.id, "step-1", () => {
|
|
688
|
+
if (variant === "outside") f.write("Assets/Unowned.cs", "// synthetic unexpected import side effect");
|
|
689
|
+
if (variant === "deleted") fs.unlinkSync(path.join(f.project, "Assets/fixture.cs"));
|
|
690
|
+
if (variant === "guid") f.write("Assets/fixture.cs.meta", "guid: aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\n");
|
|
691
|
+
if (variant === "missing-meta") f.write(target, "// synthetic new file without imported metadata");
|
|
692
|
+
return syntheticEditorSuccess();
|
|
693
|
+
});
|
|
694
|
+
assert.equal(result.operationStatus, variant === "missing-meta" ? "completed" : "failed");
|
|
695
|
+
f.write(request.handoffPaths[0], "NCCGS-HANDOFF: COMPLETE\nSYNTHETIC false completion");
|
|
696
|
+
}
|
|
697
|
+
const observed = observe(f.project, f.result(run), "agent_result");
|
|
698
|
+
assert.equal(f.state().reservations.at(-1).status, "failed", variant);
|
|
699
|
+
assert.throws(() => recordStandalone(f.project, f.task(), "implementation", f.stage(run, observed)), /completed managed dispatch/);
|
|
700
|
+
assert.equal(f.state().usage.dispatches, 3); assert.equal(f.task().stages.review.verdict, "CHANGES_REQUIRED");
|
|
701
|
+
} finally { f.dispose(); }
|
|
702
|
+
}
|
|
703
|
+
});
|
|
704
|
+
|
|
705
|
+
test("Unity repair interruption retains ownership, partial evidence and spent attempts", () => {
|
|
706
|
+
for (const variant of ["timeout", "signal", "nonzero", "missing-cli"]) {
|
|
707
|
+
const f = fixture();
|
|
708
|
+
try {
|
|
709
|
+
const { packet, request } = prepareUnityRepair(f), run = f.dispatch(request); f.start(run);
|
|
710
|
+
guardTool(f.project, editorCall(f, run, packet.unity.steps[0]));
|
|
711
|
+
const error = Object.assign(new Error("SYNTHETIC interrupted editor operation"), { code: variant === "missing-cli" ? "ENOENT" : "ETIMEDOUT" });
|
|
712
|
+
const result = runRepairEditor(f.project, "synthetic", run.reservation.id, "step-1", () => variant === "signal" ? { status: null, signal: "SIGTERM" }
|
|
713
|
+
: variant === "nonzero" ? { status: 6 } : { status: null, error });
|
|
714
|
+
assert.equal(result.status, "FAIL");
|
|
715
|
+
assert.throws(() => guardTool(f.project, repairCall(f, run, "read-after-failure")), /running, correlated|failed or remains uncertain/);
|
|
716
|
+
const partial = "NCCGS-HANDOFF: PARTIAL\nSYNTHETIC interrupted Editor operation; retain original attempt";
|
|
717
|
+
guardTool(f.project, repairCall(f, run, "partial", "Write", { file_path: request.handoffPaths[0], content: partial }));
|
|
718
|
+
f.write(request.handoffPaths[0], partial);
|
|
719
|
+
if (variant !== "missing-cli") {
|
|
720
|
+
assert.equal(f.state().reservations.at(-1).status, "uncertain");
|
|
721
|
+
const observed = observe(f.project, f.result(run), "agent_result");
|
|
722
|
+
const retained = f.state().reservations.at(-1);
|
|
723
|
+
assert.equal(retained.status, "uncertain"); assert.equal(retained.workStatus, "incomplete-editor");
|
|
724
|
+
assert.equal(retained.telemetry.totalTokens, 456); assert.equal(retained.telemetry.durationMs, 123);
|
|
725
|
+
assert.equal(retained.runtime.path, observed.path);
|
|
726
|
+
assert.equal(observe(f.project, f.result(run), "agent_result").duplicate, true);
|
|
727
|
+
assert.throws(() => recordStandalone(f.project, f.task(), "implementation", f.stage(run, observed)), /handoff is partial|completed managed dispatch/);
|
|
728
|
+
assert.throws(() => f.dispatch(), /owns the project/);
|
|
729
|
+
reconcile(f.project, run.reservation.id, { status: "failed", reason: "SYNTHETIC external operator confirmed the fake operation stopped", reference: "SYNTHETIC TEST ONLY" });
|
|
730
|
+
} else observe(f.project, f.result(run), "agent_result");
|
|
731
|
+
assert.equal(f.state().usage.dispatches, 3); assert.equal(f.state().active.length, 0);
|
|
732
|
+
assert.equal(fs.readFileSync(path.join(f.project, request.handoffPaths[0]), "utf8"), partial);
|
|
733
|
+
assert.ok(f.state().reservations.at(-1).editorSteps[0].evidence);
|
|
734
|
+
assert.equal(f.state().reservations.at(-1).telemetry.totalTokens, 456);
|
|
735
|
+
} finally { f.dispose(); }
|
|
736
|
+
}
|
|
737
|
+
});
|
|
738
|
+
|
|
739
|
+
test("Unity repair CLI rejects caller-supplied commands, duplicate flags and forwarded payloads", () => {
|
|
740
|
+
const f = fixture();
|
|
741
|
+
try {
|
|
742
|
+
const base = ["repair-editor", "--project", f.project, "--id", "synthetic", "--dispatch", "synthetic", "--step", "one"];
|
|
743
|
+
for (const extra of [["--command", "evil"], ["--step", "two"], ["--", "unity"], ["--timeout", "999999"]])
|
|
744
|
+
assert.throws(() => taskRun([...base, ...extra]), /unique.*no forwarded/);
|
|
745
|
+
} finally { f.dispose(); }
|
|
746
|
+
});
|
|
747
|
+
|
|
748
|
+
test("Unity repair executable hook and CLI retain a missing-tool failure without launching Unity", () => {
|
|
749
|
+
const f = fixture();
|
|
750
|
+
try {
|
|
751
|
+
const { packet, request } = prepareUnityRepair(f), run = f.dispatch(request); f.start(run);
|
|
752
|
+
const admitted = f.hook("closure-guard.mjs", "pre", editorCall(f, run, packet.unity.steps[0]));
|
|
753
|
+
assert.equal(admitted.status, 0, admitted.stderr);
|
|
754
|
+
const env = Object.fromEntries(Object.entries(process.env).filter(([key]) => key.toLowerCase() !== "path"));
|
|
755
|
+
env.PATH = f.project; // No unity executable in this disposable synthetic fixture.
|
|
756
|
+
const child = spawnSync(process.execPath, [path.join(root, ".claude/nccgs/tools/task.mjs"), "repair-editor", "--project", f.project,
|
|
757
|
+
"--id", "synthetic", "--dispatch", run.reservation.id, "--step", "step-1"], { cwd: f.project, env, windowsHide: true, encoding: "utf8" });
|
|
758
|
+
assert.equal(child.status, 1, child.stderr);
|
|
759
|
+
const response = JSON.parse(child.stdout);
|
|
760
|
+
assert.equal(response.operationStatus, "failed");
|
|
761
|
+
assert.match(json(path.join(f.project, response.evidence.path)).error, /ENOENT/);
|
|
762
|
+
assert.equal(f.state().reservations.at(-1).editorSteps.length, 1);
|
|
763
|
+
assert.equal(f.state().usage.dispatches, 3);
|
|
764
|
+
} finally { f.dispose(); }
|
|
765
|
+
});
|
|
766
|
+
|
|
767
|
+
test("Unity repair code-only profile does not require Editor availability for existing files", () => {
|
|
768
|
+
const f = fixture();
|
|
769
|
+
try {
|
|
770
|
+
const { packet, request, target } = prepareUnityRepair(f, "unity-code", false, 0);
|
|
771
|
+
delete packet.unity.capabilities;
|
|
772
|
+
f.write(request.workPacket, packet);
|
|
773
|
+
const run = f.dispatch(request); f.start(run);
|
|
774
|
+
const prompt = dispatchPrompt(f.project, "synthetic", run.reservation.id).prompt;
|
|
775
|
+
assert.match(prompt, /No Editor steps are reserved/); assert.doesNotMatch(prompt, /undefined/);
|
|
776
|
+
guardTool(f.project, repairCall(f, run, "code-only", "Edit", { file_path: target }));
|
|
777
|
+
f.write(target, "// SYNTHETIC compile-error correction without an available Editor\n");
|
|
778
|
+
f.write(request.handoffPaths[0], "NCCGS-HANDOFF: COMPLETE\nSYNTHETIC code fix; coordinator checks pending");
|
|
779
|
+
const observed = observe(f.project, f.result(run), "agent_result");
|
|
780
|
+
assert.equal(f.state().reservations.at(-1).status, "completed");
|
|
781
|
+
assert.equal(f.state().reservations.at(-1).editorSteps, undefined);
|
|
782
|
+
recordStandalone(f.project, f.task(), "implementation", f.stage(run, observed));
|
|
783
|
+
assert.ok(f.task().history.some(h => h.stages?.review?.verdict === "CHANGES_REQUIRED"));
|
|
784
|
+
assert.equal(f.task().reviewBudget.used, 1);
|
|
785
|
+
} finally { f.dispose(); }
|
|
786
|
+
});
|
|
787
|
+
|
|
788
|
+
test("repair refuses missing, stale, broad and malformed packets before spending quota", () => {
|
|
789
|
+
const f = fixture();
|
|
790
|
+
try {
|
|
791
|
+
const { packet, request } = prepareRepair(f);
|
|
792
|
+
assert.throws(() => f.dispatch({ purpose: "repair" }), /workPacket/);
|
|
793
|
+
const invalid = [
|
|
794
|
+
{ ...packet, taskId: "wrong" }, { ...packet, contractHash: "stale" }, { ...packet, sourceSnapshot: "stale" },
|
|
795
|
+
{ ...packet, solution: "" }, { ...packet, commands: ["node sweep.mjs"] }, { ...packet, changes: [] },
|
|
796
|
+
{ ...packet, changes: [{ path: "Assets", instruction: "Broad directory" }] },
|
|
797
|
+
{ ...packet, changes: [{ path: "../outside.cs", instruction: "External mutation" }] },
|
|
798
|
+
{ ...packet, changes: [packet.changes[0], packet.changes[0]] },
|
|
799
|
+
{ ...packet, context: [{ ...packet.context[0], sha256: "stale" }] },
|
|
800
|
+
{ ...packet, verification: ["undeclared"] }, { ...packet, budget: { workCalls: 13 } },
|
|
801
|
+
];
|
|
802
|
+
for (const bad of invalid) {
|
|
803
|
+
f.write(request.workPacket, bad);
|
|
804
|
+
assert.throws(() => f.dispatch(request));
|
|
805
|
+
assert.equal(f.state().usage.dispatches, 2);
|
|
806
|
+
}
|
|
807
|
+
f.write(request.workPacket, packet);
|
|
808
|
+
assert.throws(() => f.dispatch({ ...request, writePaths: ["Assets"] }), /exactly match/);
|
|
809
|
+
assert.throws(() => f.dispatch({ ...request, handoffPaths: [] }), /one fresh handoff/);
|
|
810
|
+
assert.equal(f.state().usage.dispatches, 2);
|
|
811
|
+
assert.ok(f.dispatch(request).reservation.workPacket.reference.sha256);
|
|
812
|
+
} finally { f.dispose(); }
|
|
813
|
+
});
|
|
814
|
+
|
|
815
|
+
test("repair prompt is pinned, injected into the Agent input and exposes coordinator checks", () => {
|
|
816
|
+
const f = fixture();
|
|
817
|
+
try {
|
|
818
|
+
const { request } = prepareRepair(f), run = f.dispatch(request);
|
|
819
|
+
const prompt = taskRun(["dispatch-prompt", "--project", f.project, "--id", "synthetic", "--dispatch", run.reservation.id]).prompt;
|
|
820
|
+
assert.equal(prompt, dispatchPrompt(f.project, "synthetic", run.reservation.id).prompt);
|
|
821
|
+
assert.match(prompt, /Chosen solution: Replace/); assert.match(prompt, /Coordinator runs required checks after return: synthetic-check/);
|
|
822
|
+
assert.ok(prompt.length < 4500);
|
|
823
|
+
const result = f.hook("closure-guard.mjs", "pre", run.envelope);
|
|
824
|
+
assert.equal(result.status, 0, result.stderr);
|
|
825
|
+
const output = JSON.parse(result.stdout).hookSpecificOutput;
|
|
826
|
+
assert.equal(output.hookEventName, "PreToolUse");
|
|
827
|
+
assert.equal(output.updatedInput.prompt, prompt); assert.equal(output.updatedInput.subagent_type, run.request.agent);
|
|
828
|
+
assert.equal(output.permissionDecision, undefined); // Do not grant native permissions.
|
|
829
|
+
for (const relative of [request.workPacket, f.analysis]) assert.throws(() => guardTool(f.project, {
|
|
830
|
+
session_id: "synthetic-session", tool_name: "Write", tool_input: { file_path: relative, content: "tamper" } }), /immutable/);
|
|
831
|
+
} finally { f.dispose(); }
|
|
832
|
+
});
|
|
833
|
+
|
|
834
|
+
test("repair counts admitted and denied calls durably, deduplicates retries and reserves handoff", () => {
|
|
835
|
+
const f = fixture();
|
|
836
|
+
try {
|
|
837
|
+
const { request } = prepareRepair(f, { workCalls: 2, readCalls: 1, handoffCalls: 2 }), run = f.dispatch(request); f.start(run);
|
|
838
|
+
const read = repairCall(f, run, "read");
|
|
839
|
+
assert.match(guardTool(f.project, read).additionalContext, /1 work, 0 reads/);
|
|
840
|
+
guardTool(f.project, read);
|
|
841
|
+
assert.equal(f.state().reservations.at(-1).toolBudget.work, 1);
|
|
842
|
+
assert.throws(() => guardTool(f.project, { ...read, tool_input: { file_path: f.analysis } }), /reused with different/);
|
|
843
|
+
const sweep = repairCall(f, run, "sweep", "Bash", { command: "mkdir C:/Temp/vsharness && node sweep.mjs", SECRET: "NEVER_STORE_THIS" });
|
|
844
|
+
assert.throws(() => guardTool(f.project, sweep), /only project reads/);
|
|
845
|
+
assert.throws(() => guardTool(f.project, sweep), /only project reads/);
|
|
846
|
+
assert.equal(f.state().reservations.at(-1).toolBudget.work, 2);
|
|
847
|
+
assert.throws(() => guardTool(f.project, repairCall(f, run, "extra-edit", "Edit", { file_path: "Assets/fixture.cs" })), /work allowance exhausted/);
|
|
848
|
+
const content = "NCCGS-HANDOFF: PARTIAL\nChosen correction not applied; work allowance exhausted.";
|
|
849
|
+
guardTool(f.project, repairCall(f, run, "handoff", "Write", { file_path: request.handoffPaths[0], content }));
|
|
850
|
+
f.write(request.handoffPaths[0], content);
|
|
851
|
+
const before = f.state();
|
|
852
|
+
assert.equal(before.active.length, 1); assert.equal(before.usage.dispatches, 3);
|
|
853
|
+
assert.doesNotMatch(JSON.stringify(before.reservations.at(-1).toolBudget), /NEVER_STORE_THIS|mkdir|sweep\.mjs/);
|
|
854
|
+
observe(f.project, f.result(run), "agent_result");
|
|
855
|
+
assert.equal(f.state().reservations.at(-1).status, "failed");
|
|
856
|
+
assert.equal(f.state().active.length, 0); assert.equal(f.state().usage.dispatches, 3);
|
|
857
|
+
assert.throws(() => guardTool(f.project, repairCall(f, run, "late", "Write", { file_path: request.handoffPaths[0], content: "NCCGS-HANDOFF: COMPLETE" })), /running, correlated/);
|
|
858
|
+
} finally { f.dispose(); }
|
|
859
|
+
});
|
|
860
|
+
|
|
861
|
+
test("repair rejects out-of-scope tools and identity spoofing without executing them", () => {
|
|
862
|
+
const f = fixture();
|
|
863
|
+
try {
|
|
864
|
+
const { request } = prepareRepair(f), run = f.dispatch(request); f.start(run);
|
|
865
|
+
// Transport-level refusal happens before repair admission: assert the same
|
|
866
|
+
// rejected Unity call while requiring every reservation/cost field unchanged.
|
|
867
|
+
const beforeTransport = f.state();
|
|
868
|
+
assert.throws(() => guardTool(f.project, repairCall(f, run, "transport-denied", "mcp__unity__execute", { code: "mutation" })), /Unity CLI-only/);
|
|
869
|
+
assert.deepEqual(f.state(), beforeTransport);
|
|
870
|
+
const cases = [
|
|
871
|
+
["Write", { file_path: "Assets/other.cs" }], ["Write", { file_path: ".nccgs/evidence/analysis/undeclared.md" }],
|
|
872
|
+
["Read", { file_path: path.join(path.dirname(f.project), "outside.cs") }],
|
|
873
|
+
["Glob", { pattern: "../*.cs" }], ["Grep", { pattern: "source", path: ".." }],
|
|
874
|
+
["Glob", { pattern: "{../*,Assets/**}" }],
|
|
875
|
+
["PowerShell", { command: "Write-Output test" }],
|
|
876
|
+
["WebFetch", { url: "https://example.com" }], ["Agent", { prompt: "nested" }],
|
|
877
|
+
["NotebookEdit", { notebook_path: "Assets/fixture.cs" }],
|
|
878
|
+
];
|
|
879
|
+
for (const [index, [name, tool]] of cases.entries()) assert.throws(() => guardTool(f.project, repairCall(f, run, `denied-${index}`, name, tool)));
|
|
880
|
+
const read = repairCall(f, run, "identity");
|
|
881
|
+
assert.throws(() => guardTool(f.project, { ...read, session_id: "other-session" }), /correlated/);
|
|
882
|
+
assert.throws(() => guardTool(f.project, { ...read, agent_id: "other-agent" }), /correlated/);
|
|
883
|
+
assert.throws(() => guardTool(f.project, { ...read, tool_use_id: undefined }), /identity missing/);
|
|
884
|
+
const marker = f.state().reservations[0].id;
|
|
885
|
+
assert.throws(() => guardTool(f.project, { ...read, agent_id: "other-agent", tool_input: { ...read.tool_input, prompt: `[NCCGS-DISPATCH:${marker}]` } }), /correlated/);
|
|
886
|
+
assert.equal(fs.readFileSync(path.join(f.project, "Assets/fixture.cs"), "utf8"), "// SYNTHETIC TEST SOURCE; no game implementation.\n");
|
|
887
|
+
assert.equal(f.state().reservations.at(-1).toolBudget.work, cases.length);
|
|
888
|
+
} finally { f.dispose(); }
|
|
889
|
+
});
|
|
890
|
+
|
|
891
|
+
test("repair read allowance preserves edit calls and handoff has its own limit", () => {
|
|
892
|
+
const f = fixture();
|
|
893
|
+
try {
|
|
894
|
+
const { request } = prepareRepair(f, { readCalls: 1, handoffCalls: 1 }), run = f.dispatch(request); f.start(run);
|
|
895
|
+
guardTool(f.project, repairCall(f, run, "read"));
|
|
896
|
+
assert.throws(() => guardTool(f.project, repairCall(f, run, "read-again")), /read allowance exhausted/);
|
|
897
|
+
assert.doesNotThrow(() => guardTool(f.project, repairCall(f, run, "edit", "Edit", { file_path: "Assets/fixture.cs", old_string: "fixture", new_string: "fixed" })));
|
|
898
|
+
guardTool(f.project, repairCall(f, run, "handoff", "Write", { file_path: request.handoffPaths[0] }));
|
|
899
|
+
assert.throws(() => guardTool(f.project, repairCall(f, run, "handoff-extra", "Write", { file_path: request.handoffPaths[0] })), /handoff allowance exhausted/);
|
|
900
|
+
} finally { f.dispose(); }
|
|
901
|
+
});
|
|
902
|
+
|
|
903
|
+
test("repair deadline and context drift stop work while permitting an honest partial handoff", async () => {
|
|
904
|
+
for (const drift of ["deadline", "packet", "context", "contract"]) {
|
|
905
|
+
const f = fixture();
|
|
906
|
+
try {
|
|
907
|
+
const { request } = prepareRepair(f, drift === "deadline" ? { durationMs: 1 } : undefined), run = f.dispatch(request); f.start(run);
|
|
908
|
+
if (drift === "deadline") await new Promise(resolve => setTimeout(resolve, 5));
|
|
909
|
+
if (drift === "packet") f.write(request.workPacket, "tampered");
|
|
910
|
+
if (drift === "context") f.write(f.analysis, "tampered context");
|
|
911
|
+
if (drift === "contract") f.write(".nccgs/tasks/synthetic.json", { ...f.task(), contractHash: "changed" });
|
|
912
|
+
assert.throws(() => guardTool(f.project, repairCall(f, run, "after-drift")), /deadline|changed|hash|stale/);
|
|
913
|
+
assert.doesNotThrow(() => guardTool(f.project, repairCall(f, run, "handoff", "Write", { file_path: request.handoffPaths[0], content: "NCCGS-HANDOFF: PARTIAL" })));
|
|
914
|
+
assert.equal(f.state().active.length, 1);
|
|
915
|
+
} finally { f.dispose(); }
|
|
916
|
+
}
|
|
917
|
+
});
|
|
918
|
+
|
|
919
|
+
test("tampered repair packet before launch fails the unclaimed attempt without refund", () => {
|
|
920
|
+
const f = fixture();
|
|
921
|
+
try {
|
|
922
|
+
const { request } = prepareRepair(f), run = f.dispatch(request);
|
|
923
|
+
f.write(request.workPacket, "tampered");
|
|
924
|
+
assert.throws(() => guardTool(f.project, run.envelope), /changed|hash|stale/);
|
|
925
|
+
assert.equal(f.state().active.length, 0); assert.equal(f.state().usage.dispatches, 3);
|
|
926
|
+
assert.equal(f.state().reservations.at(-1).invocationStatus, "not-started");
|
|
927
|
+
} finally { f.dispose(); }
|
|
928
|
+
});
|
|
929
|
+
|
|
930
|
+
test("concurrent hook processes cannot both consume the last repair work call", async () => {
|
|
931
|
+
const f = fixture();
|
|
932
|
+
try {
|
|
933
|
+
const { request } = prepareRepair(f, { workCalls: 1, readCalls: 1 }), run = f.dispatch(request); f.start(run);
|
|
934
|
+
const invoke = input => new Promise((resolve, reject) => {
|
|
935
|
+
const child = spawn(process.execPath, [path.join(root, ".claude/nccgs/hooks/closure-guard.mjs"), "pre"], { cwd: f.project, windowsHide: true, stdio: ["pipe", "pipe", "pipe"] });
|
|
936
|
+
let stdout = "", stderr = "";
|
|
937
|
+
child.stdout.on("data", value => { stdout += value; }); child.stderr.on("data", value => { stderr += value; });
|
|
938
|
+
child.on("error", reject); child.on("close", code => resolve({ code, stdout, stderr }));
|
|
939
|
+
child.stdin.end(JSON.stringify(input));
|
|
940
|
+
});
|
|
941
|
+
const results = await Promise.all([invoke(repairCall(f, run, "parallel-1")), invoke(repairCall(f, run, "parallel-2"))]);
|
|
942
|
+
assert.deepEqual(results.map(r => r.code).sort(), [0, 2], JSON.stringify(results));
|
|
943
|
+
const denied = results.findIndex(r => r.code === 2);
|
|
944
|
+
// The existing process lock deliberately fails closed instead of waiting.
|
|
945
|
+
// Retry the same ID after both owners exit, without refunding the winner.
|
|
946
|
+
if (/Locked:/.test(results[denied].stderr)) {
|
|
947
|
+
const retry = await invoke(repairCall(f, run, `parallel-${denied + 1}`));
|
|
948
|
+
assert.equal(retry.code, 2); assert.match(retry.stderr, /work allowance exhausted/);
|
|
949
|
+
} else assert.match(results[denied].stderr, /work allowance exhausted/);
|
|
950
|
+
assert.equal(f.state().reservations.at(-1).toolBudget.work, 2);
|
|
951
|
+
assert.equal(f.state().reservations.at(-1).toolBudget.admissions.filter(a => a.allowed).length, 1);
|
|
952
|
+
const handoff = f.hook("closure-guard.mjs", "pre", repairCall(f, run, "handoff", "Write", { file_path: request.handoffPaths[0] }));
|
|
953
|
+
assert.equal(handoff.status, 0, handoff.stderr);
|
|
954
|
+
assert.match(JSON.parse(handoff.stdout).hookSpecificOutput.additionalContext, /0 work/);
|
|
955
|
+
} finally { f.dispose(); }
|
|
956
|
+
});
|
|
957
|
+
|
|
958
|
+
test("native hook permits task record reads but still refuses direct governance writes", () => {
|
|
959
|
+
const f = fixture();
|
|
960
|
+
try {
|
|
961
|
+
for (const file_path of [".nccgs/tasks/synthetic.json", path.join(f.project, ".nccgs/tasks/synthetic.json")]) {
|
|
962
|
+
const input = { hook_event_name: "PreToolUse", session_id: "synthetic-session", tool_input: { file_path } };
|
|
963
|
+
const read = f.hook("closure-guard.mjs", "pre", { ...input, tool_name: "Read" });
|
|
964
|
+
assert.equal(read.status, 0, read.stderr);
|
|
965
|
+
assert.equal(f.hook("closure-guard.mjs", "pre", { ...input, tool_name: "Write" }).status, 2);
|
|
966
|
+
}
|
|
967
|
+
} finally { f.dispose(); }
|
|
968
|
+
});
|
|
969
|
+
|
|
970
|
+
test("repair reservation retry preserves spent tool history and detects inconsistent counters", () => {
|
|
971
|
+
const f = fixture();
|
|
972
|
+
try {
|
|
973
|
+
const { request } = prepareRepair(f), run = f.dispatch(request); f.start(run);
|
|
974
|
+
guardTool(f.project, repairCall(f, run, "read"));
|
|
975
|
+
const retried = reserve(f.project, "synthetic", run.request);
|
|
976
|
+
assert.equal(retried.id, run.reservation.id); assert.equal(retried.toolBudget.work, 1);
|
|
977
|
+
const ledger = json(path.join(f.project, ".nccgs/orchestration.json"));
|
|
978
|
+
ledger.dispatches[run.reservation.id].toolBudget.work = 0;
|
|
979
|
+
f.write(".nccgs/orchestration.json", ledger); // Explicit corruption fixture.
|
|
980
|
+
assert.throws(() => guardTool(f.project, repairCall(f, run, "after-reset")), /differ from admission history/);
|
|
981
|
+
assert.equal(f.state().usage.dispatches, 3);
|
|
982
|
+
} finally { f.dispose(); }
|
|
983
|
+
});
|
|
984
|
+
|
|
985
|
+
test("context tampering cannot turn a COMPLETE repair handoff into completed work", () => {
|
|
986
|
+
const f = fixture();
|
|
987
|
+
try {
|
|
988
|
+
const { request } = prepareRepair(f), run = f.dispatch(request); f.start(run);
|
|
989
|
+
f.write(request.handoffPaths[0], "NCCGS-HANDOFF: COMPLETE\nSynthetic untrusted assertion.");
|
|
990
|
+
f.write(f.analysis, "Externally corrupted evidence");
|
|
991
|
+
const observed = observe(f.project, f.result(run), "agent_result");
|
|
992
|
+
const dispatch = f.state().reservations.at(-1);
|
|
993
|
+
assert.equal(dispatch.status, "failed"); assert.equal(dispatch.invocationStatus, "completed");
|
|
994
|
+
assert.match(dispatch.failure, /Repair context changed/);
|
|
995
|
+
assert.throws(() => recordStandalone(f.project, f.task(), "implementation", f.stage(run, observed)), /completed managed dispatch/);
|
|
996
|
+
assert.equal(f.state().active.length, 0);
|
|
997
|
+
} finally { f.dispose(); }
|
|
998
|
+
});
|
|
999
|
+
|
|
1000
|
+
test("native harness turn exhaustion overrides a COMPLETE file and completed invocation", () => {
|
|
1001
|
+
const f = fixture();
|
|
1002
|
+
try {
|
|
1003
|
+
const first = f.dispatch({ handoffPaths: [f.analysis] }); f.start(first);
|
|
1004
|
+
f.write(f.analysis, "NCCGS-HANDOFF: COMPLETE\nSynthetic premature completion.");
|
|
1005
|
+
const exhausted = f.result(first, { harnessNoteCount: 1, harnessTailCount: 0, content: [
|
|
1006
|
+
{ type: "text", text: "NOTE: this agent stopped at its 20-turn limit before finishing. The text below is PARTIAL output; treat it as incomplete." },
|
|
1007
|
+
{ type: "text", text: "SECRET_RESULT I am complete." }
|
|
1008
|
+
] });
|
|
1009
|
+
const observation = observe(f.project, exhausted, "agent_result");
|
|
1010
|
+
const entry = f.state().reservations[0];
|
|
1011
|
+
assert.equal(observation.status, "failed"); assert.equal(entry.status, "failed");
|
|
1012
|
+
assert.equal(entry.invocationStatus, "completed"); assert.equal(entry.workStatus, "incomplete");
|
|
1013
|
+
assert.equal(entry.nativeCompletion.reason, "turn-limit"); assert.equal(entry.nativeCompletion.turnLimit, 20);
|
|
1014
|
+
assert.equal(entry.handoff.partial.length, 0); assert.equal(f.state().active.length, 0);
|
|
1015
|
+
assert.equal(f.state().usage.dispatches, 1);
|
|
1016
|
+
assert.throws(() => recordStandalone(f.project, f.task(), "implementation", f.stage(first, observation)), /Runtime observation is incomplete/);
|
|
1017
|
+
const duplicate = observe(f.project, f.result(first), "agent_result");
|
|
1018
|
+
assert.equal(duplicate.duplicate, true); assert.equal(duplicate.status, "failed");
|
|
1019
|
+
assert.doesNotMatch(JSON.stringify(json(path.join(f.project, observation.observationSource.path))), /SECRET_RESULT|SECRET_PROMPT|SECRET_KEY/);
|
|
1020
|
+
assert.throws(() => f.dispatch({ kind: "review", agent: "nccgs-adversarial-reviewer", writePaths: [] }), /missing|stale/i);
|
|
1021
|
+
} finally { f.dispose(); }
|
|
1022
|
+
});
|
|
1023
|
+
|
|
1024
|
+
test("native completion ignores quoted model prose and fails closed on unsupported harness metadata", () => {
|
|
1025
|
+
for (const [metadata, failed] of [
|
|
1026
|
+
[{ content: [{ type: "text", text: "NOTE: this agent stopped at its 20-turn limit before finishing. Quoted example in model prose." }] }, false],
|
|
1027
|
+
[{ harnessNoteCount: 1, content: [{ type: "text", text: "An unknown provider advisory" }] }, true],
|
|
1028
|
+
[{ harnessNoteCount: 2, content: [{ type: "text", text: "Malformed metadata" }] }, true],
|
|
1029
|
+
[{ harnessNoteCount: -1 }, true],
|
|
1030
|
+
[{ harnessTailCount: 1, content: [{ type: "text", text: "NOTE: this agent stopped at its 12-turn limit before finishing." }] }, true]
|
|
1031
|
+
]) {
|
|
1032
|
+
const f = fixture(); try {
|
|
1033
|
+
const run = f.dispatch(); f.start(run);
|
|
1034
|
+
const observation = observe(f.project, f.result(run, metadata), "agent_result");
|
|
1035
|
+
assert.equal(observation.status, failed ? "failed" : "completed");
|
|
1036
|
+
assert.equal(f.state().active.length, 0);
|
|
1037
|
+
} finally { f.dispose(); }
|
|
1038
|
+
}
|
|
1039
|
+
});
|
|
1040
|
+
|
|
1041
|
+
function prepareStructuredReview(f, values = {}, body = null, beforeCompletion = null) {
|
|
1042
|
+
const author = f.dispatch({ agent: "nccgs-game-designer" }); f.start(author);
|
|
1043
|
+
f.write(f.analysis, "Synthetic analysis; no game or provider acceptance.");
|
|
1044
|
+
const authorRuntime = observe(f.project, f.result(author), "agent_result");
|
|
1045
|
+
recordStandalone(f.project, f.task(), "analysis", { ...f.stage(author, authorRuntime), evidence: artifact(f.project, f.analysis) });
|
|
1046
|
+
const reviewPath = ".nccgs/evidence/analysis/synthetic-review.md";
|
|
1047
|
+
const reviewer = f.dispatch({ kind: "review", agent: "nccgs-adversarial-reviewer", writePaths: [], handoffPaths: [reviewPath] });
|
|
1048
|
+
f.start(reviewer);
|
|
1049
|
+
const task = f.task();
|
|
1050
|
+
const result = { schemaVersion: 1, taskId: task.id, contractHash: task.contractHash, snapshot: snapshot(f.project, task).sha256,
|
|
1051
|
+
verdict: "PASS", criteriaMet: true, findings: [], conditions: [], ...values };
|
|
1052
|
+
f.write(reviewPath, body ?? `NCCGS-HANDOFF: COMPLETE\n\n\`\`\`nccgs-review\n${JSON.stringify(result)}\n\`\`\`\n`);
|
|
1053
|
+
beforeCompletion?.(reviewer, result, reviewPath);
|
|
1054
|
+
const observed = observe(f.project, f.result(reviewer), "agent_result");
|
|
1055
|
+
return { ...f.stage(reviewer, observed), evidence: artifact(f.project, reviewPath), verdict: result.verdict,
|
|
1056
|
+
criteriaMet: result.criteriaMet, findings: result.findings, conditions: result.conditions };
|
|
1057
|
+
}
|
|
1058
|
+
|
|
1059
|
+
test("review conditions are pinned to the reviewer and cannot be removed by coordinator", () => {
|
|
1060
|
+
const f = fixture("plan", true);
|
|
1061
|
+
try {
|
|
1062
|
+
const stage = prepareStructuredReview(f, { conditions: [{ id: "C1", description: "Baseline diff has not run" }] });
|
|
1063
|
+
assert.throws(() => recordStandalone(f.project, f.task(), "review", { ...stage, conditions: [] }), /conditions differs/);
|
|
1064
|
+
assert.throws(() => recordStandalone(f.project, f.task(), "review", stage), /unresolved review conditions block PASS/);
|
|
1065
|
+
assert.throws(() => recordStandalone(f.project, f.task(), "review", { ...stage, verdict: "BLOCKED", criteriaMet: false }), /verdict differs/);
|
|
1066
|
+
assert.throws(() => recordStandalone(f.project, f.task(), "report", { outcome: "success", summary: "False closure" }), /review: missing|stale/i);
|
|
1067
|
+
assert.equal(f.task().reviewBudget.used, 0); // Rejected record consumes no extra review round; worker attempt remains spent.
|
|
1068
|
+
assert.equal(f.state().usage.dispatches, 2);
|
|
1069
|
+
assert.equal(recordStandalone(f.project, f.task(), "report", { outcome: "partial", summary: "Awaiting baseline evidence and independent review" }).phase, "PARTIAL");
|
|
1070
|
+
} finally { f.dispose(); }
|
|
1071
|
+
});
|
|
1072
|
+
|
|
1073
|
+
test("dispatch preparation produces bounded PARTIAL drafts without changing task, ledger or files", () => {
|
|
1074
|
+
const f = fixture(); try {
|
|
1075
|
+
const handoffPath = ".nccgs/evidence/analysis/worker handoff.md";
|
|
1076
|
+
const worker = f.dispatch({ handoffPaths: [handoffPath] });
|
|
1077
|
+
const before = JSON.stringify({ task: f.task(), state: f.state() });
|
|
1078
|
+
const prepared = taskRun(["dispatch-prompt", "--project", f.project, "--id", "synthetic", "--dispatch", worker.reservation.id]);
|
|
1079
|
+
assert.ok(prepared.prompt.startsWith(`[NCCGS-DISPATCH:${worker.reservation.id}]`));
|
|
1080
|
+
assert.match(prepared.prompt, /Source write ownership: \["Assets\/fixture.cs"\]/);
|
|
1081
|
+
assert.match(prepared.prompt, /synthetic-check|pinned task contract/);
|
|
1082
|
+
assert.match(prepared.handoffs[0].content, /^NCCGS-HANDOFF: PARTIAL/);
|
|
1083
|
+
assert.equal(fs.existsSync(path.join(f.project, handoffPath)), false);
|
|
1084
|
+
assert.equal(JSON.stringify({ task: f.task(), state: f.state() }), before);
|
|
1085
|
+
f.write("Assets/fixture.cs", "// changed before launch");
|
|
1086
|
+
assert.throws(() => taskRun(["dispatch-prompt", "--project", f.project, "--id", "synthetic", "--dispatch", worker.reservation.id]), /snapshot stale/);
|
|
1087
|
+
} finally { f.dispose(); }
|
|
1088
|
+
});
|
|
1089
|
+
|
|
1090
|
+
test("reviewer validates and corrects its draft before completion; coordinator imports exact pinned result", () => {
|
|
1091
|
+
const f = fixture("plan", true); try {
|
|
1092
|
+
const stage = prepareStructuredReview(f, { findings: [{ severity: "INFO", status: "NOTED", description: "Synthetic disclosed limitation" }] }, null,
|
|
1093
|
+
(reviewer, value, file) => {
|
|
1094
|
+
const args = ["handoff-validate", "--project", f.project, "--id", "synthetic", "--dispatch", reviewer.reservation.id, "--file", file];
|
|
1095
|
+
const before = JSON.stringify({ task: f.task(), state: f.state() });
|
|
1096
|
+
assert.throws(() => taskRun(args), /OPEN, RESOLVED or ACCEPTED/);
|
|
1097
|
+
assert.equal(JSON.stringify({ task: f.task(), state: f.state() }), before);
|
|
1098
|
+
value.findings[0].status = "ACCEPTED";
|
|
1099
|
+
f.write(file, `NCCGS-HANDOFF: PARTIAL\n\`\`\`nccgs-review\n${JSON.stringify(value)}\n\`\`\`\n`);
|
|
1100
|
+
assert.equal(taskRun(args).complete, false);
|
|
1101
|
+
assert.equal(f.task().stages.review, undefined);
|
|
1102
|
+
f.write(file, `NCCGS-HANDOFF: COMPLETE\n\`\`\`nccgs-review\n${JSON.stringify(value)}\n\`\`\`\n`);
|
|
1103
|
+
assert.equal(taskRun(args).complete, true);
|
|
1104
|
+
const command = `node .claude/nccgs/tools/task.mjs handoff-validate --id synthetic --dispatch ${reviewer.reservation.id} --file ${file}`;
|
|
1105
|
+
const envelope = { session_id: "synthetic-session", agent_id: reviewer.agentId, tool_name: "Bash", tool_input: { command } };
|
|
1106
|
+
assert.doesNotThrow(() => guardTool(f.project, envelope));
|
|
1107
|
+
const hook = f.hook("closure-guard.mjs", "pre", { ...envelope, hook_event_name: "PreToolUse" });
|
|
1108
|
+
assert.equal(hook.status, 0, hook.stderr);
|
|
1109
|
+
for (const bad of [command + " --verdict PASS", command + " --file " + file, command + " -- node evil.mjs", command + "; echo unsafe",
|
|
1110
|
+
command.replace("--id synthetic", "--id other"), command.replace(file, f.analysis), command.replace("handoff-validate", "record-review"),
|
|
1111
|
+
command.replace(reviewer.reservation.id, "different-dispatch")]) {
|
|
1112
|
+
assert.throws(() => guardTool(f.project, { ...envelope, tool_input: { command: bad } }));
|
|
1113
|
+
}
|
|
1114
|
+
});
|
|
1115
|
+
const bytes = fs.readFileSync(path.join(f.project, stage.evidence.path));
|
|
1116
|
+
const oldTask = f.task(), oldLedger = JSON.stringify(f.state());
|
|
1117
|
+
const args = ["record-review", "--project", f.project, "--id", "synthetic", "--dispatch", stage.dispatchId, "--file", stage.evidence.path];
|
|
1118
|
+
const recorded = taskRun(args);
|
|
1119
|
+
for (const key of ["verdict", "criteriaMet", "findings", "conditions", "runtime", "evidence", "snapshot", "dispatchId"]) assert.deepEqual(recorded.stages.review[key], stage[key]);
|
|
1120
|
+
assert.equal(recorded.reviewBudget.used, oldTask.reviewBudget.used + 1);
|
|
1121
|
+
assert.equal(recorded.history.length, oldTask.history.length + 1);
|
|
1122
|
+
assert.deepEqual(recorded.history.slice(0, oldTask.history.length), oldTask.history);
|
|
1123
|
+
assert.equal(JSON.stringify(f.state()), oldLedger);
|
|
1124
|
+
assert.deepEqual(fs.readFileSync(path.join(f.project, stage.evidence.path)), bytes);
|
|
1125
|
+
assert.equal(recorded.closed, undefined);
|
|
1126
|
+
const saved = JSON.stringify(f.task());
|
|
1127
|
+
assert.throws(() => taskRun(args), /only once/);
|
|
1128
|
+
assert.equal(JSON.stringify(f.task()), saved);
|
|
1129
|
+
assert.throws(() => taskRun([...args, "--verdict", "PASS"]), /unique/);
|
|
1130
|
+
assert.throws(() => taskRun([...args, "--dispatch", stage.dispatchId]), /unique/);
|
|
1131
|
+
} finally { f.dispose(); }
|
|
1132
|
+
});
|
|
1133
|
+
|
|
1134
|
+
test("automatic review import preserves negative verdicts and mandatory manual acceptance", () => {
|
|
1135
|
+
for (const verdict of ["BLOCKED", "CHANGES_REQUIRED", "PASS"]) {
|
|
1136
|
+
const f = fixture("plan", true, { manualGates: ["OWNER-01"] }); try {
|
|
1137
|
+
const stage = prepareStructuredReview(f, { verdict, criteriaMet: verdict === "PASS",
|
|
1138
|
+
conditions: verdict === "PASS" ? [] : [{ id: "C1", description: "Required investigation remains" }] });
|
|
1139
|
+
const recorded = taskRun(["record-review", "--project", f.project, "--id", "synthetic", "--dispatch", stage.dispatchId, "--file", stage.evidence.path]);
|
|
1140
|
+
assert.equal(recorded.stages.review.verdict, verdict);
|
|
1141
|
+
assert.deepEqual(recorded.stages.review.conditions, stage.conditions);
|
|
1142
|
+
if (verdict === "PASS") {
|
|
1143
|
+
recordStandalone(f.project, f.task(), "report", { outcome: "success", summary: "Synthetic technical result; owner decision still pending" });
|
|
1144
|
+
assert.throws(() => taskRun(["close", "--project", f.project, "--id", "synthetic"]), /acceptance: missing/);
|
|
1145
|
+
} else assert.throws(() => recordStandalone(f.project, f.task(), "report", { outcome: "success", summary: "Cannot invent success" }), /review has not passed/);
|
|
1146
|
+
} finally { f.dispose(); }
|
|
1147
|
+
}
|
|
1148
|
+
});
|
|
1149
|
+
|
|
1150
|
+
test("automatic review import rejects tampering, conditional PASS, missing measurements and legacy policy without consuming a record", () => {
|
|
1151
|
+
for (const variant of ["tampered", "source", "conditional", "unmeasured", "legacy", "unattached", "incomplete", "malformed"]) {
|
|
1152
|
+
const f = fixture("plan", variant !== "legacy", variant === "unmeasured" ? { acceptance: [{ id: "AC", description: "Measured result", check: "synthetic-check",
|
|
1153
|
+
assertions: [{ artifact: ".nccgs/evidence/measurements/result.json", pointer: "/actual", op: "eq", value: true }] }] } : {});
|
|
1154
|
+
try {
|
|
1155
|
+
const stage = prepareStructuredReview(f, variant === "conditional" ? { conditions: [{ id: "C1", description: "Still needs evidence" }] } : {},
|
|
1156
|
+
variant === "incomplete" ? "NCCGS-HANDOFF: PARTIAL\nNot finished" : variant === "malformed" ? "NCCGS-HANDOFF: COMPLETE\n```nccgs-review\ninvalid\n```" : null);
|
|
1157
|
+
let file = stage.evidence.path;
|
|
1158
|
+
if (variant === "tampered") f.write(file, fs.readFileSync(path.join(f.project, file), "utf8") + "\nEdited after completion\n");
|
|
1159
|
+
if (variant === "source") f.write("Assets/fixture.cs", "// unrelated source change invalidates evidence");
|
|
1160
|
+
if (variant === "unattached") { file = ".nccgs/evidence/analysis/copied.md"; f.write(file, fs.readFileSync(path.join(f.project, stage.evidence.path), "utf8")); }
|
|
1161
|
+
const before = JSON.stringify({ task: f.task(), state: f.state() });
|
|
1162
|
+
assert.throws(() => taskRun(["record-review", "--project", f.project, "--id", "synthetic", "--dispatch", stage.dispatchId, "--file", file]));
|
|
1163
|
+
assert.equal(JSON.stringify({ task: f.task(), state: f.state() }), before);
|
|
1164
|
+
} finally { f.dispose(); }
|
|
1165
|
+
}
|
|
1166
|
+
});
|
|
1167
|
+
|
|
1168
|
+
test("complete negative review retains pending conditions; complete unconditional review may close", () => {
|
|
1169
|
+
for (const verdict of ["BLOCKED", "PASS"]) {
|
|
1170
|
+
const f = fixture("plan", true); try {
|
|
1171
|
+
const stage = prepareStructuredReview(f, { verdict, criteriaMet: verdict === "PASS",
|
|
1172
|
+
conditions: verdict === "PASS" ? [] : [{ id: "C1", description: "Required verification is missing" }] });
|
|
1173
|
+
recordStandalone(f.project, f.task(), "review", stage);
|
|
1174
|
+
if (verdict === "PASS") {
|
|
1175
|
+
recordStandalone(f.project, f.task(), "report", { outcome: "success", summary: "Synthetic complete review, no product claim" });
|
|
1176
|
+
assert.equal(taskRun(["close", "--project", f.project, "--id", "synthetic"]).phase, "DONE");
|
|
1177
|
+
f.write(stage.evidence.path, "NCCGS-HANDOFF: COMPLETE\nChanged after review");
|
|
1178
|
+
assert.throws(() => taskRun(["check", "--project", f.project, "--id", "synthetic", "--target", "done"]), /changed|stale|hash/i);
|
|
1179
|
+
} else {
|
|
1180
|
+
assert.throws(() => recordStandalone(f.project, f.task(), "report", { outcome: "success", summary: "False success" }), /review has not passed/i);
|
|
1181
|
+
assert.equal(recordStandalone(f.project, f.task(), "report", { outcome: "blocked", summary: "Missing verification" }).phase, "BLOCKED");
|
|
1182
|
+
}
|
|
1183
|
+
} finally { f.dispose(); }
|
|
1184
|
+
}
|
|
1185
|
+
});
|
|
1186
|
+
|
|
1187
|
+
test("missing malformed or unpinned review JSON cannot certify a fresh task", () => {
|
|
1188
|
+
for (const variant of ["missing", "invalid", "wrong-task", "unattached", "changed-findings"]) {
|
|
1189
|
+
const f = fixture("plan", true); try {
|
|
1190
|
+
const body = variant === "missing" ? "NCCGS-HANDOFF: COMPLETE\nPASS after the coordinator runs a diff."
|
|
1191
|
+
: variant === "invalid" ? "NCCGS-HANDOFF: COMPLETE\n```nccgs-review\nnot json\n```\n" : null;
|
|
1192
|
+
const stage = prepareStructuredReview(f, variant === "wrong-task" ? { taskId: "another" } : {}, body);
|
|
1193
|
+
if (variant === "unattached") stage.evidence = f.write(".nccgs/evidence/analysis/coordinator.md", fs.readFileSync(path.join(f.project, stage.evidence.path), "utf8"));
|
|
1194
|
+
if (variant === "changed-findings") stage.findings = [{ severity: "LOW", status: "OPEN", description: "Invented coordinator finding" }];
|
|
1195
|
+
assert.throws(() => recordStandalone(f.project, f.task(), "review", stage), /Review handoff/);
|
|
1196
|
+
} finally { f.dispose(); }
|
|
1197
|
+
}
|
|
1192
1198
|
});
|