@haposoft/cafekit 0.16.0-rc.5 → 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 +13 -5
  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 +3 -8
  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
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # @haposoft/cafekit
2
2
 
3
- > Native spec-driven workflow and runtime bundle for Claude Code, Codex CLI, and OpenCode.
3
+ > Native spec-driven workflow and runtime bundle for Claude Code and Codex CLI.
4
4
 
5
5
  [![Version](https://img.shields.io/badge/version-0.16.0-blue.svg)](https://github.com/haposoft/cafekit)
6
6
  [![License](https://img.shields.io/badge/license-MIT-green.svg)](LICENSE)
@@ -28,14 +28,6 @@ Codex CLI install support:
28
28
  - merges a CafeKit-owned block into root `AGENTS.md`
29
29
  - requires no generated `.codex/config.toml` and never changes global trust config
30
30
 
31
- OpenCode install support:
32
- - installs command wrappers under `.opencode/commands/`
33
- - installs converted OpenCode agents under `.opencode/agents/`
34
- - installs CafeKit skills under `.opencode/skills/`
35
- - installs OpenCode plugin runtime files under `.opencode/plugins/`
36
- - merges `opencode.json` and root `AGENTS.md`
37
- - omits the Claude `hapo:` command prefix
38
-
39
31
  ## Install
40
32
 
41
33
  Run in your project root:
@@ -59,7 +51,7 @@ npx @haposoft/cafekit --platform codex --force-overwrite
59
51
 
60
52
  Requirements:
61
53
  - Node.js 18+
62
- - Claude Code, Codex CLI, or OpenCode; choose a runtime when prompted or use `--platform`
54
+ - Claude Code or Codex CLI; choose a runtime when prompted or use `--platform`
63
55
 
64
56
  ## Git ignore policy
65
57
 
@@ -75,15 +67,13 @@ plans/*
75
67
  .cafekit-backup/
76
68
  .cafekit.lock
77
69
  .claude/
78
- .opencode/
79
70
  .codex/
80
71
  .agents/
81
72
  ```
82
73
 
83
74
  Runtime folders are local — reinstall with `npx @haposoft/cafekit` rather than
84
- committing them. Inside the runtime, CafeKit also installs `.claude/.gitignore`,
85
- `.opencode/.gitignore`, or the Codex pair `.codex/.gitignore` +
86
- `.agents/.gitignore`. These keep secrets, skill dependencies, session state,
75
+ committing them. Inside the runtime, CafeKit also installs `.claude/.gitignore` or the
76
+ Codex pair `.codex/.gitignore` + `.agents/.gitignore`. These keep secrets, skill dependencies, session state,
87
77
  and hook logs out of git even if someone partially un-ignores a runtime.
88
78
  Teams may deliberately commit runtime files, but should also commit the
89
79
  ownership manifest so selective updates share the same baseline.
@@ -141,37 +131,11 @@ without requiring Git and remain stable when a session starts in a subdirectory.
141
131
  CafeKit uses Codex's native status and usage UI instead of installing the
142
132
  Claude statusline.
143
133
 
144
- OpenCode targets:
145
-
146
- ```text
147
- .opencode/
148
- ├── .gitignore
149
- ├── commands/
150
- ├── agents/
151
- ├── skills/
152
- ├── rules/
153
- ├── scripts/
154
- ├── references/
155
- ├── plugins/
156
- ├── cafekit.json
157
- ├── runtime.json
158
- └── package.json
159
-
160
- AGENTS.md
161
- opencode.json
162
- ```
163
-
164
- OpenCode setup also:
165
- - binds generated commands to matching CafeKit subagents with `agent` + `subtask`
166
- - configures `permission.skill` and `permission.task`
167
- - optionally writes `model` to `opencode.json` from `OPENCODE_MODEL`, `OPENCODE_DEFAULT_MODEL`, or installer input
168
-
169
134
  To check the installed CafeKit package version:
170
135
 
171
136
  ```bash
172
137
  cat .claude/cafekit.json
173
138
  cat .codex/cafekit.json
174
- cat .opencode/cafekit.json
175
139
  ```
176
140
 
177
141
  ## Core Skills
@@ -224,17 +188,6 @@ $hapo-test meet-transcript-mvp --full
224
188
  $hapo-code-review meet-transcript-mvp --pending
225
189
  ```
226
190
 
227
- OpenCode uses the generated command names without the Claude `hapo:` prefix:
228
-
229
- ```bash
230
- /question "Which files define the current CafeKit install/runtime behavior?" --repo
231
- /brainstorm Explore approaches for a Google Meet transcript extension
232
- /specs Build a Google Meet transcript extension with AI summaries
233
- /develop meet-transcript-mvp
234
- /test --full
235
- /code-review --pending
236
- ```
237
-
238
191
  Reconstruct as-is docs from a legacy codebase:
239
192
 
240
193
  ```bash
@@ -253,16 +206,26 @@ hosted or specialized tools may not enter the local hook path.
253
206
 
254
207
  ## Spec Output
255
208
 
256
- Specs are stored under:
209
+ New process-first Specs packets are stored under:
257
210
 
258
211
  ```text
259
212
  specs/<feature-name>/
260
- ├── spec.json
261
- ├── requirements.md
262
- ├── research.md
263
- ├── design.md
264
- └── tasks/task-R*.md
265
- ```
213
+ ├── plan.md
214
+ ├── task-01-<slug>.md
215
+ └── task-NN-<slug>.md
216
+ ```
217
+
218
+ The process-first workflow has three human decision gates. `hapo:specs` opens
219
+ C1 to fix scope before authoring and C2 to resolve adversarial findings, then
220
+ stops. A later explicit `hapo:develop` invocation executes the tasks and
221
+ presents C3 for closeout only after real execution proof. Each task has one
222
+ `Status:` field and keeps its canonical `## Receipt` inline, including the
223
+ exact Verification Plan command, `Exit: 0`, `Verification: PASS`,
224
+ runtime-derived Base and Head, and current command output.
225
+
226
+ Existing packets with `spec.json`, nested `tasks/task-R*.md`, or separate
227
+ receipts remain supported through the legacy compatibility adapter. New Specs
228
+ work does not author those legacy artifacts.
266
229
 
267
230
  ## Development
268
231
 
@@ -66,7 +66,7 @@ function initRepo(root, { version = '0.0.1-fixture' } = {}) {
66
66
  }
67
67
 
68
68
  function writeLegacyBridge(root, { baseCommit, semanticDigest = cf.sha256Tag('bridge-digest') }) {
69
- writeJson(path.join(root, 'specs/cafekit-semantic-eval-firewall/reports/bootstrap-legacy-bridge-review.json'), {
69
+ writeJson(path.join(root, 'specs/archive/cafekit-semantic-eval-firewall/reports/bootstrap-legacy-bridge-review.json'), {
70
70
  schema_version: '1',
71
71
  verdict: 'PASS',
72
72
  blocking_count: 0,
@@ -78,7 +78,7 @@ function writeLegacyBridge(root, { baseCommit, semanticDigest = cf.sha256Tag('br
78
78
  }
79
79
 
80
80
  function writeFeatureSeedPass(root, { reviewReceiptDigest = cf.sha256Tag('seed-review-receipt') } = {}) {
81
- writeJson(path.join(root, 'specs/cafekit-semantic-eval-firewall/spec.json'), {
81
+ writeJson(path.join(root, 'specs/archive/cafekit-semantic-eval-firewall/spec.json'), {
82
82
  validation: {
83
83
  semantic_review_history: {
84
84
  entries: [{
@@ -201,7 +201,7 @@ test('R1.14 negative: an absent or malformed legacy-bridge artifact fails the pr
201
201
  const inst = cf.createChangeFirewall({ root });
202
202
  assert.throws(() => inst.assertLegacyBridgeArtifact(), (error) => error.code === 'legacy_bridge_missing');
203
203
 
204
- writeJson(path.join(root, 'specs/cafekit-semantic-eval-firewall/reports/bootstrap-legacy-bridge-review.json'), {
204
+ writeJson(path.join(root, 'specs/archive/cafekit-semantic-eval-firewall/reports/bootstrap-legacy-bridge-review.json'), {
205
205
  schema_version: '1', verdict: 'FAIL', blocking_count: 0,
206
206
  reviewed_criteria: ['R1.1'], semantic_digest: cf.sha256Tag('x'),
207
207
  bootWindowBaseCommit: '0'.repeat(40), written_at: new Date().toISOString(),
@@ -379,7 +379,7 @@ test('C14/R1.8: bootstrapBaseline refuses before the seed PASS exists, and refus
379
379
  const inst = cf.createChangeFirewall({ root });
380
380
 
381
381
  // feature spec.json exists but carries no semantic_review_history yet (no seed PASS).
382
- writeJson(path.join(root, 'specs/cafekit-semantic-eval-firewall/spec.json'), { validation: {} });
382
+ writeJson(path.join(root, 'specs/archive/cafekit-semantic-eval-firewall/spec.json'), { validation: {} });
383
383
  assert.throws(
384
384
  () => inst.bootstrapBaseline({ baseCommit, headCommit, bootstrapReviewDigest: cf.sha256Tag('x'), packageVersion: '0.0.1-fixture' }),
385
385
  (error) => error.code === 'seed_pass_missing',
@@ -2131,4 +2131,3 @@ test('REGRESSION round7 (critical): a committed C10 authority renamed away then
2131
2131
  assert.throws(() => inst.createFreezeManifest(null), (error) => error.code === 'out_of_band_edit');
2132
2132
  });
2133
2133
  });
2134
-
@@ -45,6 +45,16 @@ function bindReceipt(root, value, feature = 'auth', session = 'session-a') {
45
45
  return value.replaceAll(VALID_BASE, context.base).replaceAll(VALID_HEAD, context.head);
46
46
  }
47
47
 
48
+ function workflowRuntimeContext(root, feature = 'auth', session = 'session-a') {
49
+ return PROVENANCE.deriveRuntimeContext({
50
+ projectRoot: root,
51
+ specsRoot: path.join(root, 'specs'),
52
+ specFile: path.join(root, 'specs', feature, 'plan.md'),
53
+ featureName: feature,
54
+ runtimeSession: session,
55
+ });
56
+ }
57
+
48
58
  function installFeatureReceipts(root, session = 'session-a') {
49
59
  const specsRoot = path.join(root, 'specs');
50
60
  if (!fs.existsSync(specsRoot)) return;
@@ -414,6 +424,133 @@ test('Codex completion gate cannot be bypassed from a nested cwd', () => {
414
424
  });
415
425
  });
416
426
 
427
+ test('Codex completion gate validates explicit process-v3 inline Receipt', () => {
428
+ inHookFixture((root, hooks) => {
429
+ const nested = path.join(root, 'packages', 'app');
430
+ const feature = path.join(root, 'specs', 'auth');
431
+ const taskFile = path.join(feature, 'task-01-auth.md');
432
+ fs.mkdirSync(nested, { recursive: true });
433
+ fs.mkdirSync(feature, { recursive: true });
434
+ fs.writeFileSync(path.join(feature, 'plan.md'), '# Auth plan\n');
435
+ fs.writeFileSync(taskFile, [
436
+ '# Task 01: auth', '', 'Status: done', '',
437
+ '## Verification Plan', '', '- Command: node --test', '',
438
+ ].join('\n'));
439
+
440
+ const gate = path.join(hooks, 'spec-gate.cjs');
441
+ const payload = {
442
+ cwd: nested,
443
+ featureName: 'auth',
444
+ session_id: 'session-a',
445
+ hook_event_name: 'Stop',
446
+ stop_hook_active: false,
447
+ };
448
+ const blocked = runHook(gate, nested, payload);
449
+ assert.equal(blocked.status, 0, blocked.stderr);
450
+ assert.match(JSON.parse(blocked.stdout).reason, /missing_receipt/);
451
+
452
+ const context = workflowRuntimeContext(root);
453
+ fs.writeFileSync(taskFile, [
454
+ '# Task 01: auth', '', 'Status: done', '',
455
+ '## Verification Plan', '', '- Command: node --test', '',
456
+ '## Receipt', '',
457
+ 'Verification: PASS', 'Command: node --test', 'Exit: 0',
458
+ `Base: ${context.base}`, `Head: ${context.head}`,
459
+ '```text', '$ node --test', 'pass: 1', '```', '',
460
+ ].join('\n'));
461
+ const passed = runHook(gate, nested, payload);
462
+ assert.equal(passed.status, 0, passed.stderr);
463
+ assert.equal(passed.stdout, '');
464
+ });
465
+ });
466
+
467
+ test('Codex process-v3 Receipt command must match the exact Verification Plan command', () => {
468
+ inHookFixture((root, hooks) => {
469
+ const feature = path.join(root, 'specs', 'auth');
470
+ fs.mkdirSync(feature, { recursive: true });
471
+ fs.writeFileSync(path.join(feature, 'plan.md'), '# Auth plan\n');
472
+ const context = workflowRuntimeContext(root);
473
+ fs.writeFileSync(path.join(feature, 'task-01-auth.md'), [
474
+ '# Task 01: auth', '', 'Status: done', '',
475
+ '## Verification Plan', '', '- Command: pnpm test', '',
476
+ '## Receipt', '', 'Verification: PASS', 'Command: true', 'Exit: 0',
477
+ `Base: ${context.base}`, `Head: ${context.head}`,
478
+ '```text', '$ true', 'pass: 1', '```', '',
479
+ ].join('\n'));
480
+
481
+ const result = runHook(path.join(hooks, 'spec-gate.cjs'), root, {
482
+ cwd: root,
483
+ session_id: 'session-a',
484
+ hook_event_name: 'Stop',
485
+ stop_hook_active: false,
486
+ });
487
+ assert.equal(result.status, 0, result.stderr);
488
+ assert.notEqual(result.stdout, '', 'a substituted receipt command must block');
489
+ assert.match(JSON.parse(result.stdout).reason, /command_identity/);
490
+ });
491
+ });
492
+
493
+ test('Codex process-v3 Stop ignores a completed packet when one packet remains active', () => {
494
+ inHookFixture((root, hooks) => {
495
+ const completed = path.join(root, 'specs', 'auth');
496
+ const active = path.join(root, 'specs', 'active');
497
+ fs.mkdirSync(completed, { recursive: true });
498
+ fs.mkdirSync(active, { recursive: true });
499
+ fs.writeFileSync(path.join(completed, 'plan.md'), '# Auth plan\n');
500
+ fs.writeFileSync(path.join(active, 'plan.md'), '# Active plan\n');
501
+ const context = workflowRuntimeContext(root);
502
+ fs.writeFileSync(path.join(completed, 'task-01-auth.md'), [
503
+ '# Task 01: auth', '', 'Status: done', '',
504
+ '## Verification Plan', '', '- Command: node --test', '',
505
+ '## Receipt', '',
506
+ 'Verification: PASS', 'Command: node --test', 'Exit: 0',
507
+ `Base: ${context.base}`, `Head: ${context.head}`,
508
+ '```text', '$ node --test', 'pass: 1', '```', '',
509
+ ].join('\n'));
510
+ fs.writeFileSync(path.join(active, 'task-01-active.md'), [
511
+ '# Task 01: active', '', 'Status: pending', '',
512
+ '## Verification Plan', '', '- Command: node --test', '',
513
+ ].join('\n'));
514
+
515
+ const result = runHook(path.join(hooks, 'spec-gate.cjs'), root, {
516
+ cwd: root,
517
+ session_id: 'session-a',
518
+ hook_event_name: 'Stop',
519
+ stop_hook_active: false,
520
+ });
521
+ assert.equal(result.status, 0, result.stderr);
522
+ assert.equal(result.stdout, '');
523
+ });
524
+ });
525
+
526
+ test('Codex process-v3 Stop accepts two completed packets with valid Receipts', () => {
527
+ inHookFixture((root, hooks) => {
528
+ for (const featureName of ['auth', 'other']) {
529
+ const feature = path.join(root, 'specs', featureName);
530
+ fs.mkdirSync(feature, { recursive: true });
531
+ fs.writeFileSync(path.join(feature, 'plan.md'), `# ${featureName} plan\n`);
532
+ const context = workflowRuntimeContext(root, featureName);
533
+ fs.writeFileSync(path.join(feature, `task-01-${featureName}.md`), [
534
+ `# Task 01: ${featureName}`, '', 'Status: done', '',
535
+ '## Verification Plan', '', '- Command: node --test', '',
536
+ '## Receipt', '',
537
+ 'Verification: PASS', 'Command: node --test', 'Exit: 0',
538
+ `Base: ${context.base}`, `Head: ${context.head}`,
539
+ '```text', '$ node --test', 'pass: 1', '```', '',
540
+ ].join('\n'));
541
+ }
542
+
543
+ const result = runHook(path.join(hooks, 'spec-gate.cjs'), root, {
544
+ cwd: root,
545
+ session_id: 'session-a',
546
+ hook_event_name: 'Stop',
547
+ stop_hook_active: false,
548
+ });
549
+ assert.equal(result.status, 0, result.stderr);
550
+ assert.equal(result.stdout, '');
551
+ });
552
+ });
553
+
417
554
  test('empty or forged Codex hook authority fails closed', () => {
418
555
  inHookFixture((root, hooks) => {
419
556
  const gate = path.join(hooks, 'spec-gate.cjs');
@@ -28,6 +28,35 @@ const INSTALLER = path.join(PACKAGE_ROOT, 'bin/install.js');
28
28
  const MANIFEST = JSON.parse(
29
29
  fs.readFileSync(path.join(PACKAGE_ROOT, 'src/claude/migration-manifest.json'), 'utf8')
30
30
  );
31
+ const SPECS_SOURCE_ROOT = path.join(PACKAGE_ROOT, 'src/claude/skills/specs');
32
+ const V3_SPECS_BUNDLE = [
33
+ 'SKILL.md',
34
+ 'references/review.md',
35
+ 'references/templates.md',
36
+ 'templates/design.md',
37
+ 'templates/requirements-init.md',
38
+ 'templates/requirements.md',
39
+ 'templates/research.md',
40
+ 'templates/spec-state.json',
41
+ 'templates/task.md'
42
+ ];
43
+ const OBSOLETE_SPECS_FILES = [
44
+ 'references/archive-workflow.md',
45
+ 'references/ask-user-question-gates.md',
46
+ 'references/codebase-analysis.md',
47
+ 'references/cross-spec-dependency.md',
48
+ 'references/research-strategy.md',
49
+ 'references/scope-inquiry.md',
50
+ 'references/translation-mirror.md',
51
+ 'rules/design-discovery-full.md',
52
+ 'rules/design-discovery-light.md',
53
+ 'rules/design-principles.md',
54
+ 'rules/design-review.md',
55
+ 'rules/ears-format.md',
56
+ 'rules/phase-decision-matrix.md',
57
+ 'rules/task-scoring-rubric.md',
58
+ 'rules/tasks-generation.md'
59
+ ];
31
60
 
32
61
  function inTempProject(run) {
33
62
  const root = fs.mkdtempSync(path.join(os.tmpdir(), 'cafekit-codex-native-'));
@@ -130,91 +159,99 @@ function assertInstalledVerificationModel(grounderPath, designTemplate) {
130
159
  }
131
160
  }
132
161
 
133
- function artifactRouterRecords(skill) {
134
- const lines = skill.split('\n');
135
- const heading = lines.findIndex((line) => line.trim() === '## Artifact router');
136
- const start = lines.findIndex((line, index) => index > heading && line.trim().startsWith('|'));
137
- const rows = [];
138
- for (let index = start; index >= 0 && index < lines.length && lines[index].trim().startsWith('|'); index += 1) {
139
- const cells = lines[index].split('|').slice(1, -1).map((cell) => cell.trim());
140
- if (!cells.every((cell) => /^:?-+:?$/.test(cell))) rows.push(cells);
141
- }
142
- const header = rows.shift() || [];
143
- return new Map(rows.map((cells) => [
144
- (cells[0] || '').replaceAll('`', ''),
145
- Object.fromEntries(header.map((name, index) => [name, cells[index] || '']))
146
- ]));
147
- }
148
-
149
162
  function installedAuthoringProjectionIssues(files) {
150
163
  const issues = [];
151
- const routes = artifactRouterRecords(files.skill);
152
- const none = routes.get('None');
153
- if (!none || none['Durable core'] !== 'none'
154
- || none['Research route'] !== 'absent' || none['Task route'] !== 'absent') {
155
- issues.push('none-route');
164
+ const foreignRuntimeBoundary = 'If you are Claude Code or any other runtime, ignore this entire Codex block';
165
+ if (!files.skill.includes('Task files are flat beside `plan.md`')
166
+ || !files.skill.includes('Do not create a nested task directory.')) {
167
+ issues.push('flat-layout-entrypoint');
156
168
  }
157
- for (const depth of ['Compact', 'Full']) {
158
- const route = routes.get(depth);
159
- if (!route || !['spec.json', 'requirements.md', 'design.md'].every((name) => (
160
- route['Durable core'].includes(`\`${name}\``)
161
- ))) issues.push(`${depth.toLowerCase()}-core`);
162
- if (!route?.['Research route'].includes('research trigger')) issues.push(`${depth.toLowerCase()}-research`);
163
- if (!route?.['Task route'].includes('typed topology trigger')) issues.push(`${depth.toLowerCase()}-tasks`);
169
+ if (!files.templates.includes('The primary layout is always flat:')
170
+ || !files.templates.includes('task-NN-<slug>.md')) {
171
+ issues.push('flat-layout-template');
164
172
  }
165
- if (/\balways search\b|\bcomprehensive research\b|\bcapture all findings\b/i.test(files.discovery)) {
166
- issues.push('default-research');
173
+ for (const gate of ['C1 — Scope', 'C2 — Findings', 'C3 — Done']) {
174
+ if (!files.skill.includes(gate)) issues.push(`human-gate-${gate.slice(0, 2).toLowerCase()}`);
167
175
  }
168
- if (/(?:limit|maximum|at most|top|≤|<=)\s*(?:to\s*)?3\b/i.test(files.review)) {
169
- issues.push('review-cap');
176
+ if (!files.review.includes('fresh-context red team')
177
+ || !/Cap the presented\s+list at 15/.test(files.review)
178
+ || !files.review.includes('at most two review-and-repair rounds')) {
179
+ issues.push('adversarial-review');
170
180
  }
171
- if (!/For every `RN\.M`/.test(files.review)) issues.push('criterion-coverage');
172
- if (/\bcurrent_phase\b/.test(files.codex)) issues.push('phase-alias');
173
- for (const state of ['in_progress', 'paused', 'blocked', 'done']) {
174
- if (!files.codex.includes(`\`${state}\``)) issues.push(`lifecycle-${state}`);
181
+ if (!files.skill.includes('inline `## Receipt`')
182
+ || !files.templates.includes('## Canonical inline Receipt')) {
183
+ issues.push('inline-receipt');
184
+ }
185
+ for (const field of [
186
+ 'Verification: PASS', 'Command:', 'Exit: 0', 'Base:', 'Head:'
187
+ ]) {
188
+ if (!files.templates.includes(field)) issues.push(`receipt-${field.toLowerCase()}`);
189
+ }
190
+ if (!/^name: hapo-specs$/m.test(files.skill)) issues.push('codex-skill-name');
191
+ const bundle = [files.skill, files.review, files.templates, files.legacyTemplates].join('\n');
192
+ const runtimeProjection = `${bundle}\n${files.codex.replace(foreignRuntimeBoundary, '')}`;
193
+ if (!files.codex.includes(foreignRuntimeBoundary)
194
+ || files.codex.includes('If you are Codex CLI or any other runtime, ignore this entire Codex block')) {
195
+ issues.push('codex-ownership-boundary');
175
196
  }
176
- if (/^##\s+Evidence\s*$/m.test(files.tasks)) issues.push('task-evidence');
177
- if (/\(P\)/.test(files.tasks)) issues.push('task-priority-marker');
178
197
  for (const claudeOnly of [
179
198
  'AskUserQuestion', 'TaskCreate', 'TaskGet', 'TaskUpdate', 'TaskList',
180
- 'WebSearch', 'WebFetch', 'SendMessage'
199
+ 'WebSearch', 'WebFetch', 'SendMessage', 'Claude Code', '.claude', '/hapo:', 'hapo:'
181
200
  ]) {
182
- if (files.codex.includes(claudeOnly)) issues.push(`claude-tool-${claudeOnly}`);
201
+ if (runtimeProjection.includes(claudeOnly)) issues.push(`claude-vocabulary-${claudeOnly}`);
202
+ }
203
+ for (const command of ['$hapo-specs', '$hapo-develop', '$hapo-sync']) {
204
+ if (!files.codex.includes(command)) issues.push(`codex-command-${command}`);
205
+ }
206
+ for (const state of ['pending', 'in_progress', 'paused', 'blocked', 'done']) {
207
+ if (!files.codex.includes(`\`${state}\``)) issues.push(`lifecycle-${state}`);
183
208
  }
184
- if (!files.codex.includes('semantic_model')
185
- || !files.codex.includes('explicit installed machine semantic-sync step')
186
- || !files.codex.includes('round-trip')
187
- || !files.codex.includes('node .codex/scripts/spec-scaffold.cjs <feature> --sync-semantic-model')) {
188
- issues.push('semantic-model-promotion');
209
+ if (!files.codex.includes('flat `task-NN-*.md` files')
210
+ || !files.codex.includes('inline `## Receipt`')
211
+ || !files.codex.includes('C1') || !files.codex.includes('C2') || !files.codex.includes('C3')) {
212
+ issues.push('codex-process-v3');
189
213
  }
190
214
  return issues;
191
215
  }
192
216
 
193
217
  function assertInstalledAuthoringProjection(root) {
194
- const read = (relative) => fs.readFileSync(path.join(root, relative), 'utf8');
218
+ const installedRoot = path.join(root, '.agents/skills/specs');
219
+ const relativeFiles = (directory) => allFiles(directory, () => true)
220
+ .map((file) => path.relative(directory, file).split(path.sep).join('/'))
221
+ .sort();
222
+ assert.deepEqual(relativeFiles(SPECS_SOURCE_ROOT), V3_SPECS_BUNDLE);
223
+ assert.deepEqual(relativeFiles(installedRoot), V3_SPECS_BUNDLE);
224
+
225
+ for (const relative of V3_SPECS_BUNDLE) {
226
+ const sourcePath = path.join(SPECS_SOURCE_ROOT, relative);
227
+ const expected = normalizeCodexBody(fs.readFileSync(sourcePath, 'utf8'), sourcePath);
228
+ const actual = fs.readFileSync(path.join(installedRoot, relative), 'utf8');
229
+ assert.equal(actual, expected, `Codex Specs projection drifted: ${relative}`);
230
+ }
231
+ for (const relative of OBSOLETE_SPECS_FILES) {
232
+ assert.equal(fs.existsSync(path.join(installedRoot, relative)), false, `obsolete Specs file installed: ${relative}`);
233
+ }
234
+
235
+ const read = (relative) => fs.readFileSync(path.join(installedRoot, relative), 'utf8');
195
236
  const files = {
196
- skill: read('.agents/skills/specs/SKILL.md'),
197
- discovery: [
198
- read('.agents/skills/specs/rules/design-discovery-light.md'),
199
- read('.agents/skills/specs/rules/design-discovery-full.md')
200
- ].join('\n'),
201
- review: [
202
- read('.agents/skills/specs/rules/design-review.md'),
203
- read('.agents/skills/specs/references/review.md')
204
- ].join('\n'),
205
- tasks: [
206
- read('.agents/skills/specs/rules/tasks-generation.md'),
207
- read('.agents/skills/specs/rules/task-scoring-rubric.md'),
208
- read('.agents/skills/specs/rules/phase-decision-matrix.md')
209
- ].join('\n'),
210
- codex: [read('AGENTS.md'), read('.codex/rules/state-sync.md')].join('\n')
237
+ skill: read('SKILL.md'),
238
+ review: read('references/review.md'),
239
+ templates: read('references/templates.md'),
240
+ legacyTemplates: V3_SPECS_BUNDLE
241
+ .filter((relative) => relative.startsWith('templates/'))
242
+ .map(read)
243
+ .join('\n'),
244
+ codex: [
245
+ fs.readFileSync(path.join(root, 'AGENTS.md'), 'utf8'),
246
+ fs.readFileSync(path.join(root, '.codex/rules/state-sync.md'), 'utf8')
247
+ ].join('\n')
211
248
  };
212
249
  assert.deepEqual(installedAuthoringProjectionIssues(files), []);
213
250
  const mutations = [
214
- ['discovery', (value) => `${value}\nAlways search before design.`],
215
- ['review', (value) => `${value}\nLimit to 3 blockers.`],
216
- ['codex', (value) => `${value}\nSet current_phase then call TaskUpdate.`],
217
- ['tasks', (value) => `${value}\n## Evidence\nUse (P).`]
251
+ ['skill', (value) => value.replace('Task files are flat beside `plan.md`', 'Task files may be nested')],
252
+ ['review', (value) => value.replace('list at 15', 'list without a cap')],
253
+ ['templates', (value) => value.replace('Verification: PASS', 'Verification: UNKNOWN')],
254
+ ['codex', (value) => `${value}\nUse TaskUpdate for state changes.`]
218
255
  ];
219
256
  for (const [key, mutate] of mutations) {
220
257
  const changed = { ...files, [key]: mutate(files[key]) };
@@ -271,7 +308,7 @@ test('platform resolver keeps saved runtimes and newly detected Codex', async ()
271
308
  const root = fs.mkdtempSync(path.join(os.tmpdir(), 'cafekit-codex-detect-'));
272
309
  const originalCwd = process.cwd();
273
310
  try {
274
- for (const folder of ['.claude', '.opencode', '.codex']) {
311
+ for (const folder of ['.claude', '.codex']) {
275
312
  fs.mkdirSync(path.join(root, folder), { recursive: true });
276
313
  }
277
314
  fs.writeFileSync(
@@ -287,7 +324,7 @@ test('platform resolver keeps saved runtimes and newly detected Codex', async ()
287
324
  t: (key) => key
288
325
  };
289
326
  await resolvePlatforms(ctx);
290
- assert.deepEqual(ctx.platforms, ['claude', 'opencode', 'codex']);
327
+ assert.deepEqual(ctx.platforms, ['claude', 'codex']);
291
328
  } finally {
292
329
  process.chdir(originalCwd);
293
330
  fs.rmSync(root, { recursive: true, force: true });
@@ -645,7 +682,7 @@ test('Codex Windows hook launchers stay project-bound without Git from nested cw
645
682
  });
646
683
  });
647
684
 
648
- test('Codex install is project-local, native, and upgrade-safe', () => {
685
+ test('Codex fresh install is exact while refresh and upgrade preserve existing files', () => {
649
686
  inTempProject((root) => {
650
687
  const userInstructions = '# User rules\n\nKeep this exact.\n';
651
688
  fs.writeFileSync(path.join(root, 'AGENTS.md'), userInstructions);
@@ -653,7 +690,6 @@ test('Codex install is project-local, native, and upgrade-safe', () => {
653
690
  const first = install(root);
654
691
  assert.equal(first.status, 0, `${first.stdout}\n${first.stderr}`);
655
692
  assert.equal(fs.existsSync(path.join(root, '.claude')), false);
656
- assert.equal(fs.existsSync(path.join(root, '.opencode')), false);
657
693
  assert.equal(fs.existsSync(path.join(root, '.codex', 'config.toml')), false);
658
694
 
659
695
  for (const relative of [
@@ -666,10 +702,7 @@ test('Codex install is project-local, native, and upgrade-safe', () => {
666
702
  '.codex/scripts/spec-ground.cjs',
667
703
  '.codex/scripts/validate-spec-output.cjs',
668
704
  '.agents/.gitignore',
669
- '.agents/skills/specs/SKILL.md',
670
- '.agents/skills/specs/templates/design.md',
671
- '.agents/skills/specs/templates/task.md',
672
- '.agents/skills/specs/templates/spec-state.json'
705
+ ...V3_SPECS_BUNDLE.map((file) => `.agents/skills/specs/${file}`)
673
706
  ]) {
674
707
  assert.equal(fs.existsSync(path.join(root, relative)), true, `missing ${relative}`);
675
708
  }
@@ -777,7 +810,13 @@ test('Codex install is project-local, native, and upgrade-safe', () => {
777
810
  ...allFiles(path.join(root, '.agents', 'skills'), (file) => file.endsWith('.md'))
778
811
  ];
779
812
  const visible = modelVisibleFiles.map((file) => fs.readFileSync(file, 'utf8')).join('\n');
780
- assert.doesNotMatch(visible, /\bAgent\(|subagent_type|`Agent`|\bSendMessage\b|\/hapo:|\bhapo:|Claude Code/);
813
+ const foreignRuntimeBoundary = 'If you are Claude Code or any other runtime, ignore this entire Codex block';
814
+ assert.match(visible, new RegExp(foreignRuntimeBoundary));
815
+ assert.doesNotMatch(visible, /If you are Codex CLI or any other runtime, ignore this entire Codex block/);
816
+ assert.doesNotMatch(
817
+ visible.replaceAll(foreignRuntimeBoundary, ''),
818
+ /\bAgent\(|subagent_type|`Agent`|\bSendMessage\b|\/hapo:|\bhapo:|Claude Code/,
819
+ );
781
820
  assert.doesNotMatch(visible, /@@PRIVACY_PROMPT_START@@|Claude Tasks/);
782
821
  assert.match(visible, /\$hapo-specs/);
783
822
 
@@ -796,10 +835,22 @@ test('Codex install is project-local, native, and upgrade-safe', () => {
796
835
 
797
836
  const questionSkill = path.join(root, '.agents', 'skills', 'question', 'SKILL.md');
798
837
  fs.appendFileSync(questionSkill, '\nUSER-CODEX-SENTINEL\n');
838
+ // Fresh installs are exact. Refresh/upgrade intentionally documents the
839
+ // current Codex limitation: removed skill paths are not pruned.
840
+ const obsoleteSpecsRule = path.join(
841
+ root, '.agents', 'skills', 'specs', 'rules', 'design-principles.md'
842
+ );
843
+ fs.mkdirSync(path.dirname(obsoleteSpecsRule), { recursive: true });
844
+ fs.writeFileSync(obsoleteSpecsRule, 'LEGACY-CODEX-ORPHAN\n');
799
845
 
800
846
  const sameVersion = install(root);
801
847
  assert.equal(sameVersion.status, 0, `${sameVersion.stdout}\n${sameVersion.stderr}`);
802
848
  assert.match(fs.readFileSync(questionSkill, 'utf8'), /USER-CODEX-SENTINEL/);
849
+ assert.equal(
850
+ fs.existsSync(obsoleteSpecsRule),
851
+ true,
852
+ 'known limitation: Codex refresh does not prune obsolete skill files'
853
+ );
803
854
 
804
855
  const metadataPath = path.join(root, '.codex', 'cafekit.json');
805
856
  const metadata = JSON.parse(fs.readFileSync(metadataPath, 'utf8'));
@@ -809,6 +860,11 @@ test('Codex install is project-local, native, and upgrade-safe', () => {
809
860
  const upgrade = install(root);
810
861
  assert.equal(upgrade.status, 0, `${upgrade.stdout}\n${upgrade.stderr}`);
811
862
  assert.match(fs.readFileSync(questionSkill, 'utf8'), /USER-CODEX-SENTINEL/);
863
+ assert.equal(
864
+ fs.existsSync(obsoleteSpecsRule),
865
+ true,
866
+ 'known limitation: Codex upgrade does not prune obsolete skill files'
867
+ );
812
868
  assert.ok(fs.readFileSync(path.join(root, 'AGENTS.md'), 'utf8').startsWith(userInstructions));
813
869
 
814
870
  const forced = install(root, ['--force-overwrite']);