@haposoft/cafekit 0.16.0-rc.3 → 0.16.0-rc.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (91) hide show
  1. package/bin/__tests__/codex-hooks.test.js +444 -14
  2. package/bin/__tests__/codex-native.test.js +228 -7
  3. package/bin/__tests__/develop-contract.test.js +615 -185
  4. package/bin/__tests__/installer-safety.test.js +39 -0
  5. package/bin/__tests__/opencode-spec-gate.test.js +24 -16
  6. package/bin/__tests__/package-inventory.test.js +687 -13
  7. package/bin/__tests__/specs-v2-execution-closeout.test.js +421 -0
  8. package/bin/__tests__/specs-v2-policy-and-scaffold.test.js +601 -0
  9. package/bin/__tests__/specs-v2-validator-grounder-contract.test.js +952 -0
  10. package/bin/__tests__/test-runtime-dependency-closure.cjs +182 -0
  11. package/bin/__tests__/validator-grounding.test.js +1914 -221
  12. package/bin/lib/managed-writer.js +12 -3
  13. package/bin/phases/codex-runtime.js +17 -1
  14. package/package.json +1 -1
  15. package/src/claude/CLAUDE.md +6 -0
  16. package/src/claude/agents/code-auditor.md +20 -0
  17. package/src/claude/agents/spec-maker.md +143 -99
  18. package/src/claude/hooks/__tests__/completion-authority.test.js +291 -58
  19. package/src/claude/hooks/__tests__/privacy-block.test.js +172 -0
  20. package/src/claude/hooks/__tests__/semantic-review-authority.test.js +257 -0
  21. package/src/claude/hooks/__tests__/spec-gate.test.js +115 -40
  22. package/src/claude/hooks/completion-authority-check.cjs +37 -288
  23. package/src/claude/hooks/completion-authority-state.cjs +10 -8
  24. package/src/claude/hooks/completion-authority.cjs +13 -7
  25. package/src/claude/hooks/docs-sync.cjs +5 -2
  26. package/src/claude/hooks/lib/privacy-command-analysis.cjs +393 -0
  27. package/src/claude/hooks/lib/runtime-path-safety.cjs +119 -0
  28. package/src/claude/hooks/privacy-block.cjs +77 -30
  29. package/src/claude/hooks/semantic-review-authority.cjs +152 -0
  30. package/src/claude/hooks/spec-gate.cjs +47 -117
  31. package/src/claude/hooks/spec-state.cjs +8 -2
  32. package/src/claude/migration-manifest.json +9 -1
  33. package/src/claude/rules/state-sync.md +52 -21
  34. package/src/claude/rules/workflow.md +17 -9
  35. package/src/claude/scripts/spec-final-state.cjs +244 -0
  36. package/src/claude/scripts/spec-ground.cjs +535 -173
  37. package/src/claude/scripts/spec-readiness.cjs +118 -0
  38. package/src/claude/scripts/spec-receipt.cjs +162 -0
  39. package/src/claude/scripts/spec-resolver.cjs +67 -16
  40. package/src/claude/scripts/spec-scaffold.cjs +1354 -139
  41. package/src/claude/scripts/spec-semantic-model.cjs +396 -0
  42. package/src/claude/scripts/validate-spec-output.cjs +2782 -142
  43. package/src/claude/scripts/workflow-policy.cjs +501 -183
  44. package/src/claude/settings/settings.json +4 -0
  45. package/src/claude/skills/code-review/SKILL.md +12 -3
  46. package/src/claude/skills/code-review/references/spec-compliance-review.md +10 -2
  47. package/src/claude/skills/code-review/references/verification-gate.md +13 -0
  48. package/src/claude/skills/develop/SKILL.md +47 -60
  49. package/src/claude/skills/develop/references/parallel-waves.md +9 -6
  50. package/src/claude/skills/develop/references/quality-gate.md +19 -12
  51. package/src/claude/skills/specs/SKILL.md +183 -177
  52. package/src/claude/skills/specs/references/ask-user-question-gates.md +27 -18
  53. package/src/claude/skills/specs/references/cross-spec-dependency.md +8 -4
  54. package/src/claude/skills/specs/references/research-strategy.md +4 -1
  55. package/src/claude/skills/specs/references/review.md +200 -260
  56. package/src/claude/skills/specs/references/scope-inquiry.md +18 -5
  57. package/src/claude/skills/specs/rules/design-discovery-full.md +33 -81
  58. package/src/claude/skills/specs/rules/design-discovery-light.md +21 -42
  59. package/src/claude/skills/specs/rules/design-principles.md +136 -180
  60. package/src/claude/skills/specs/rules/design-review.md +42 -97
  61. package/src/claude/skills/specs/rules/ears-format.md +42 -50
  62. package/src/claude/skills/specs/rules/phase-decision-matrix.md +42 -41
  63. package/src/claude/skills/specs/rules/task-scoring-rubric.md +2 -1
  64. package/src/claude/skills/specs/rules/tasks-generation.md +189 -300
  65. package/src/claude/skills/specs/templates/design.md +49 -263
  66. package/src/claude/skills/specs/templates/requirements.md +26 -36
  67. package/src/claude/skills/specs/templates/research.md +9 -98
  68. package/src/claude/skills/specs/templates/spec-state.json +23 -85
  69. package/src/claude/skills/specs/templates/task.md +30 -102
  70. package/src/claude/skills/sync/SKILL.md +33 -10
  71. package/src/claude/skills/sync/references/sync-protocols.md +31 -13
  72. package/src/claude/skills/test/SKILL.md +17 -7
  73. package/src/codex/AGENTS.md +6 -0
  74. package/src/codex/hooks/completion-authority-check.cjs +43 -240
  75. package/src/codex/hooks/completion-authority-state.cjs +10 -8
  76. package/src/codex/hooks/completion-authority.cjs +13 -7
  77. package/src/codex/hooks/lib/hook-context.cjs +16 -2
  78. package/src/codex/hooks/lib/privacy-command-analysis.cjs +3 -0
  79. package/src/codex/hooks/lib/privacy-state.cjs +38 -16
  80. package/src/codex/hooks/lib/runtime-path-safety.cjs +3 -0
  81. package/src/codex/hooks/lib/spec-receipt.cjs +72 -134
  82. package/src/codex/hooks/lib/spec-utils.cjs +56 -5
  83. package/src/codex/hooks/privacy-approval.cjs +1 -1
  84. package/src/codex/hooks/privacy-block.cjs +67 -11
  85. package/src/codex/hooks/semantic-review-authority.cjs +130 -0
  86. package/src/codex/hooks/spec-gate.cjs +49 -34
  87. package/src/codex/hooks/spec-state.cjs +10 -3
  88. package/src/codex/hooks/state.cjs +2 -0
  89. package/src/codex/hooks.json +6 -1
  90. package/src/codex/rules/state-sync.md +47 -27
  91. package/src/common/AGENTS.md +1 -1
@@ -23,6 +23,7 @@ const { upsertManagedCoreBlock } = require('../lib/instruction-blocks');
23
23
  const { upsertManagedCodexBlock, normalizeCodexBody } = require('../lib/codex-install');
24
24
  const { copyRecursive } = require('../lib/copy-utils');
25
25
  const { createTracker, sha256 } = require('../lib/manifest');
26
+ const { copyManagedTree } = require('../lib/managed-writer');
26
27
 
27
28
  const START = '<!-- CAFEKIT CLAUDE START -->';
28
29
  const END = '<!-- CAFEKIT CLAUDE END -->';
@@ -672,6 +673,44 @@ test('managed targets and snapshots reject project symlink traversal', () => {
672
673
  });
673
674
  });
674
675
 
676
+ test('managed tree copy rejects source and destination symlink traversal before writes', () => {
677
+ inTempProject((root) => {
678
+ const source = path.join(root, 'source');
679
+ const outsideSource = path.join(root, 'outside-source.txt');
680
+ const destination = path.join('.codex', 'payload');
681
+ fs.mkdirSync(source);
682
+ fs.writeFileSync(outsideSource, 'outside');
683
+ fs.symlinkSync(outsideSource, path.join(source, 'linked.txt'));
684
+ const ctx = {
685
+ dryRun: false,
686
+ options: { forceOverwrite: false },
687
+ ownership: {},
688
+ results: { preservedFiles: [] },
689
+ };
690
+ const copy = () => copyManagedTree({
691
+ src: source,
692
+ dest: destination,
693
+ platformFolder: '.codex',
694
+ ctx,
695
+ tracker: null,
696
+ });
697
+ assert.throws(copy, /source tree cannot contain symlinks/i);
698
+ assert.equal(fs.existsSync(destination), false);
699
+
700
+ fs.rmSync(path.join(source, 'linked.txt'));
701
+ fs.writeFileSync(path.join(source, 'safe.txt'), 'safe');
702
+ const outsideDestination = fs.mkdtempSync(path.join(os.tmpdir(), 'cafekit-managed-outside-'));
703
+ try {
704
+ fs.symlinkSync(outsideDestination, path.join(root, '.codex'));
705
+ assert.throws(copy, /Refusing to follow symlinked managed path/);
706
+ assert.deepEqual(fs.readdirSync(outsideDestination), []);
707
+ } finally {
708
+ fs.rmSync(path.join(root, '.codex'), { force: true });
709
+ fs.rmSync(outsideDestination, { recursive: true, force: true });
710
+ }
711
+ });
712
+ });
713
+
675
714
  test('managed Claude Addressing survives force refresh when template drops it', () => {
676
715
  inTempProject(() => {
677
716
  const before = '# User preface\n\n';
@@ -47,6 +47,7 @@ function ensureSharedValidator(tmp) {
47
47
  const sources = [
48
48
  ['workflow-policy.cjs', path.join(PACKAGE_ROOT, 'src/claude/scripts/workflow-policy.cjs')],
49
49
  ['spec-resolver.cjs', path.join(PACKAGE_ROOT, 'src/claude/scripts/spec-resolver.cjs')],
50
+ ['provenance.cjs', path.join(PACKAGE_ROOT, 'src/claude/scripts/provenance.cjs')],
50
51
  ];
51
52
  const dests = [
52
53
  path.join(tmp, '.opencode', 'scripts'),
@@ -66,7 +67,7 @@ test('OpenCode SpecGate lists multiple active candidates and does not pick first
66
67
  fs.mkdirSync(specsDir, { recursive: true });
67
68
  for (const name of ['alpha', 'beta']) {
68
69
  fs.mkdirSync(path.join(specsDir, name), { recursive: true });
69
- fs.writeFileSync(path.join(specsDir, name, 'spec.json'), JSON.stringify({ status: 'in_progress', current_phase: 'design', task_registry: {} }));
70
+ fs.writeFileSync(path.join(specsDir, name, 'spec.json'), JSON.stringify({ feature_name: name, status: 'in_progress', current_phase: 'design', task_registry: {} }));
70
71
  }
71
72
  const out = path.join(tmp, 'plugins', 'spec-gate.mjs');
72
73
  compileToMjs(GATE_SRC, out);
@@ -109,6 +110,7 @@ test('OpenCode installed gate reports missing and malformed shared policy as con
109
110
  }
110
111
  fs.mkdirSync(path.join(tmp, 'specs', 'demo', 'tasks'), { recursive: true });
111
112
  fs.writeFileSync(path.join(tmp, 'specs', 'demo', 'spec.json'), JSON.stringify({
113
+ feature_name: 'demo',
112
114
  status: 'in_progress',
113
115
  task_registry: { 'tasks/task.md': { status: 'done', completed_at: '2026-08-11T00:00:00.000Z' } },
114
116
  }));
@@ -145,6 +147,7 @@ test('OpenCode hard-blocks completion tools with a controlled result and allows
145
147
  const taskDir = path.join(tmp, 'specs', 'demo', 'tasks');
146
148
  fs.mkdirSync(taskDir, { recursive: true });
147
149
  fs.writeFileSync(path.join(tmp, 'specs', 'demo', 'spec.json'), JSON.stringify({
150
+ feature_name: 'demo',
148
151
  status: 'in_progress',
149
152
  task_registry: { 'tasks/task.md': { status: 'done', completed_at: '2026-08-11T00:00:00.000Z' } },
150
153
  }));
@@ -183,7 +186,7 @@ test('OpenCode explicit feature/path target selects the requested spec and never
183
186
  try {
184
187
  for (const name of ['alpha', 'beta']) {
185
188
  fs.mkdirSync(path.join(tmp, 'specs', name), { recursive: true });
186
- fs.writeFileSync(path.join(tmp, 'specs', name, 'spec.json'), JSON.stringify({ status: 'in_progress', task_registry: {} }));
189
+ fs.writeFileSync(path.join(tmp, 'specs', name, 'spec.json'), JSON.stringify({ feature_name: name, status: 'in_progress', task_registry: {} }));
187
190
  }
188
191
  const out = path.join(tmp, 'plugins', 'spec-gate.mjs');
189
192
  compileToMjs(GATE_SRC, out);
@@ -226,6 +229,7 @@ test('OpenCode adapter enforces declared task artifact SHA-256 with missing, inv
226
229
  const specsDir = path.join(tmp, 'specs');
227
230
  fs.mkdirSync(path.join(specsDir, 'artifact-demo', 'tasks'), { recursive: true });
228
231
  fs.writeFileSync(path.join(specsDir, 'artifact-demo', 'spec.json'), JSON.stringify({
232
+ feature_name: 'artifact-demo',
229
233
  status: 'in_progress',
230
234
  task_registry: {
231
235
  'tasks/task.md': {
@@ -276,8 +280,9 @@ test('OpenCode gate and state use the same configured external specs authority',
276
280
  const externalSpecs = path.join(tmp, 'external-specs');
277
281
  fs.mkdirSync(path.join(localSpecs, 'local'), { recursive: true });
278
282
  fs.mkdirSync(path.join(externalSpecs, 'remote', 'tasks'), { recursive: true });
279
- fs.writeFileSync(path.join(localSpecs, 'local', 'spec.json'), JSON.stringify({ status: 'in_progress', current_phase: 'local' }));
283
+ fs.writeFileSync(path.join(localSpecs, 'local', 'spec.json'), JSON.stringify({ feature_name: 'local', status: 'in_progress', current_phase: 'local' }));
280
284
  fs.writeFileSync(path.join(externalSpecs, 'remote', 'spec.json'), JSON.stringify({
285
+ feature_name: 'remote',
281
286
  status: 'in_progress',
282
287
  current_phase: 'remote',
283
288
  task_registry: { 'tasks/task.md': { status: 'done', completed_at: '2026-08-11T00:00:00.000Z' } },
@@ -325,7 +330,7 @@ test('OpenCode SpecState injects multiple-active warning with candidates', async
325
330
  fs.mkdirSync(specsDir, { recursive: true });
326
331
  for (const name of ['alpha', 'beta']) {
327
332
  fs.mkdirSync(path.join(specsDir, name), { recursive: true });
328
- fs.writeFileSync(path.join(specsDir, name, 'spec.json'), JSON.stringify({ status: 'in_progress', current_phase: 'design', task_registry: {} }));
333
+ fs.writeFileSync(path.join(specsDir, name, 'spec.json'), JSON.stringify({ feature_name: name, status: 'in_progress', current_phase: 'design', task_registry: {} }));
329
334
  }
330
335
  const out = path.join(tmp, 'plugins', 'spec-state.mjs');
331
336
  compileToMjs(STATE_SRC, out);
@@ -350,6 +355,7 @@ test('OpenCode SpecGate reports Exit:1 receipt as a completion failure', async (
350
355
  fs.mkdirSync(path.join(specsDir, 'demo', 'tasks'), { recursive: true });
351
356
  // Single active spec with done task but Exit 1 must remain incomplete.
352
357
  fs.writeFileSync(path.join(specsDir, 'demo', 'spec.json'), JSON.stringify({
358
+ feature_name: 'demo',
353
359
  status: 'in_progress',
354
360
  current_phase: 'implement',
355
361
  task_registry: { 'tasks/task.md': { status: 'done', completed_at: '2026-08-11T00:00:00.000Z' } },
@@ -398,7 +404,7 @@ test('OpenCode SpecState single active injects tollgate block (not multiple)', a
398
404
  try {
399
405
  const specsDir = path.join(tmp, 'specs');
400
406
  fs.mkdirSync(path.join(specsDir, 'solo'), { recursive: true });
401
- fs.writeFileSync(path.join(specsDir, 'solo', 'spec.json'), JSON.stringify({ status: 'in_progress', current_phase: 'design', task_registry: { 'tasks/a.md': { status: 'pending' } } }));
407
+ fs.writeFileSync(path.join(specsDir, 'solo', 'spec.json'), JSON.stringify({ feature_name: 'solo', status: 'in_progress', current_phase: 'design', task_registry: { 'tasks/a.md': { status: 'pending' } } }));
402
408
  const out = path.join(tmp, 'plugins', 'spec-state.mjs');
403
409
  compileToMjs(STATE_SRC, out);
404
410
  const { SpecState } = await import(pathToFileURL(out).href);
@@ -422,6 +428,7 @@ test('OpenCode tollgate cache binds canonical root, full session identity, and s
422
428
  for (const project of [projectA, projectB]) {
423
429
  fs.mkdirSync(path.join(project, 'specs', 'same'), { recursive: true });
424
430
  fs.writeFileSync(path.join(project, 'specs', 'same', 'spec.json'), JSON.stringify({
431
+ feature_name: 'same',
425
432
  status: 'in_progress',
426
433
  current_phase: 'design',
427
434
  task_registry: { 'tasks/task.md': { status: 'pending' } },
@@ -443,6 +450,7 @@ test('OpenCode tollgate cache binds canonical root, full session identity, and s
443
450
  fs.rmSync(path.join(projectA, 'specs', 'same'), { recursive: true, force: true });
444
451
  fs.mkdirSync(path.join(projectA, 'specs', 'other'), { recursive: true });
445
452
  fs.writeFileSync(path.join(projectA, 'specs', 'other', 'spec.json'), JSON.stringify({
453
+ feature_name: 'other',
446
454
  status: 'in_progress',
447
455
  current_phase: 'design',
448
456
  task_registry: { 'tasks/task.md': { status: 'pending' } },
@@ -479,7 +487,7 @@ test('OpenCode P0 regression: placeholder, explicit failure, artifact, symlink a
479
487
  try {
480
488
  const specsDir = path.join(tmp, 'specs');
481
489
  fs.mkdirSync(path.join(specsDir, 'demo', 'tasks'), { recursive: true });
482
- fs.writeFileSync(path.join(specsDir, 'demo', 'spec.json'), JSON.stringify({ status: 'in_progress', current_phase: 'implement', task_registry: { 'tasks/task.md': { status: 'done', completed_at: '2026-08-11T00:00:00.000Z' } } }));
490
+ fs.writeFileSync(path.join(specsDir, 'demo', 'spec.json'), JSON.stringify({ feature_name: 'demo', status: 'in_progress', current_phase: 'implement', task_registry: { 'tasks/task.md': { status: 'done', completed_at: '2026-08-11T00:00:00.000Z' } } }));
483
491
  fs.writeFileSync(path.join(specsDir, 'demo', 'tasks', 'task.md'), ['# Task','','**Status:** done','','## Evidence','',body].join('\n'));
484
492
  const out = path.join(tmp, 'plugins', 'spec-gate.mjs');
485
493
  compileToMjs(GATE_SRC, out);
@@ -503,7 +511,7 @@ test('OpenCode P0 regression: placeholder, explicit failure, artifact, symlink a
503
511
  try {
504
512
  const specsDir = path.join(tmp, 'specs');
505
513
  fs.mkdirSync(path.join(specsDir, 'demo', 'tasks'), { recursive: true });
506
- fs.writeFileSync(path.join(specsDir, 'demo', 'spec.json'), JSON.stringify({ status: 'in_progress', current_phase: 'implement', task_registry: { 'tasks/task.md': { status: 'done', completed_at: '2026-08-11T00:00:00.000Z' } } }));
514
+ fs.writeFileSync(path.join(specsDir, 'demo', 'spec.json'), JSON.stringify({ feature_name: 'demo', status: 'in_progress', current_phase: 'implement', task_registry: { 'tasks/task.md': { status: 'done', completed_at: '2026-08-11T00:00:00.000Z' } } }));
507
515
  fs.writeFileSync(path.join(specsDir, 'demo', 'tasks', 'task.md'), ['# Task','','**Status:** done','','## Evidence','','Verification: PASS','Command: npm run todo:test','Exit: 0',`Base: ${BASE_SHA}`,`Head: ${HEAD_SHA}`].join('\n'));
508
516
  const out = path.join(tmp, 'plugins', 'spec-gate.mjs');
509
517
  compileToMjs(GATE_SRC, out);
@@ -525,7 +533,7 @@ test('OpenCode P0 regression: placeholder, explicit failure, artifact, symlink a
525
533
  try {
526
534
  const specsDir = path.join(tmp, 'specs');
527
535
  fs.mkdirSync(path.join(specsDir, 'feat', 'tasks'), { recursive: true });
528
- fs.writeFileSync(path.join(specsDir, 'feat', 'spec.json'), JSON.stringify({ status: 'in_progress', task_registry: { 'tasks/task.md': { status: 'done', completed_at: '2026-08-11T00:00:00.000Z' } } }));
536
+ fs.writeFileSync(path.join(specsDir, 'feat', 'spec.json'), JSON.stringify({ feature_name: 'feat', status: 'in_progress', task_registry: { 'tasks/task.md': { status: 'done', completed_at: '2026-08-11T00:00:00.000Z' } } }));
529
537
  const outside = path.join(tmp, 'outside-task.md');
530
538
  fs.writeFileSync(outside, `# Task\n\n**Status:** done\n\n## Evidence\n\nVerification: PASS\nCommand: pnpm test\nExit: 0\nBase: ${BASE_SHA}\nHead: ${HEAD_SHA}\n`);
531
539
  const link = path.join(specsDir, 'feat', 'tasks', 'task.md');
@@ -552,7 +560,7 @@ test('OpenCode P0 regression: placeholder, explicit failure, artifact, symlink a
552
560
  const specsDir = path.join(tmp, 'specs');
553
561
  fs.mkdirSync(path.join(specsDir, 'good'), { recursive: true });
554
562
  fs.mkdirSync(path.join(specsDir, 'bad'), { recursive: true });
555
- fs.writeFileSync(path.join(specsDir, 'good', 'spec.json'), JSON.stringify({ status: 'in_progress' }));
563
+ fs.writeFileSync(path.join(specsDir, 'good', 'spec.json'), JSON.stringify({ feature_name: 'good', status: 'in_progress' }));
556
564
  fs.writeFileSync(path.join(specsDir, 'bad', 'spec.json'), '{ malformed');
557
565
  const out = path.join(tmp, 'plugins', 'spec-state.mjs');
558
566
  compileToMjs(STATE_SRC, out);
@@ -572,10 +580,10 @@ test('OpenCode P0 regression: placeholder, explicit failure, artifact, symlink a
572
580
  try {
573
581
  const specsDir = path.join(tmp, 'specs');
574
582
  fs.mkdirSync(path.join(specsDir, 'valid'), { recursive: true });
575
- fs.writeFileSync(path.join(specsDir, 'valid', 'spec.json'), JSON.stringify({ status: 'in_progress', task_registry: {} }));
583
+ fs.writeFileSync(path.join(specsDir, 'valid', 'spec.json'), JSON.stringify({ feature_name: 'valid', status: 'in_progress', task_registry: {} }));
576
584
  const outside = path.join(tmp, 'outside-spec');
577
585
  fs.mkdirSync(outside, { recursive: true });
578
- fs.writeFileSync(path.join(outside, 'spec.json'), JSON.stringify({ status: 'in_progress' }));
586
+ fs.writeFileSync(path.join(outside, 'spec.json'), JSON.stringify({ feature_name: 'linked', status: 'in_progress' }));
579
587
  const link = path.join(specsDir, 'linked');
580
588
  fs.symlinkSync(outside, link);
581
589
  const out = path.join(tmp, 'plugins', 'spec-state.mjs');
@@ -596,7 +604,7 @@ test('OpenCode P0 regression: placeholder, explicit failure, artifact, symlink a
596
604
  try {
597
605
  const specsDir = path.join(tmp, 'specs');
598
606
  fs.mkdirSync(path.join(specsDir, 'demo', 'tasks'), { recursive: true });
599
- fs.writeFileSync(path.join(specsDir, 'demo', 'spec.json'), JSON.stringify({ status: 'in_progress', current_phase: 'implement', task_registry: { 'tasks/task.md': { status: 'done', completed_at: '2026-08-11T00:00:00.000Z' } } }));
607
+ fs.writeFileSync(path.join(specsDir, 'demo', 'spec.json'), JSON.stringify({ feature_name: 'demo', status: 'in_progress', current_phase: 'implement', task_registry: { 'tasks/task.md': { status: 'done', completed_at: '2026-08-11T00:00:00.000Z' } } }));
600
608
  // Notes with failure handling should pass (not structured)
601
609
  fs.writeFileSync(path.join(specsDir, 'demo', 'tasks', 'task.md'), ['# Task','','**Status:** done','','## Evidence','','Verification: PASS','Command: pnpm test','Exit: 0',`Base: ${BASE_SHA}`,`Head: ${HEAD_SHA}`,'Notes: verifies failure handling'].join('\n'));
602
610
  const out = path.join(tmp, 'plugins', 'spec-gate.mjs');
@@ -639,7 +647,7 @@ test('OpenCode P0 regression: placeholder, explicit failure, artifact, symlink a
639
647
  try {
640
648
  const specsDir = path.join(tmp, 'specs');
641
649
  fs.mkdirSync(path.join(specsDir, 'demo', 'tasks'), { recursive: true });
642
- fs.writeFileSync(path.join(specsDir, 'demo', 'spec.json'), JSON.stringify({ status: 'in_progress', current_phase: 'implement', task_registry: { 'tasks/task.md': { status: 'done', completed_at: '2026-08-11T00:00:00.000Z' } } }));
650
+ fs.writeFileSync(path.join(specsDir, 'demo', 'spec.json'), JSON.stringify({ feature_name: 'demo', status: 'in_progress', current_phase: 'implement', task_registry: { 'tasks/task.md': { status: 'done', completed_at: '2026-08-11T00:00:00.000Z' } } }));
643
651
  fs.writeFileSync(path.join(specsDir, 'demo', 'tasks', 'task.md'), ['# Task','','**Status:** done','','## Evidence','','Verification: PASS','Command: pnpm test','Exit: 0',`Base: ${BASE_SHA}`,`Head: ${HEAD_SHA}`].join('\n'));
644
652
  const out = path.join(tmp, 'plugins', 'spec-gate.mjs');
645
653
  compileToMjs(GATE_SRC, out);
@@ -661,7 +669,7 @@ test('OpenCode P0 regression: placeholder, explicit failure, artifact, symlink a
661
669
  try {
662
670
  const specsDir = path.join(tmp, 'specs');
663
671
  fs.mkdirSync(path.join(specsDir, 'feat', 'tasks'), { recursive: true });
664
- fs.writeFileSync(path.join(specsDir, 'feat', 'spec.json'), JSON.stringify({ status: 'in_progress', task_registry: { 'tasks/task.md': { status: 'done', completed_at: '2026-08-11T00:00:00.000Z' } } }));
672
+ fs.writeFileSync(path.join(specsDir, 'feat', 'spec.json'), JSON.stringify({ feature_name: 'feat', status: 'in_progress', task_registry: { 'tasks/task.md': { status: 'done', completed_at: '2026-08-11T00:00:00.000Z' } } }));
665
673
  fs.writeFileSync(path.join(specsDir, 'feat', 'tasks', 'task.md'), ['# Task','','**Status:** done','','## Evidence','','Verification: PASS','Command: pnpm test','Exit: 0',`Base: ${BASE_SHA}`,`Head: ${HEAD_SHA}`].join('\n'));
666
674
  const out = path.join(tmp, 'plugins', 'spec-gate.mjs');
667
675
  compileToMjs(GATE_SRC, out);
@@ -714,7 +722,7 @@ test('OpenCode P0 phantom vectors via installed shared validator (r3)', async ()
714
722
  try {
715
723
  const specsDir = path.join(tmp, 'specs');
716
724
  fs.mkdirSync(path.join(specsDir, 'demo', 'tasks'), { recursive: true });
717
- fs.writeFileSync(path.join(specsDir, 'demo', 'spec.json'), JSON.stringify({ status: 'in_progress', current_phase: 'implement', task_registry: { 'tasks/task.md': { status: 'done', completed_at: '2026-08-11T00:00:00.000Z' } } }));
725
+ fs.writeFileSync(path.join(specsDir, 'demo', 'spec.json'), JSON.stringify({ feature_name: 'demo', status: 'in_progress', current_phase: 'implement', task_registry: { 'tasks/task.md': { status: 'done', completed_at: '2026-08-11T00:00:00.000Z' } } }));
718
726
  fs.writeFileSync(path.join(specsDir, 'demo', 'tasks', 'task.md'), ['# Task','','**Status:** done','','## Evidence','','Verification: PASS','Command: pnpm test','Exit: 0',`Base: ${BASE_SHA}`,`Head: ${HEAD_SHA}`, v].join('\n'));
719
727
  const out = path.join(tmp, 'plugins', 'spec-gate.mjs');
720
728
  compileToMjs(GATE_SRC, out);
@@ -758,7 +766,7 @@ test('OpenCode P0 phantom vectors via installed shared validator (r3)', async ()
758
766
  try {
759
767
  const specsDir = path.join(tmp, 'specs');
760
768
  fs.mkdirSync(path.join(specsDir, 'demo', 'tasks'), { recursive: true });
761
- fs.writeFileSync(path.join(specsDir, 'demo', 'spec.json'), JSON.stringify({ status: 'in_progress', current_phase: 'implement', task_registry: { 'tasks/task.md': { status: 'done', completed_at: '2026-08-11T00:00:00.000Z' } } }));
769
+ fs.writeFileSync(path.join(specsDir, 'demo', 'spec.json'), JSON.stringify({ feature_name: 'demo', status: 'in_progress', current_phase: 'implement', task_registry: { 'tasks/task.md': { status: 'done', completed_at: '2026-08-11T00:00:00.000Z' } } }));
762
770
  fs.writeFileSync(path.join(specsDir, 'demo', 'tasks', 'task.md'), [
763
771
  '# Task',
764
772
  '',