@haposoft/cafekit 0.16.0-rc.6 → 0.16.0-rc.7

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 (86) hide show
  1. package/README.md +21 -58
  2. package/bin/__tests__/change-firewall.semantic-firewall.test.js +4 -5
  3. package/bin/__tests__/codex-hooks.test.js +137 -0
  4. package/bin/__tests__/codex-native.test.js +130 -74
  5. package/bin/__tests__/develop-contract.test.js +155 -76
  6. package/bin/__tests__/installer-safety.test.js +19 -154
  7. package/bin/__tests__/package-inventory.test.js +10 -20
  8. package/bin/__tests__/release-preflight.semantic-firewall.test.js +2 -2
  9. package/bin/__tests__/validator-grounding.test.js +11 -11
  10. package/bin/install.js +2 -7
  11. package/bin/lib/context.js +0 -45
  12. package/bin/lib/copy-utils.js +1 -1
  13. package/bin/lib/i18n.js +0 -3
  14. package/bin/lib/manifest.js +1 -1
  15. package/bin/phases/claude-runtime.js +1 -3
  16. package/bin/phases/codex-runtime.js +4 -2
  17. package/bin/phases/copy-payload.js +3 -15
  18. package/bin/phases/post-install.js +5 -17
  19. package/bin/phases/root-config.js +2 -3
  20. package/bin/phases/select-platform.js +2 -5
  21. package/bin/phases/summary.js +0 -1
  22. package/package.json +2 -5
  23. package/src/claude/CLAUDE.md +19 -6
  24. package/src/claude/agents/project-manager.md +41 -9
  25. package/src/claude/agents/spec-maker.md +76 -155
  26. package/src/claude/gitignore +1 -1
  27. package/src/claude/hooks/__tests__/spec-gate.test.js +162 -0
  28. package/src/claude/hooks/lib/context.cjs +1 -1
  29. package/src/claude/hooks/lib/detect.cjs +1 -1
  30. package/src/claude/hooks/spec-gate.cjs +44 -9
  31. package/src/claude/hooks/spec-state.cjs +22 -11
  32. package/src/claude/hooks/task-scaffold-guard.cjs +1 -1
  33. package/src/claude/migration-manifest.json +17 -3
  34. package/src/claude/rules/manage-docs.md +43 -24
  35. package/src/claude/rules/state-sync.md +57 -62
  36. package/src/claude/rules/workflow.md +30 -13
  37. package/src/claude/scripts/change-firewall.cjs +2 -2
  38. package/src/claude/scripts/generate-skill-catalog.cjs +0 -1
  39. package/src/claude/scripts/spec-receipt.cjs +141 -1
  40. package/src/claude/scripts/spec-resolver.cjs +288 -0
  41. package/src/claude/skills/develop/SKILL.md +89 -125
  42. package/src/claude/skills/develop/references/parallel-waves.md +75 -162
  43. package/src/claude/skills/develop/references/quality-gate.md +61 -121
  44. package/src/claude/skills/develop/references/subagent-patterns.md +31 -38
  45. package/src/claude/skills/question/SKILL.md +1 -1
  46. package/src/claude/skills/specs/SKILL.md +142 -204
  47. package/src/claude/skills/specs/references/review.md +109 -243
  48. package/src/claude/skills/specs/references/templates.md +173 -0
  49. package/src/claude/skills/sync/SKILL.md +66 -90
  50. package/src/claude/skills/sync/references/sync-protocols.md +62 -83
  51. package/src/codex/AGENTS.md +20 -7
  52. package/src/codex/hooks/lib/spec-receipt.cjs +18 -0
  53. package/src/codex/hooks/lib/spec-utils.cjs +25 -0
  54. package/src/codex/hooks/spec-gate.cjs +46 -16
  55. package/src/codex/hooks/spec-state.cjs +22 -12
  56. package/src/codex/rules/state-sync.md +57 -61
  57. package/src/common/AGENTS.md +8 -1
  58. package/bin/__tests__/opencode-spec-gate.test.js +0 -799
  59. package/bin/lib/opencode-install.js +0 -899
  60. package/bin/phases/opencode-runtime.js +0 -38
  61. package/src/claude/skills/specs/references/archive-workflow.md +0 -74
  62. package/src/claude/skills/specs/references/ask-user-question-gates.md +0 -70
  63. package/src/claude/skills/specs/references/codebase-analysis.md +0 -102
  64. package/src/claude/skills/specs/references/cross-spec-dependency.md +0 -76
  65. package/src/claude/skills/specs/references/research-strategy.md +0 -107
  66. package/src/claude/skills/specs/references/scope-inquiry.md +0 -107
  67. package/src/claude/skills/specs/references/translation-mirror.md +0 -83
  68. package/src/claude/skills/specs/rules/design-discovery-full.md +0 -45
  69. package/src/claude/skills/specs/rules/design-discovery-light.md +0 -28
  70. package/src/claude/skills/specs/rules/design-principles.md +0 -138
  71. package/src/claude/skills/specs/rules/design-review.md +0 -55
  72. package/src/claude/skills/specs/rules/ears-format.md +0 -42
  73. package/src/claude/skills/specs/rules/phase-decision-matrix.md +0 -56
  74. package/src/claude/skills/specs/rules/task-scoring-rubric.md +0 -52
  75. package/src/claude/skills/specs/rules/tasks-generation.md +0 -189
  76. package/src/opencode/AGENTS.md +0 -41
  77. package/src/opencode/plugins/docs-sync.ts +0 -175
  78. package/src/opencode/plugins/inspect-block.ts +0 -164
  79. package/src/opencode/plugins/privacy-block.ts +0 -182
  80. package/src/opencode/plugins/rules.ts +0 -155
  81. package/src/opencode/plugins/session.ts +0 -179
  82. package/src/opencode/plugins/spec-gate.ts +0 -457
  83. package/src/opencode/plugins/spec-state.ts +0 -311
  84. package/src/opencode/plugins/state.ts +0 -253
  85. package/src/opencode/plugins/task-scaffold-guard.ts +0 -161
  86. package/src/opencode/plugins/usage.ts +0 -178
@@ -27,6 +27,7 @@ const TEST_SKILL = path.join(PACKAGE_ROOT, 'src/claude/skills/test/SKILL.md');
27
27
  const SYNC_SKILL = path.join(PACKAGE_ROOT, 'src/claude/skills/sync/SKILL.md');
28
28
  const CODE_REVIEW_SKILL = path.join(PACKAGE_ROOT, 'src/claude/skills/code-review/SKILL.md');
29
29
  const PARALLEL_WAVES = path.join(PACKAGE_ROOT, 'src/claude/skills/develop/references/parallel-waves.md');
30
+ const SYNC_PROTOCOLS = path.join(PACKAGE_ROOT, 'src/claude/skills/sync/references/sync-protocols.md');
30
31
  const PROVENANCE = path.join(PACKAGE_ROOT, '../../docs/provenance.md');
31
32
  const RUNTIME_ROOT = fs.mkdtempSync(path.join(os.tmpdir(), 'cafekit-policy-runtime-'));
32
33
  const RUNTIME_SPEC = path.join(RUNTIME_ROOT, 'specs', 'demo', 'spec.json');
@@ -75,28 +76,62 @@ function installClaudeRuntimeClosure(fixtureRoot, entry = 'hooks/spec-gate.cjs')
75
76
  return path.join(destinationRoot, entry);
76
77
  }
77
78
 
78
- function markdownSection(content, heading) {
79
- const marker = `## ${heading}`;
80
- const start = content.indexOf(marker);
81
- assert.notEqual(start, -1, `missing section ${heading}`);
82
- const rest = content.slice(start + marker.length);
83
- const end = rest.search(/\n## /);
84
- return end === -1 ? rest : rest.slice(0, end);
79
+ function markdownLegacyRegions(content) {
80
+ const headings = [];
81
+ const primaryLines = [];
82
+ const legacyLines = [];
83
+ const hierarchy = [];
84
+
85
+ for (const [index, line] of content.split('\n').entries()) {
86
+ const heading = line.match(/^(#{1,6})\s+(.+?)\s*$/);
87
+ if (heading) {
88
+ const level = heading[1].length;
89
+ while (hierarchy.at(-1)?.level >= level) hierarchy.pop();
90
+ const entry = { level, text: heading[2], line: index + 1 };
91
+ hierarchy.push(entry);
92
+ headings.push(entry);
93
+ }
94
+
95
+ const target = hierarchy.some(({ text }) => /legacy/i.test(text))
96
+ ? legacyLines
97
+ : primaryLines;
98
+ target.push({ line: index + 1, text: line });
99
+ }
100
+
101
+ return {
102
+ headings,
103
+ primary: primaryLines.map(({ text }) => text).join('\n'),
104
+ legacy: legacyLines.map(({ text }) => text).join('\n'),
105
+ primaryLines,
106
+ };
85
107
  }
86
108
 
87
- function markdownTable(section, firstHeader) {
88
- const lines = section.split('\n').filter((line) => line.trim().startsWith('|'));
89
- const cells = (line) => line.split('|').slice(1, -1).map((cell) => cell.trim());
90
- const headerIndex = lines.findIndex((line) => cells(line)[0] === firstHeader);
91
- assert.notEqual(headerIndex, -1, `missing table headed by ${firstHeader}`);
92
- const headers = cells(lines[headerIndex]);
93
- const rows = {};
94
- for (const line of lines.slice(headerIndex + 2)) {
95
- const values = cells(line);
96
- if (values.length !== headers.length) break;
97
- rows[values[0].replaceAll('`', '')] = Object.fromEntries(headers.map((header, index) => [header, values[index]]));
109
+ function assertVocabularyIsLegacyOnly(filePath, regions) {
110
+ const vocabulary = [
111
+ ['v2.1', /\bv2\.1\b/i],
112
+ ['spec.json', /\bspec\.json\b/i],
113
+ ['semantic_model', /\bsemantic[-_]model\b/i],
114
+ ['machine authority', /\bmachine authority\b/i],
115
+ ['task_registry', /\btask_registry\b/i],
116
+ ['workflow_policy', /\bworkflow_policy\b/i],
117
+ ['planning_depth', /\bplanning_depth\b/i],
118
+ ['assurance_level', /\bassurance_level\b/i],
119
+ ['execution_tier', /\bexecution_tier\b/i],
120
+ ['classified_minimum', /\bclassified_minimum\b/i],
121
+ ['coordination.boundaries', /\bcoordination\.boundaries\b/i],
122
+ ['lane', /\blane\b/i],
123
+ ];
124
+ const violations = [];
125
+ for (const { line, text } of regions.primaryLines) {
126
+ for (const [term, pattern] of vocabulary) {
127
+ if (pattern.test(text)) violations.push(`${line}:${term}`);
128
+ }
98
129
  }
99
- return rows;
130
+ assert.deepEqual(
131
+ violations,
132
+ [],
133
+ `${path.relative(PACKAGE_ROOT, filePath)} has v2.1 vocabulary outside a Legacy heading`,
134
+ );
100
135
  }
101
136
 
102
137
  function runNode(root, script, args) {
@@ -305,11 +340,16 @@ test('CLI rejects flash and parallel before any mutation', () => {
305
340
  }
306
341
  });
307
342
 
308
- test('develop pre-state guard names executable policy contract', () => {
343
+ test('develop flags retain their process-v3 contracts', () => {
309
344
  const develop = read(DEVELOP);
310
345
  assert.match(develop, /node \.claude\/scripts\/workflow-policy\.cjs --flash --parallel --json/);
311
- assert.match(develop, /No spec state, task receipt, worktree, subagent, or commit was created/);
312
- assert.match(develop, /flash\+parallel fail-fast/i);
346
+ assert.match(develop, /exits `2` before a task edit, receipt, worktree, subagent, or commit/i);
347
+ assert.match(develop, /--notes` is opt-in/i);
348
+ assert.match(develop, /implementation-notes-template\.html/);
349
+ assert.match(develop, /Never create it by default/i);
350
+ assert.match(develop, /--parallel \[N\][\s\S]*parallel-waves\.md[\s\S]*isolated worktrees[\s\S]*one controller writer/i);
351
+ assert.match(develop, /--flash[\s\S]*Status: in_progress[\s\S]*FLASH_UNVERIFIED[\s\S]*awaiting \/hapo:test <feature>/i);
352
+ assert.match(develop, /FLASH_UNVERIFIED[\s\S]*do not unblock dependents[\s\S]*sync-finalize path/i);
313
353
  });
314
354
 
315
355
  test('delegation plan consumes legacy tiers as obligations without an agent chain', () => {
@@ -674,7 +714,7 @@ test('approval schema fails closed for explicit null, array, empty, and malforme
674
714
  assert.throws(() => POLICY.approvalState({ schema_version: '1.0' }), /schema_version/);
675
715
  });
676
716
 
677
- test('CLI exposes a derived lane and Develop consumes independent canonical policy axes', () => {
717
+ test('CLI exposes a derived lane without making it primary Develop authority', () => {
678
718
  const cli = spawnSync(process.execPath, [
679
719
  POLICY_PATH,
680
720
  '--classify-lane',
@@ -687,18 +727,17 @@ test('CLI exposes a derived lane and Develop consumes independent canonical poli
687
727
  assert.equal(payload.classification.lane, 'Direct');
688
728
  assert.equal(payload.policy.requiresSpec, false);
689
729
  const develop = read(DEVELOP);
730
+ const regions = markdownLegacyRegions(develop);
690
731
  assert.doesNotMatch(develop, /DO NOT write implementation code until an approved spec exists/i);
691
- const policySection = develop.split('## Policy authority')[1]?.split('\n## ')[0] || '';
692
- for (const authorityTerm of ['planning_depth', 'assurance_level', 'Risk', 'derived']) {
693
- assert.ok(policySection.includes(authorityTerm), `policy authority missing ${authorityTerm}`);
694
- }
695
- for (const compatibilityView of ['**Direct**', '**Standard**', '**Critical**']) {
696
- assert.ok(policySection.includes(compatibilityView), `missing derived view ${compatibilityView}`);
697
- }
698
- assert.ok(policySection.includes('derived Strict view'));
699
- assert.ok(policySection.includes('Legacy approval fields'));
700
- assert.ok(policySection.replace(/\s+/g, ' ').includes('zero authority'));
701
- assert.match(develop, /Classify lane[\s\S]*test receipt[\s\S]*docs-impact sync/i);
732
+ assert.doesNotMatch(regions.primary, /planning_depth|assurance_level|execution_tier|\blane\b/i);
733
+ assert.match(regions.legacy, /planning_depth/);
734
+ assert.match(regions.legacy, /assurance_level/);
735
+ assert.match(regions.legacy, /execution_tier/);
736
+ assert.match(regions.legacy, /derived lane/i);
737
+ assert.match(regions.primary, /specs\/<feature>\/plan\.md[\s\S]*task-NN-\*\.md/i);
738
+ assert.match(regions.primary, /one unblocked task at a time/i);
739
+ assert.match(regions.primary, /Verification Plan/);
740
+ assert.match(regions.primary, /inline Receipt/i);
702
741
  assert.doesNotMatch(develop, /D2\[Step 3/);
703
742
  assert.match(develop, /--notes.*opt-in/i);
704
743
  assert.doesNotMatch(develop, /--no-notes/);
@@ -717,30 +756,67 @@ test('CLI exposes a derived lane and Develop consumes independent canonical poli
717
756
  assert.notDeepEqual(fullRoutine.proof_obligations, compactStrict.proof_obligations);
718
757
  });
719
758
 
720
- test('artifact router keeps optional expansion bound to uncertainty and typed topology', () => {
721
- const router = markdownSection(read(SPECS), 'Artifact router');
722
- const rows = markdownTable(router, 'Depth');
723
- assert.deepEqual(Object.keys(rows), ['None', 'Compact', 'Full']);
724
- assert.equal(rows.None['Durable core'], 'none');
725
- for (const depth of ['Compact', 'Full']) {
726
- assert.deepEqual(
727
- rows[depth]['Durable core'].match(/`[^`]+`/g),
728
- ['`spec.json`', '`requirements.md`', '`design.md`'],
729
- `${depth} must share the canonical three-file core`,
730
- );
731
- assert.equal(rows[depth]['Research route'], 'only on a research trigger');
732
- assert.equal(rows[depth]['Task route'], 'only on a typed topology trigger');
733
- }
734
- assert.notEqual(rows.Compact['Design expansion'], rows.Full['Design expansion']);
735
- for (const falseTrigger of ['Requirement count', 'size', 'planning depth', 'assurance level', 'risk label', 'architectural layers']) {
736
- assert.ok(router.includes(falseTrigger), `router must name inert trigger ${falseTrigger}`);
737
- }
738
- for (const boundary of ['ownership', 'dependency', 'transition', 'proof', 'parallel']) {
739
- assert.ok(router.includes(boundary), `router must name typed topology ${boundary}`);
759
+ test('Specs primary output is a flat process-first packet with isolated legacy compatibility', () => {
760
+ const specs = read(SPECS);
761
+ const regions = markdownLegacyRegions(specs);
762
+ const legacyHeadings = regions.headings.filter(({ text }) => /legacy/i.test(text));
763
+ assert.equal(legacyHeadings.length, 1);
764
+ assert.match(regions.primary, /specs\/<feature>\/[\s\S]*plan\.md[\s\S]*task-01-<slug>\.md[\s\S]*task-02-<slug>\.md/i);
765
+ assert.match(regions.primary, /Task files are flat beside `plan\.md`/i);
766
+ assert.match(regions.primary, /one task at a time/i);
767
+ assert.match(regions.primary, /inline `## Receipt`/i);
768
+ assert.match(regions.primary, /C1 — Scope[\s\S]*C2 — Findings[\s\S]*C3 — Done/i);
769
+ assert.match(regions.legacy, /spec\.json/i);
770
+ assert.match(regions.legacy, /never requires the legacy kernel/i);
771
+ assertVocabularyIsLegacyOnly(SPECS, regions);
772
+ });
773
+
774
+ test('R7 Develop and Sync surfaces teach process-v3 and isolate hierarchical Legacy sections', () => {
775
+ const surfaces = [
776
+ ['develop', DEVELOP],
777
+ ['parallel waves', PARALLEL_WAVES],
778
+ ['sync', SYNC_SKILL],
779
+ ['sync protocols', SYNC_PROTOCOLS],
780
+ ];
781
+ const regionsByName = new Map();
782
+ let legacyCorpus = '';
783
+
784
+ for (const [name, filePath] of surfaces) {
785
+ const regions = markdownLegacyRegions(read(filePath));
786
+ const legacyHeadings = regions.headings.filter(({ text }) => /legacy/i.test(text));
787
+ assert.equal(legacyHeadings.length, 1, `${name} must have exactly one Legacy heading`);
788
+ assert.match(regions.primary, /inline (?:`## )?Receipts?/i, `${name} must teach inline Receipts`);
789
+ assert.match(regions.legacy, /spec\.json/i, `${name} must retain the spec.json adapter`);
790
+ assert.match(regions.legacy, /task_registry/i, `${name} must retain task_registry compatibility`);
791
+ assertVocabularyIsLegacyOnly(filePath, regions);
792
+ regionsByName.set(name, regions);
793
+ legacyCorpus += `\n${regions.legacy}`;
740
794
  }
741
- assert.ok(router.includes('unresolved material uncertainty'));
742
- assert.ok(router.includes('external-current fact'));
743
- assert.ok(router.includes('explicit user request'));
795
+
796
+ const develop = regionsByName.get('develop').primary;
797
+ assert.match(develop, /`specs\/<feature>\/plan\.md` plus flat `task-NN-\*\.md`/i);
798
+ assert.match(develop, /selects one unblocked task at a time/i);
799
+ assert.match(develop, /Outcome and Acceptance/i);
800
+ assert.match(develop, /Verification Plan/);
801
+ assert.match(develop, /final `## Receipt`/i);
802
+
803
+ const sync = regionsByName.get('sync').primary;
804
+ assert.match(sync, /<task-NN-slug\.md>/i);
805
+ assert.match(sync, /specs\/<feature>\/task-\*\.md` beside `plan\.md`/i);
806
+ assert.match(sync, /Acceptance[\s\S]*Verification Plan/i);
807
+ assert.match(sync, /current inline Receipt/i);
808
+
809
+ const waves = regionsByName.get('parallel waves').primary;
810
+ assert.match(waves, /plan task table[\s\S]*flat task's[\s\S]*`## Dependencies`/i);
811
+ assert.match(waves, /acceptance, Verification Plan/i);
812
+ assert.match(waves, /one file has one writer per wave/i);
813
+ assert.match(waves, /writes inline Receipts and Status updates one task at a\s+time/i);
814
+
815
+ const protocols = regionsByName.get('sync protocols').primary;
816
+ assert.match(protocols, /`plan\.md` and flat `task-\*\.md` files/i);
817
+ assert.match(protocols, /acceptance IDs/i);
818
+ assert.match(protocols, /current inline Receipt/i);
819
+ assert.match(legacyCorpus, /sync-finalize/i);
744
820
  });
745
821
 
746
822
  test('canonical verdict adapter handles completion and unfinished decisions', () => {
@@ -896,11 +972,9 @@ test('auto authoring reaches readiness only after promotion, validation, groundi
896
972
  const specs = read(SPECS);
897
973
  const executableBlocks = [...specs.matchAll(/```bash\n([\s\S]*?)```/g)].map((match) => match[1]);
898
974
  assert.ok(executableBlocks.every((block) => !/hapo:?develop/i.test(block)), 'Specs must never execute Develop');
899
- const handoff = markdownSection(specs, 'Platform acceptance and handoff');
900
- assert.ok(handoff.includes('After readiness'));
901
- assert.ok(handoff.includes('/hapo:develop <feature>'));
902
- assert.ok(handoff.includes('$hapo-develop <feature>'));
903
- assert.ok(handoff.includes('must stop in Specs'));
975
+ assert.match(specs, /An implementation workflow owns execution/i);
976
+ assert.match(specs, /selects one unblocked task[\s\S]*runs\s+the task's verification[\s\S]*inline `## Receipt`/i);
977
+ assert.match(specs, /The user decides\s+at C3 whether the feature is done/i);
904
978
  } finally {
905
979
  fs.rmSync(fixture.root, { recursive: true, force: true });
906
980
  }
@@ -1519,18 +1593,29 @@ test('flash selective promotion - only specific task is promoted, not blanket',
1519
1593
  });
1520
1594
 
1521
1595
  test('parallel waves require immutable provenance receipts and safe recovery', () => {
1522
- const waves = read(PARALLEL_WAVES);
1596
+ const waves = markdownLegacyRegions(read(PARALLEL_WAVES)).primary;
1597
+ assert.match(waves, /only for explicit parallel execution/i);
1598
+ assert.match(waves, /worktree isolation[\s\S]*fall back to the sequential loop/i);
1599
+ assert.match(waves, /Clamp `--parallel N` to 1\.\.5; default to 3/i);
1600
+ assert.match(waves, /clean destination[\s\S]*base_sha[\s\S]*base compatibility/i);
1601
+ assert.match(waves, /parallel consent[\s\S]*worker commits[\s\S]*controller cherry-picks/i);
1523
1602
  for (const field of ['base_sha', 'head_sha', 'branch', 'worktree_path', 'commit_range']) {
1524
1603
  assert.match(waves, new RegExp(`\\b${field}\\b`));
1525
1604
  }
1526
1605
  assert.match(waves, /git diff(?: --stat)? [^\\n]*(?:BASE_SHA|base_sha)[^\\n]*(?:HEAD_SHA|head_sha)/i);
1527
1606
  assert.match(waves, /new commit|new commits|Every fix is a new commit/i);
1528
- assert.match(waves, /destination tree must be clean/i);
1529
- assert.match(waves, /compatible with the selected base/i);
1530
- assert.match(waves, /consent.*commit|commit.*consent/i);
1531
- assert.match(waves, /retention.*retained|retained.*worktree/i);
1532
- assert.match(waves, /cleanup_authorization|explicit discard/i);
1533
- assert.match(waves, /directory overlap|lockfiles|manifests|export barrels|migrations|registries|generated artifacts|shared state writers/i);
1607
+ assert.match(waves, /plan task table[\s\S]*flat task's[\s\S]*Dependencies/i);
1608
+ for (const sharedSurface of ['registry', 'lockfile', 'manifest', 'generated output', 'migration', 'export barrel']) {
1609
+ assert.match(waves, new RegExp(sharedSurface, 'i'));
1610
+ }
1611
+ assert.match(waves, /One file has one writer per wave/i);
1612
+ assert.match(waves, /controller owns[\s\S]*state and evidence synchronization/i);
1613
+ assert.match(waves, /authorization covers worker commits/i);
1614
+ assert.match(waves, /blocked or failed worker keeps[\s\S]*worktree and branch/i);
1615
+ assert.match(waves, /Never[\s\S]*force-delete[\s\S]*merge success or explicit discard authorization/i);
1616
+ assert.match(waves, /git cherry-pick <worker-commit>/i);
1617
+ assert.match(waves, /On conflict, abort the cherry-pick[\s\S]*retain its[\s\S]*worktree\/branch/i);
1618
+ assert.match(waves, /cleanup_authorization: merged-release/i);
1534
1619
  assert.match(waves, /affected integration|final.*integration/i);
1535
1620
  for (const category of ['baseline', 'environment', 'spec', 'code']) {
1536
1621
  assert.match(waves, new RegExp(`\\b${category}\\b`));
@@ -2635,17 +2720,11 @@ test('P0 regression: safeTaskFile fail-closed on realpath error and single autho
2635
2720
  // Single authority: Claude hook should delegate to POLICY, not duplicate parser
2636
2721
  assert.match(specGateSrc, /POLICY\.validateCanonicalReceipt/);
2637
2722
  assert.doesNotMatch(specGateSrc, /const EXPLICIT_FAILURE/);
2638
- // Codex and OpenCode should delegate to shared workflow-policy
2723
+ // Codex should delegate to shared workflow-policy
2639
2724
  const codexSrc = fs2.readFileSync(path2.join(PACKAGE_ROOT, 'src/codex/hooks/lib/spec-receipt.cjs'), 'utf8');
2640
2725
  assert.match(codexSrc, /getSharedValidate/);
2641
2726
  assert.match(codexSrc, /workflow-policy\.cjs/);
2642
2727
  assert.doesNotMatch(codexSrc, /const PLACEHOLDER_TOKENS/);
2643
- const opencodeSrc = fs2.readFileSync(path2.join(PACKAGE_ROOT, 'src/opencode/plugins/spec-gate.ts'), 'utf8');
2644
- assert.match(opencodeSrc, /getSharedValidate/);
2645
- assert.match(opencodeSrc, /workflow-policy\.cjs/);
2646
- // The parser should only exist in workflow-policy, not in adapters (check that adapter does not define full validate logic)
2647
- const opencodeValidateCount = (opencodeSrc.match(/function validateCanonicalReceipt/g) || []).length;
2648
- assert.equal(opencodeValidateCount, 1, 'OpenCode should have only delegating validate, not duplicate parser');
2649
2728
  // safeTaskFile fail-closed: simulate realpathSync throwing
2650
2729
  const tmp = fs2.mkdtempSync(path2.join(os2.tmpdir(), 'cafekit-realpath-'));
2651
2730
  try {
@@ -8,10 +8,6 @@ const { spawnSync } = require('node:child_process');
8
8
  const { test } = require('node:test');
9
9
 
10
10
  const backup = require('../lib/backup');
11
- const {
12
- copyOpenCodeAgentsMdFile,
13
- normalizeOpenCodeBody
14
- } = require('../lib/opencode-install');
15
11
  const {
16
12
  commandFailureReason,
17
13
  resolvePackageCommand
@@ -34,8 +30,6 @@ const SHARED_TEMPLATE = fs.readFileSync(path.join(__dirname, '../../src/common/A
34
30
  const INSTALLER = path.join(__dirname, '../install.js');
35
31
  const CODEX_START = '<!-- CAFEKIT CODEX START -->';
36
32
  const CODEX_END = '<!-- CAFEKIT CODEX END -->';
37
- const OPENCODE_START = '<!-- CAFEKIT OPENCODE START -->';
38
- const OPENCODE_END = '<!-- CAFEKIT OPENCODE END -->';
39
33
 
40
34
  function managedBody(content, start, end) {
41
35
  const startIndex = content.indexOf(start);
@@ -47,7 +41,7 @@ function managedBody(content, start, end) {
47
41
  function runCombinedInstaller(root, extraArgs = []) {
48
42
  return spawnSync(
49
43
  process.execPath,
50
- [INSTALLER, '--platform', 'claude,codex,opencode', '--yes', ...extraArgs],
44
+ [INSTALLER, '--platform', 'claude,codex', '--yes', ...extraArgs],
51
45
  { cwd: root, encoding: 'utf8', env: { ...process.env, PATH: '/usr/bin:/bin' } }
52
46
  );
53
47
  }
@@ -118,7 +112,7 @@ test('legacy managed root gitignore migrates A13 plan patterns and preserves use
118
112
  '.cafekit-backup/',
119
113
  '.cafekit.lock',
120
114
  '.claude/',
121
- '.opencode/',
115
+ '.legacy-runtime/',
122
116
  '.codex/',
123
117
  '.agents/',
124
118
  '',
@@ -150,7 +144,7 @@ test('legacy managed root gitignore migrates A13 plan patterns and preserves use
150
144
  '.cafekit-backup/',
151
145
  '.cafekit.lock',
152
146
  '.claude/',
153
- '.opencode/',
147
+ '.legacy-runtime/',
154
148
  '.codex/',
155
149
  '.agents/'
156
150
  ]);
@@ -188,8 +182,7 @@ test('legacy managed root gitignore migrates A13 plan patterns and preserves use
188
182
  test('malformed managed marker topology fails transactionally and preserves exact bytes', () => {
189
183
  const markers = {
190
184
  claude: ['<!-- CAFEKIT CORE START -->', '<!-- CAFEKIT CORE END -->'],
191
- codex: ['<!-- CAFEKIT CODEX START -->', '<!-- CAFEKIT CODEX END -->'],
192
- opencode: ['<!-- CAFEKIT OPENCODE START -->', '<!-- CAFEKIT OPENCODE END -->']
185
+ codex: ['<!-- CAFEKIT CODEX START -->', '<!-- CAFEKIT CODEX END -->']
193
186
  };
194
187
  const topologies = (start, end) => [
195
188
  `user\n${end}\n`,
@@ -227,37 +220,31 @@ test('combined install keeps CORE neutral and records native shared-root trade-o
227
220
  const claude = fs.readFileSync('CLAUDE.md', 'utf8');
228
221
  const core = managedBody(agents, CORE_START, CORE_END);
229
222
  const codex = managedBody(agents, CODEX_START, CODEX_END);
230
- const opencode = managedBody(agents, OPENCODE_START, OPENCODE_END);
231
223
  const claudeBlock = managedBody(claude, START, END);
232
224
 
233
225
  for (const [content, marker] of [
234
226
  [agents, CORE_START],
235
227
  [claude, START],
236
- [agents, CODEX_START],
237
- [agents, OPENCODE_START]
228
+ [agents, CODEX_START]
238
229
  ]) assert.equal((content.match(new RegExp(marker, 'g')) || []).length, 1);
239
230
 
240
- assert.doesNotMatch(core, /Claude|Codex|OpenCode|\.claude|\.codex|\.opencode|\/hapo:|\$hapo-/i);
241
- assert.doesNotMatch(claudeBlock, /Codex|OpenCode|\.codex|\.opencode|\$hapo-/i);
231
+ assert.doesNotMatch(core, /Claude|Codex|\.claude|\.codex|\/hapo:|\$hapo-/i);
232
+ assert.doesNotMatch(claudeBlock, /Codex|\.codex|\$hapo-/i);
242
233
  assert.match(codex, /native project instruction surface is root `AGENTS\.md`/);
243
- assert.match(opencode, /native project instruction surface is root `AGENTS\.md`/);
244
234
  assert.match(agents, /shared-root trade-off is intentional/);
245
235
  // H5 remediation: ownership/ignore contract must be explicit — not just marker presence
246
236
  assert.match(core, /runtime-neutral/i);
247
237
  assert.match(core, /fail-safe/i);
248
238
  assert.match(core, /Ignore managed blocks not owned by your runtime/i);
249
239
  assert.match(codex, /owned by Codex/i);
250
- assert.match(codex, /ignore this entire Codex block/i);
240
+ assert.match(codex, /If you are Claude Code or any other runtime, ignore this entire Codex block/i);
241
+ assert.doesNotMatch(codex, /If you are Codex CLI or any other runtime, ignore this entire Codex block/i);
251
242
  assert.match(codex, /fail-safe/i);
252
- assert.match(opencode, /owned by OpenCode/i);
253
- assert.match(opencode, /ignore this entire OpenCode block/i);
254
- assert.match(opencode, /fail-safe/i);
255
243
  // CORE must not contain runtime-specific directives that would leak
256
244
  assert.doesNotMatch(core, /\$hapo-|hapo:/i);
257
245
  // No cross-runtime directive leakage: Claude block must not contain foreign runtime directives
258
- assert.doesNotMatch(claudeBlock, /<!-- CAFEKIT (CODEX|OPENCODE) /);
259
- assert.doesNotMatch(codex, /<!-- CAFEKIT (CORE|OPENCODE) /);
260
- assert.doesNotMatch(opencode, /<!-- CAFEKIT (CORE|CODEX) /);
246
+ assert.doesNotMatch(claudeBlock, /<!-- CAFEKIT CODEX /);
247
+ assert.doesNotMatch(codex, /<!-- CAFEKIT CORE /);
261
248
 
262
249
  const userAgents = '\n## User-owned combined note\nKeep this exact.\n';
263
250
  const userClaude = '\n## User-owned Claude note\nKeep this exact.\n';
@@ -271,17 +258,15 @@ test('combined install keeps CORE neutral and records native shared-root trade-o
271
258
  assert.match(afterClaude, /User-owned Claude note\nKeep this exact\./);
272
259
  assert.equal((afterAgents.match(new RegExp(CORE_START, 'g')) || []).length, 1);
273
260
  assert.equal((afterAgents.match(new RegExp(CODEX_START, 'g')) || []).length, 1);
274
- assert.equal((afterAgents.match(new RegExp(OPENCODE_START, 'g')) || []).length, 1);
275
261
  assert.equal((afterClaude.match(new RegExp(START, 'g')) || []).length, 1);
276
262
  assert.equal(afterAgents.slice(-userAgents.length), userAgents);
277
263
  assert.equal(afterClaude.slice(-userClaude.length), userClaude);
278
264
  // Ownership contract must survive rerun
279
265
  const afterCore = managedBody(afterAgents, CORE_START, CORE_END);
280
266
  const afterCodex = managedBody(afterAgents, CODEX_START, CODEX_END);
281
- const afterOpenCode = managedBody(afterAgents, OPENCODE_START, OPENCODE_END);
282
267
  assert.match(afterCore, /runtime-neutral/i);
283
- assert.match(afterCodex, /ignore this entire Codex block/i);
284
- assert.match(afterOpenCode, /ignore this entire OpenCode block/i);
268
+ assert.match(afterCodex, /If you are Claude Code or any other runtime, ignore this entire Codex block/i);
269
+ assert.doesNotMatch(afterCodex, /If you are Codex CLI or any other runtime, ignore this entire Codex block/i);
285
270
  });
286
271
  });
287
272
 
@@ -289,7 +274,7 @@ test('combined locale stays in CORE and addressing stays in each native managed
289
274
  inTempProject((root) => {
290
275
  const result = spawnSync(
291
276
  process.execPath,
292
- [INSTALLER, '--platform', 'claude,codex,opencode', '--yes', '--lang', 'vi'],
277
+ [INSTALLER, '--platform', 'claude,codex', '--yes', '--lang', 'vi'],
293
278
  { cwd: root, encoding: 'utf8', env: { ...process.env, PATH: '/usr/bin:/bin' } }
294
279
  );
295
280
  assert.equal(result.status, 0, `${result.stdout}\n${result.stderr}`);
@@ -301,18 +286,17 @@ test('combined locale stays in CORE and addressing stays in each native managed
301
286
  assert.equal((claude.match(/## Language Consistency <!-- cafekit:lang -->/g) || []).length, 0);
302
287
 
303
288
  configureAddressing({
304
- platforms: ['claude', 'codex', 'opencode'],
289
+ platforms: ['claude', 'codex'],
305
290
  ui: { success() {}, warn() {} },
306
291
  t: () => 'updated'
307
292
  }, 'anh');
308
293
 
309
294
  const addressedAgents = fs.readFileSync('AGENTS.md', 'utf8');
310
295
  const addressedClaude = fs.readFileSync('CLAUDE.md', 'utf8');
311
- assert.equal((addressedAgents.match(/## Addressing \(Context Overflow Indicator\)/g) || []).length, 2);
296
+ assert.equal((addressedAgents.match(/## Addressing \(Context Overflow Indicator\)/g) || []).length, 1);
312
297
  assert.equal((addressedClaude.match(/## Addressing \(Context Overflow Indicator\)/g) || []).length, 1);
313
298
  assert.equal((managedBody(addressedAgents, CORE_START, CORE_END).match(/## Addressing \(Context Overflow Indicator\)/g) || []).length, 0);
314
299
  assert.match(managedBody(addressedAgents, CODEX_START, CODEX_END), /Codex CLI always addresses the user as "anh"/);
315
- assert.match(managedBody(addressedAgents, OPENCODE_START, OPENCODE_END), /OpenCode always addresses the user as "anh"/);
316
300
  assert.match(managedBody(addressedClaude, START, END), /Claude Code always addresses the user as "anh"/);
317
301
  });
318
302
  });
@@ -359,11 +343,10 @@ test('fresh no-lang install leaves locale unset and rules hook silent', () => {
359
343
  });
360
344
  });
361
345
 
362
- test('obsolete agent pruning is ownership-aware across Claude, Codex, and OpenCode', () => {
346
+ test('obsolete agent pruning is ownership-aware across Claude and Codex', () => {
363
347
  const cases = {
364
348
  claude: ['agents/god-developer.md', '.claude'],
365
- codex: ['agents/god_developer.toml', '.codex'],
366
- opencode: ['agents/god-developer.md', '.opencode']
349
+ codex: ['agents/god_developer.toml', '.codex']
367
350
  };
368
351
  for (const [platformKey, [relative, folder]] of Object.entries(cases)) {
369
352
  for (const state of ['pristine', 'modified', 'untracked']) {
@@ -523,39 +506,7 @@ test('addressing appends when template has no section without entering shared CO
523
506
  });
524
507
  });
525
508
 
526
- test('OpenCode force refresh preserves user and Codex AGENTS blocks', () => {
527
- inTempProject(() => {
528
- const userContent = '# User instructions\n\nKeep this exact.\n';
529
- const codexBlock = '<!-- CAFEKIT CODEX START -->\nCodex managed\n<!-- CAFEKIT CODEX END -->\n';
530
- fs.writeFileSync('AGENTS.md', `${userContent}\n${codexBlock}`);
531
- const results = { copied: 0, updated: 0, skipped: 0, missingDependencies: 0 };
532
-
533
- copyOpenCodeAgentsMdFile('opencode', results, { upgrade: true });
534
-
535
- const content = fs.readFileSync('AGENTS.md', 'utf8');
536
- assert.ok(content.startsWith(userContent));
537
- assert.ok(content.includes(codexBlock.trim()));
538
- assert.equal((content.match(/<!-- CAFEKIT OPENCODE START -->/g) || []).length, 1);
539
- assert.equal(results.updated, 1);
540
- });
541
- });
542
-
543
- test('legacy unmarked OpenCode AGENTS content is wrapped without duplication', () => {
544
- inTempProject(() => {
545
- const source = path.join(__dirname, '../../src/opencode/AGENTS.md');
546
- const legacy = normalizeOpenCodeBody(fs.readFileSync(source, 'utf8'));
547
- const codexBlock = '<!-- CAFEKIT CODEX START -->\nCodex managed\n<!-- CAFEKIT CODEX END -->\n';
548
- fs.writeFileSync('AGENTS.md', `${legacy}\n${codexBlock}`);
549
- const results = { copied: 0, updated: 0, skipped: 0, missingDependencies: 0 };
550
-
551
- copyOpenCodeAgentsMdFile('opencode', results, { upgrade: true });
552
509
 
553
- const content = fs.readFileSync('AGENTS.md', 'utf8');
554
- assert.equal((content.match(/<!-- CAFEKIT OPENCODE START -->/g) || []).length, 1);
555
- assert.ok(content.includes('## OpenCode Runtime Mapping'));
556
- assert.ok(content.includes(codexBlock.trim()));
557
- });
558
- });
559
510
 
560
511
  test('locale patch does not claim ownership of a preserved user runtime', () => {
561
512
  inTempProject(() => {
@@ -737,9 +688,7 @@ test('managed Claude Addressing survives force refresh when template drops it',
737
688
 
738
689
  test('managed Codex block Addressing survives upsert with the current template', () => {
739
690
  inTempProject(() => {
740
- const codexTemplate = normalizeCodexBody(
741
- fs.readFileSync(path.join(__dirname, '../../src/codex/AGENTS.md'), 'utf8')
742
- );
691
+ const codexTemplate = fs.readFileSync(path.join(__dirname, '../../src/codex/AGENTS.md'), 'utf8');
743
692
  const userTop = '# User codex notes\n\n';
744
693
  const userBottom = '\n\n## User section\nKeep me.\n';
745
694
  const configuredAddressing = '## Addressing (Context Overflow Indicator)\n\nThe AI always addresses the user as "bro" throughout the conversation.';
@@ -757,28 +706,6 @@ test('managed Codex block Addressing survives upsert with the current template',
757
706
  });
758
707
  });
759
708
 
760
- test('managed OpenCode block Addressing survives force refresh with the current template', () => {
761
- inTempProject(() => {
762
- const userContent = '# User instructions\n\nKeep this exact.\n';
763
- const codexBlock = '<!-- CAFEKIT CODEX START -->\nCodex managed\n<!-- CAFEKIT CODEX END -->\n';
764
- const configuredAddressing = '## Addressing (Context Overflow Indicator)\n\nThe AI always addresses the user as "anh" throughout the conversation.';
765
- fs.writeFileSync(
766
- 'AGENTS.md',
767
- `${userContent}<!-- CAFEKIT OPENCODE START -->\nOpenCode runtime line.\n\n${configuredAddressing}\n<!-- CAFEKIT OPENCODE END -->\n${codexBlock}`
768
- );
769
- const results = { copied: 0, updated: 0, skipped: 0, missingDependencies: 0 };
770
-
771
- copyOpenCodeAgentsMdFile('opencode', results, { upgrade: true });
772
-
773
- const content = fs.readFileSync('AGENTS.md', 'utf8');
774
- assert.ok(content.startsWith(userContent));
775
- assert.ok(content.includes(codexBlock.trim()));
776
- assert.equal((content.match(/## Addressing \(Context Overflow Indicator\)/g) || []).length, 1);
777
- assert.ok(content.includes('The AI always addresses the user as "anh"'));
778
- assert.equal((content.match(/<!-- CAFEKIT OPENCODE START -->/g) || []).length, 1);
779
- assert.equal(results.updated, 1);
780
- });
781
- });
782
709
 
783
710
  test('copyRecursive skips generated artifacts while copying normal files', () => {
784
711
  inTempProject(() => {
@@ -799,65 +726,3 @@ test('copyRecursive skips generated artifacts while copying normal files', () =>
799
726
  assert.equal(fs.existsSync('dest/extra/bundle.pyo'), false);
800
727
  });
801
728
  });
802
-
803
- test('OpenCode gate uses a real before-hook block and documents the host boundary', () => {
804
- const opencodeAgents = fs.readFileSync(path.join(__dirname, '../../src/opencode/AGENTS.md'), 'utf8');
805
- const assertOpenCodeBoundary = (content) => {
806
- assert.match(content, /tool\.execute\.before/);
807
- assert.match(content, /hard-block supported completion\/state tools/);
808
- assert.match(content, /`task`, `taskupdate`, and `todowrite`/);
809
- assert.match(content, /CAFEKIT_SPEC_GATE_BLOCKED/);
810
- assert.match(content, /`session\.idle`\/Stop/);
811
- assert.match(content, /final assistant-turn cancellation boundary/);
812
- assert.match(content, /observational only/);
813
- assert.match(content, /full-turn parity with Claude\/Codex/);
814
- assert.doesNotMatch(content, /tool\.execute\.after/);
815
- assert.doesNotMatch(content, /best-effort/);
816
- assert.doesNotMatch(content, /tier-2/);
817
- assert.doesNotMatch(content, /\badvisory\b/i);
818
- };
819
- assertOpenCodeBoundary(opencodeAgents);
820
-
821
- const pluginPath = path.join(__dirname, '../../src/opencode/plugins/spec-gate.ts');
822
- assert.equal(fs.existsSync(pluginPath), true, 'OpenCode spec-gate plugin must exist');
823
- const plugin = fs.readFileSync(pluginPath, 'utf8');
824
- assert.match(plugin, /completion_gate/);
825
- assert.match(plugin, /tool\.execute\.before/);
826
- assert.match(plugin, /CAFEKIT_SPEC_GATE_BLOCKED/);
827
- assert.match(plugin, /throw createBlockError/);
828
- assert.match(plugin, /session\.idle/);
829
- assert.match(plugin, /final assistant message[\s\S]*outside this adapter's boundary/);
830
- assert.doesNotMatch(plugin, /advisory/i);
831
- assert.doesNotMatch(plugin, /fail-open/i);
832
- // JSDoc-safe wording: must not contain block-comment-closing pattern specs/*/spec.json
833
- assert.doesNotMatch(plugin, /specs\/\*\/spec\.json/);
834
- // Traversal check must be sibling-prefix safe (resolve + relative, not simple startsWith).
835
- assert.match(plugin, /relative\s*\(\s*resolvedFeatureDir/);
836
- assert.match(plugin, /isAbsolute/);
837
- assert.doesNotMatch(plugin, /if \(!abs\.startsWith\(featureDir\)\)/);
838
- // Single executable authorities: OpenCode delegates both validation and resolution.
839
- assert.match(plugin, /workflow-policy\.cjs/);
840
- assert.match(plugin, /spec-resolver\.cjs/);
841
- assert.match(plugin, /getSharedValidate|validateCanonicalReceipt/);
842
- assert.doesNotMatch(plugin, /const hasBase\s*&&\s*hasHead/);
843
- assert.doesNotMatch(plugin, /PLACEHOLDER_TOKENS/);
844
-
845
- const installerDocs = fs.readFileSync(path.join(__dirname, '../../../../docs/installer-architecture.md'), 'utf8');
846
- assert.doesNotMatch(installerDocs, /OpenCode.*hard.*completion gate/i);
847
-
848
- // Combined install must copy the plugin and both shared authorities.
849
- inTempProject((root) => {
850
- const result = spawnSync(process.execPath, [INSTALLER, '--platform', 'opencode', '--yes'], { cwd: root, encoding: 'utf8', env: { ...process.env, PATH: '/usr/bin:/bin' } });
851
- assert.equal(result.status, 0, `${result.stdout}\n${result.stderr}`);
852
- assert.equal(fs.existsSync(path.join(root, '.opencode/plugins/spec-gate.ts')), true);
853
- assert.equal(fs.existsSync(path.join(root, '.opencode/scripts/workflow-policy.cjs')), true);
854
- assert.equal(fs.existsSync(path.join(root, '.opencode/scripts/spec-resolver.cjs')), true);
855
- const installedAgents = fs.readFileSync(path.join(root, 'AGENTS.md'), 'utf8');
856
- assertOpenCodeBoundary(managedBody(installedAgents, OPENCODE_START, OPENCODE_END));
857
- const installed = fs.readFileSync(path.join(root, '.opencode/plugins/spec-gate.ts'), 'utf8');
858
- assert.match(installed, /tool\.execute\.before/);
859
- assert.match(installed, /CAFEKIT_SPEC_GATE_BLOCKED/);
860
- assert.match(installed, /workflow-policy\.cjs/);
861
- assert.match(installed, /spec-resolver\.cjs/);
862
- });
863
- });