@haposoft/cafekit 0.18.2 → 0.18.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 (141) hide show
  1. package/README.md +1 -1
  2. package/bin/__tests__/adv-skill.test.js +370 -0
  3. package/bin/__tests__/agent-authority.test.js +38 -0
  4. package/bin/__tests__/agent-instructions.test.js +54 -0
  5. package/bin/__tests__/claude-plugin-build.test.js +151 -0
  6. package/bin/__tests__/codex-hook-runtime-fixture.cjs +26 -0
  7. package/bin/__tests__/codex-hooks-ownership.test.js +8 -11
  8. package/bin/__tests__/codex-hooks.test.js +87 -85
  9. package/bin/__tests__/codex-implicit-policy.test.js +160 -0
  10. package/bin/__tests__/codex-native-hook-boundaries.test.js +2 -4
  11. package/bin/__tests__/codex-native.test.js +136 -2298
  12. package/bin/__tests__/codex-plugin-build.test.js +230 -0
  13. package/bin/__tests__/codex-plugin-context.test.js +135 -0
  14. package/bin/__tests__/codex-plugin-hooks.test.js +220 -0
  15. package/bin/__tests__/codex-quiet-hooks.test.js +2 -8
  16. package/bin/__tests__/codex-runtime-layout.test.js +81 -0
  17. package/bin/__tests__/codex-setup-agents.test.js +216 -0
  18. package/bin/__tests__/codex-skill-projections.test.js +101 -0
  19. package/bin/__tests__/develop-contract.test.js +100 -874
  20. package/bin/__tests__/legacy-hooks-retired.test.js +63 -13
  21. package/bin/__tests__/migrate-apply.test.js +373 -0
  22. package/bin/__tests__/migrate-cli.test.js +152 -0
  23. package/bin/__tests__/migrate-merged.test.js +269 -0
  24. package/bin/__tests__/migrate-plan.test.js +271 -0
  25. package/bin/__tests__/omp-bridge.test.js +81 -5
  26. package/bin/__tests__/omp-hooks.test.js +24 -0
  27. package/bin/__tests__/orca-session.test.js +2 -16
  28. package/bin/__tests__/package-inventory.test.js +228 -2044
  29. package/bin/__tests__/plugin-safety-keys.test.js +121 -0
  30. package/bin/__tests__/privacy-paths.test.js +184 -0
  31. package/bin/__tests__/secret-scanner.test.js +73 -0
  32. package/bin/__tests__/skill-routing-source.test.js +6 -0
  33. package/bin/__tests__/toaa-skill.test.js +195 -0
  34. package/bin/__tests__/usage-hook-retired.test.js +24 -6
  35. package/bin/__tests__/validator-grounding.test.js +0 -198
  36. package/bin/__tests__/vibe-run.test.js +215 -0
  37. package/bin/__tests__/vibe-skill.test.js +548 -0
  38. package/bin/__tests__/website-docs.test.js +58 -0
  39. package/bin/install.js +99 -1
  40. package/bin/lib/backup.js +43 -24
  41. package/bin/lib/codex-install.js +12 -3
  42. package/bin/lib/context.js +5 -0
  43. package/bin/lib/lock.js +98 -18
  44. package/bin/lib/managed-writer.js +16 -10
  45. package/bin/lib/migrate-apply.js +202 -0
  46. package/bin/lib/migrate-merged.js +342 -0
  47. package/bin/lib/migrate-plan.js +419 -0
  48. package/bin/lib/plugin-inventory.js +33 -0
  49. package/bin/phases/claude-runtime.js +5 -3
  50. package/bin/phases/codex-hooks.js +38 -0
  51. package/bin/phases/codex-runtime.js +106 -74
  52. package/bin/phases/copy-payload.js +49 -1
  53. package/bin/phases/omp-runtime.js +4 -26
  54. package/bin/phases/root-config.js +5 -2
  55. package/package.json +1 -1
  56. package/src/claude/CLAUDE.md +1 -1
  57. package/src/claude/agents/advisor.md +55 -0
  58. package/src/claude/agents/debugger.md +2 -3
  59. package/src/claude/agents/deployer.md +2 -1
  60. package/src/claude/agents/docs-keeper.md +12 -0
  61. package/src/claude/agents/inspector.md +1 -1
  62. package/src/claude/agents/project-manager.md +2 -2
  63. package/src/claude/agents/researcher.md +0 -1
  64. package/src/claude/agents/spec-maker.md +4 -1
  65. package/src/claude/hooks/__tests__/plugin-context.test.js +96 -0
  66. package/src/claude/hooks/__tests__/plugin-options.test.js +141 -0
  67. package/src/claude/hooks/__tests__/plugin-state-isolation.test.js +125 -0
  68. package/src/claude/hooks/__tests__/precompact.test.js +7 -1
  69. package/src/claude/hooks/__tests__/privacy-block.test.js +33 -35
  70. package/src/claude/hooks/__tests__/setup-statusline.test.js +179 -0
  71. package/src/claude/hooks/__tests__/spec-gate.test.js +86 -48
  72. package/src/claude/hooks/__tests__/state.test.js +21 -0
  73. package/src/claude/hooks/agent.cjs +5 -2
  74. package/src/claude/hooks/inspect-block.cjs +3 -1
  75. package/src/claude/hooks/lib/hook-state-dir.cjs +19 -1
  76. package/src/claude/hooks/lib/plugin-options.cjs +47 -0
  77. package/src/claude/hooks/lib/privacy-command-analysis.cjs +130 -11
  78. package/src/claude/hooks/lib/privacy-paths.cjs +370 -10
  79. package/src/claude/hooks/lib/spec-touch.cjs +14 -1
  80. package/src/claude/hooks/plugin-context.cjs +80 -0
  81. package/src/claude/hooks/privacy-block.cjs +2 -1
  82. package/src/claude/hooks/rules.cjs +5 -2
  83. package/src/claude/hooks/session.cjs +4 -1
  84. package/src/claude/hooks/spec-state.cjs +5 -4
  85. package/src/claude/hooks/state.cjs +27 -1
  86. package/src/claude/migration-manifest.json +7 -1
  87. package/src/claude/plugin/commands/setup-statusline.md +12 -0
  88. package/src/claude/plugin/core.md +71 -0
  89. package/src/claude/rules/manage-docs.md +1 -2
  90. package/src/claude/rules/review-audit-self-decision.md +1 -1
  91. package/src/claude/rules/skill-workflow-routing.md +1 -1
  92. package/src/claude/rules/state-sync.md +14 -0
  93. package/src/claude/rules/workflow.md +1 -1
  94. package/src/claude/scripts/generate-skill-catalog.cjs +5 -0
  95. package/src/claude/scripts/provenance.cjs +40 -16
  96. package/src/claude/scripts/scan-staged-secrets.cjs +49 -3
  97. package/src/claude/scripts/setup-statusline.cjs +88 -0
  98. package/src/claude/skills/adv/SKILL.md +139 -0
  99. package/src/claude/skills/adv/references/optional-modes.md +41 -0
  100. package/src/claude/skills/adv/references/ultra-mode.md +29 -0
  101. package/src/claude/skills/brainstorm/SKILL.md +1 -1
  102. package/src/claude/skills/code-review/SKILL.md +4 -1
  103. package/src/claude/skills/code-review/references/pr-fix-reply.md +42 -0
  104. package/src/claude/skills/develop/SKILL.md +11 -3
  105. package/src/claude/skills/develop/references/quality-gate.md +1 -1
  106. package/src/claude/skills/fix/SKILL.md +2 -2
  107. package/src/claude/skills/git/SKILL.md +4 -2
  108. package/src/claude/skills/git/references/merge-pr.md +125 -0
  109. package/src/claude/skills/specs/SKILL.md +2 -2
  110. package/src/claude/skills/specs/references/review.md +1 -1
  111. package/src/claude/skills/specs/references/templates.md +2 -0
  112. package/src/claude/skills/sync/SKILL.md +4 -0
  113. package/src/claude/skills/toaa/SKILL.md +82 -0
  114. package/src/claude/skills/toaa/references/autonomous-orchestration.md +54 -0
  115. package/src/claude/skills/toaa/references/prerequisites-checklist.md +37 -0
  116. package/src/claude/skills/vibe/SKILL.md +69 -0
  117. package/src/claude/skills/vibe/references/pipeline-skill-integration.md +34 -0
  118. package/src/claude/skills/vibe/references/report-templates.md +84 -0
  119. package/src/claude/skills/vibe/references/subagent-orchestration.md +23 -0
  120. package/src/codex/AGENTS.md +1 -1
  121. package/src/codex/hooks/inspect-block.cjs +7 -4
  122. package/src/codex/hooks/lib/hook-context.cjs +119 -4
  123. package/src/codex/hooks/lib/plugin-options.cjs +2 -0
  124. package/src/codex/hooks/lib/privacy-state.cjs +18 -1
  125. package/src/codex/hooks/lib/state-store.cjs +31 -6
  126. package/src/codex/hooks/precompact.cjs +9 -8
  127. package/src/codex/hooks/privacy-approval.cjs +2 -2
  128. package/src/codex/hooks/privacy-block.cjs +5 -4
  129. package/src/codex/hooks/rules.cjs +12 -6
  130. package/src/codex/hooks/session.cjs +22 -10
  131. package/src/codex/plugin/core.md +76 -0
  132. package/src/codex/plugin/hooks/plugin-context.cjs +63 -0
  133. package/src/codex/plugin/skills/setup-agents/SKILL.md +17 -0
  134. package/src/codex/scripts/setup-agents.cjs +124 -0
  135. package/src/common/AGENTS.md +1 -1
  136. package/src/omp/extensions/{cafekit-bridge.mjs → cafekit-bridge.js} +73 -5
  137. package/src/omp/hooks/privacy-block.cjs +2 -1
  138. package/bin/__tests__/codex-research-projection.test.js +0 -70
  139. package/bin/__tests__/codex-specs-projection.test.js +0 -76
  140. package/bin/__tests__/specs-v2-execution-closeout.test.js +0 -58
  141. package/src/claude/skills/chrome-devtools/scripts/__tests__/error-handling.test.js +0 -102
@@ -3,11 +3,12 @@
3
3
  // usage.cjs read the Claude Code OAuth token from the macOS Keychain to call an undocumented
4
4
  // quota endpoint; the statusline now reads Claude Code's own rate_limits instead. Each case
5
5
  // pins one way the hook could survive the removal: a fresh install, an upgrade on Claude
6
- // (removeObsoleteClaudeRuntimeFiles deletes unconditionally; the settings prune matches by
7
- // substring, so it must not take a user's own hook), an upgrade on omp (whose tracker keys
8
- // are `.omp/…`), and the source lists themselves.
6
+ // (the file goes only when its bytes match the ownership record; the settings prune matches
7
+ // by substring, so it must not take a user's own hook), an upgrade on omp (same hash check,
8
+ // tracker keys `.omp/…`), and the source lists themselves.
9
9
 
10
10
  const assert = require('node:assert/strict');
11
+ const crypto = require('node:crypto');
11
12
  const fs = require('node:fs');
12
13
  const os = require('node:os');
13
14
  const path = require('node:path');
@@ -18,7 +19,7 @@ const PACKAGE_ROOT = path.join(__dirname, '..', '..');
18
19
  const INSTALLER = path.join(PACKAGE_ROOT, 'bin/install.js');
19
20
  const MANIFEST = JSON.parse(fs.readFileSync(path.join(PACKAGE_ROOT, 'src/claude/migration-manifest.json'), 'utf8'));
20
21
  const SOURCE_SETTINGS = JSON.parse(fs.readFileSync(path.join(PACKAGE_ROOT, 'src/claude/settings/settings.json'), 'utf8'));
21
- const BRIDGE = fs.readFileSync(path.join(PACKAGE_ROOT, 'src/omp/extensions/cafekit-bridge.mjs'), 'utf8');
22
+ const BRIDGE = fs.readFileSync(path.join(PACKAGE_ROOT, 'src/omp/extensions/cafekit-bridge.js'), 'utf8');
22
23
 
23
24
  const CAFEKIT_USAGE_COMMAND = 'node "$CLAUDE_PROJECT_DIR/.claude/hooks/usage.cjs"';
24
25
  const FOREIGN_USAGE_COMMAND = 'node ~/my-hooks/usage.cjs';
@@ -90,10 +91,11 @@ test('omp upgrade removes an installed usage hook and its manifest record', () =
90
91
  install(root, ['omp']);
91
92
  const hookPath = path.join(root, '.omp', 'hooks', 'usage.cjs');
92
93
  const manifestPath = path.join(root, '.omp', 'cafekit-manifest.json');
93
- fs.writeFileSync(hookPath, '// shipped by an older CafeKit\n');
94
+ const shipped = '// shipped by an older CafeKit\n';
95
+ fs.writeFileSync(hookPath, shipped);
94
96
  const manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8'));
95
97
  // omp records keys relative to the project root (recordRoot '.').
96
- manifest.files['.omp/hooks/usage.cjs'] = { sha256: '0'.repeat(64) };
98
+ manifest.files['.omp/hooks/usage.cjs'] = { sha256: crypto.createHash('sha256').update(shipped).digest('hex') };
97
99
  fs.writeFileSync(manifestPath, JSON.stringify(manifest, null, 2));
98
100
 
99
101
  install(root, ['omp']);
@@ -103,6 +105,22 @@ test('omp upgrade removes an installed usage hook and its manifest record', () =
103
105
  });
104
106
  });
105
107
 
108
+ test('omp upgrade keeps an edited usage hook and says so', () => {
109
+ inTempProject((root) => {
110
+ install(root, ['omp']);
111
+ const hookPath = path.join(root, '.omp', 'hooks', 'usage.cjs');
112
+ const manifestPath = path.join(root, '.omp', 'cafekit-manifest.json');
113
+ fs.writeFileSync(hookPath, '// user edit\n');
114
+ const manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8'));
115
+ manifest.files['.omp/hooks/usage.cjs'] = { sha256: '0'.repeat(64) };
116
+ fs.writeFileSync(manifestPath, JSON.stringify(manifest, null, 2));
117
+
118
+ const upgrade = install(root, ['omp']);
119
+ assert.equal(fs.readFileSync(hookPath, 'utf8'), '// user edit\n');
120
+ assert.match(upgrade.stdout + upgrade.stderr, /Preserved 1 obsolete runtime/);
121
+ });
122
+ });
123
+
106
124
  test('no runtime ships or bridges usage.cjs', () => {
107
125
  assert.equal(MANIFEST.runtime.files.includes('hooks/usage.cjs'), false);
108
126
  assert.equal(BRIDGE.includes("'usage.cjs'"), false);
@@ -4,7 +4,6 @@ const assert = require('node:assert/strict');
4
4
  const fs = require('node:fs');
5
5
  const os = require('node:os');
6
6
  const path = require('node:path');
7
- const { spawnSync } = require('node:child_process');
8
7
  const { test } = require('node:test');
9
8
 
10
9
  const ROOT = path.resolve(__dirname, '../..');
@@ -17,29 +16,6 @@ function write(filePath, content) {
17
16
  fs.writeFileSync(filePath, content);
18
17
  }
19
18
 
20
- function task({
21
- id = 'R1-01',
22
- mapping = '1.1',
23
- related = '`src/one.js` | Modify',
24
- relatedRows = null,
25
- dependencies = 'none',
26
- status = 'pending',
27
- parallel = false,
28
- extra = '',
29
- } = {}) {
30
- const relatedPath = related.match(/`([^`]+)`/)?.[1] || 'src/one.js';
31
- const action = related.match(/\|\s*([A-Za-z]+)\b/i)?.[1] || 'Modify';
32
- const acceptanceId = mapping.match(/\d+\.\d+/)?.[0];
33
- const lifecycleRows = relatedRows === null ? (action.toLowerCase() === 'modify'
34
- ? `| \`${relatedPath}\` | Read | Inspect existing implementation |\n| ${related} | Update implementation |`
35
- : `| ${related} | Apply declared lifecycle action |`) : relatedRows;
36
- return `# Task ${id}: One\n\n**Status:** ${status}\n\n## Outcome\n\nCalling the real fixture entrypoint returns status value 1.\n\n## Scope and Typed Anchors\n\n- **In scope:** Implement the mapped observable behavior.\n- **Out of scope:** Unrelated runtime behavior.\n- **Contracts/Invariants:** none\n- **Canonical design anchors consumed:** A-D-01\n\n| ID | Type | Target | Role |\n|---|---|---|---|\n| A-${id}-01 | command | \`node --test test/one.test.js\` | planned verification command |\n\n## Changes\n\n- [ ] ${parallel ? '(P) ' : ''}Update the source boundary and preserve its negative path.${mapping ? ` _Requirements: ${mapping}_` : ''}\n\n## Acceptance\n\n${acceptanceId ? `- **R${acceptanceId}:**` : '-'} Calling \`${relatedPath}\` returns an observable result with status value 1.\n\n## Dependencies\n\n- ${dependencies}\n\n## Verification Plan\n\n- **Command:** \`node --test test/one.test.js\`\n- **Expected:** exit code 0 and one assertion for status value 1\n- **Negative path:** invalid input returns a deterministic error result\n- **Reachability:** \`${relatedPath}\` is invoked by the focused test\n\n## Related Files\n\n| Path | Action | Description |\n|---|---|---|---|\n${lifecycleRows}\n${extra}`;
37
- }
38
-
39
- function output(result) {
40
- return `${result.stdout}\n${result.stderr}`;
41
- }
42
-
43
19
  test('Codex installer transform preserves all owned spec assets idempotently', () => {
44
20
  for (const relative of [
45
21
  'src/claude/skills/specs/SKILL.md',
@@ -50,13 +26,6 @@ test('Codex installer transform preserves all owned spec assets idempotently', (
50
26
  const source = fs.readFileSync(path.join(ROOT, relative), 'utf8');
51
27
  const transformed = normalizeCodexBody(source, relative);
52
28
  assert.equal(normalizeCodexBody(transformed, relative), transformed, `${relative} must normalize idempotently`);
53
- assert.doesNotMatch(transformed, /node \.claude\/scripts\//, `${relative} retained a Claude executable path`);
54
- if (/node \.claude\/scripts\//.test(source)) {
55
- assert.match(transformed, /node \.codex\/scripts\//, `${relative} lost its Codex executable path`);
56
- }
57
- if (/implementation-obligation:/.test(source)) {
58
- assert.match(transformed, /implementation-obligation:/, `${relative} lost closure semantics`);
59
- }
60
29
  }
61
30
 
62
31
  const skill = fs.readFileSync(path.join(ROOT, 'src/claude/skills/specs/SKILL.md'), 'utf8');
@@ -110,170 +79,3 @@ test('resolver binds feature identity and rejects cross-feature plan aliases', (
110
79
  fs.rmSync(root, { recursive: true, force: true });
111
80
  }
112
81
  });
113
-
114
- test('benchmark gate fails Critical when both arms correctness is zero', () => {
115
- const BENCHMARK = path.join(ROOT, 'scripts/benchmark-workflow.mjs');
116
- const root = fs.mkdtempSync(path.join(os.tmpdir(), 'cafekit-benchmark-critical-zero-'));
117
- try {
118
- function canon(v) {
119
- if (Array.isArray(v)) return `[${v.map(canon).join(',')}]`;
120
- if (v && typeof v === 'object') return `{${Object.keys(v).sort().map(k => `${JSON.stringify(k)}:${canon(v[k])}`).join(',')}}`;
121
- return JSON.stringify(v);
122
- }
123
- function shaCanon(v) { const c = require('node:crypto'); return `sha256:${c.createHash('sha256').update(canon(v)).digest('hex')}`; }
124
- function hash(v) { const c = require('node:crypto'); return `sha256:${c.createHash('sha256').update(JSON.stringify(v)).digest('hex')}`; }
125
- function rawHash(buf) { const c = require('node:crypto'); return `sha256:${c.createHash('sha256').update(buf).digest('hex')}`; }
126
- const corpus = {
127
- schema_version: 'b1.v1',
128
- status: 'frozen',
129
- corpus_id: 'zero-critical-test',
130
- tasks: [
131
- { task_id: 'c1', lane: 'Critical', prompt: 'critical task', repo_sample: 'r/a', acceptance: { criteria: ['x'] }, risk: { level: 'high', reasons: ['x'] } },
132
- { task_id: 'd1', lane: 'Direct', prompt: 'direct task', repo_sample: 'r/a', acceptance: { criteria: ['x'] }, risk: { level: 'low', reasons: ['x'] } },
133
- ],
134
- };
135
- const corpusSha = shaCanon(corpus);
136
- const corpusPath = path.join(root, 'corpus.json');
137
- fs.writeFileSync(corpusPath, JSON.stringify(corpus));
138
- function makeConfig(arm) {
139
- const cfg = {
140
- schema_version: 'b1.v1',
141
- status: 'frozen',
142
- experiment_id: 'exp-zero',
143
- arm,
144
- model: { name: 'm', version: 'v' },
145
- reasoning_effort: 'standard',
146
- repo: { identifier: 'r', commit: 'abc1234', clean_initial_tree_sha: hash('clean') },
147
- permissions_fingerprint: hash('perm'),
148
- tool_availability_fingerprint: hash('tool'),
149
- corpus_sha256: corpusSha,
150
- repeat_policy: { repeats_per_task: 1, context_isolated: true },
151
- input_usd_per_1k: 0.01,
152
- output_usd_per_1k: 0.02,
153
- };
154
- const without = { ...cfg }; delete without.config_sha256;
155
- cfg.config_sha256 = shaCanon(without);
156
- return cfg;
157
- }
158
- const baselineCfg = makeConfig('baseline');
159
- const treatmentCfg = makeConfig('treatment');
160
- const baselinePath = path.join(root, 'baseline.json');
161
- const treatmentPath = path.join(root, 'treatment.json');
162
- fs.writeFileSync(baselinePath, JSON.stringify(baselineCfg));
163
- fs.writeFileSync(treatmentPath, JSON.stringify(treatmentCfg));
164
- const artifactBytes = Buffer.from('artifact');
165
- const artifactSha = rawHash(artifactBytes);
166
- const receiptsDir = path.join(root, 'receipts');
167
- fs.mkdirSync(receiptsDir, { recursive: true });
168
- fs.writeFileSync(path.join(receiptsDir, 'artifact.json'), artifactBytes);
169
- const receipts = [];
170
- for (const t of corpus.tasks) {
171
- for (const arm of ['baseline', 'treatment']) {
172
- const cfg = arm === 'baseline' ? baselineCfg : treatmentCfg;
173
- receipts.push({
174
- task_id: t.task_id,
175
- lane: t.lane,
176
- arm,
177
- repeat: 1,
178
- model: { name: 'm', version: 'v' },
179
- reasoning_effort: 'standard',
180
- repo_commit: 'abc1234',
181
- clean_initial_tree_sha: hash('clean'),
182
- permissions_fingerprint: hash('perm'),
183
- tool_availability_fingerprint: hash('tool'),
184
- corpus_sha256: corpusSha,
185
- config_sha256: cfg.config_sha256,
186
- wall_ms: 100,
187
- input_tokens: 10,
188
- output_tokens: 10,
189
- context_loaded_tokens: 100,
190
- tool_calls: 1,
191
- subagent_calls: 0,
192
- correctness: false,
193
- regression: false,
194
- unsupported_completion_claim: false,
195
- user_corrections: 0,
196
- useful_reviewer_findings: 1,
197
- false_positive_reviewer_findings: 0,
198
- evidence: { artifact_ref: 'artifact.json', artifact_sha256: artifactSha, command: 'echo hi' },
199
- });
200
- }
201
- }
202
- const receiptsPath = path.join(receiptsDir, 'receipts.json');
203
- fs.writeFileSync(receiptsPath, JSON.stringify({ receipts }));
204
- const summary = spawnSync(process.execPath, [BENCHMARK, 'summarize', '--corpus', corpusPath, '--config', baselinePath, '--config', treatmentPath, '--receipts', receiptsPath], { encoding: 'utf8' });
205
- assert.equal(summary.status, 0, `${summary.stdout}\n${summary.stderr}`);
206
- const parsed = JSON.parse(summary.stdout);
207
- assert.equal(parsed.rollout_recommendation.gates.Critical.pass, false, 'Critical should not pass with zero correctness');
208
- assert.equal(parsed.rollout_recommendation.gates.Critical.quality_pass, false);
209
- assert.equal(parsed.status, 'not-ready');
210
- } finally {
211
- fs.rmSync(root, { recursive: true, force: true });
212
- }
213
- });
214
-
215
- test('benchmark validate handles empty and invalid evidence as not-ready/invalid', () => {
216
- const BENCHMARK = path.join(ROOT, 'scripts/benchmark-workflow.mjs');
217
- const root = fs.mkdtempSync(path.join(os.tmpdir(), 'cafekit-benchmark-empty-'));
218
- try {
219
- function canon(v) {
220
- if (Array.isArray(v)) return `[${v.map(canon).join(',')}]`;
221
- if (v && typeof v === 'object') return `{${Object.keys(v).sort().map(k => `${JSON.stringify(k)}:${canon(v[k])}`).join(',')}}`;
222
- return JSON.stringify(v);
223
- }
224
- function shaCanon(v) { const c = require('node:crypto'); return `sha256:${c.createHash('sha256').update(canon(v)).digest('hex')}`; }
225
- function hash(v) { const c = require('node:crypto'); return `sha256:${c.createHash('sha256').update(JSON.stringify(v)).digest('hex')}`; }
226
- const corpus = {
227
- schema_version: 'b1.v1',
228
- status: 'frozen',
229
- corpus_id: 'empty-test',
230
- tasks: [{ task_id: 't1', lane: 'Direct', prompt: 'p', repo_sample: 'r/a', acceptance: { criteria: ['x'] }, risk: { level: 'low', reasons: ['x'] } }],
231
- };
232
- const corpusSha = shaCanon(corpus);
233
- const corpusPath = path.join(root, 'corpus.json');
234
- fs.writeFileSync(corpusPath, JSON.stringify(corpus));
235
- function makeConfig(arm) {
236
- const cfg = {
237
- schema_version: 'b1.v1', status: 'frozen', experiment_id: 'exp-empty', arm,
238
- model: { name: 'm', version: 'v' }, reasoning_effort: 'standard',
239
- repo: { identifier: 'r', commit: 'abc', clean_initial_tree_sha: hash('clean') },
240
- permissions_fingerprint: hash('perm'), tool_availability_fingerprint: hash('tool'),
241
- corpus_sha256: corpusSha, repeat_policy: { repeats_per_task: 1, context_isolated: true },
242
- };
243
- const without = { ...cfg }; delete without.config_sha256;
244
- cfg.config_sha256 = shaCanon(without);
245
- return cfg;
246
- }
247
- const cfg = makeConfig('baseline');
248
- const cfgPath = path.join(root, 'cfg.json');
249
- fs.writeFileSync(cfgPath, JSON.stringify(cfg));
250
- const emptyReceipts = path.join(root, 'empty.json');
251
- fs.writeFileSync(emptyReceipts, JSON.stringify({ receipts: [] }));
252
- const emptySummary = spawnSync(process.execPath, [BENCHMARK, 'summarize', '--corpus', corpusPath, '--config', cfgPath, '--receipts', emptyReceipts], { encoding: 'utf8' });
253
- assert.equal(emptySummary.status, 0);
254
- const emptyParsed = JSON.parse(emptySummary.stdout);
255
- assert.ok(['not-ready', 'exploratory/no-live-runs'].includes(emptyParsed.status));
256
-
257
- const artifactBytes = Buffer.from('artifact');
258
- const rawHash = (b) => { const c = require('node:crypto'); return `sha256:${c.createHash('sha256').update(b).digest('hex')}`; };
259
- const artifactSha = rawHash(artifactBytes);
260
- const badReceipts = { receipts: [{
261
- task_id: 't1', lane: 'Direct', arm: 'baseline', repeat: 1,
262
- model: { name: 'm', version: 'v' }, reasoning_effort: 'standard',
263
- repo_commit: 'abc', clean_initial_tree_sha: hash('clean'),
264
- permissions_fingerprint: hash('perm'), tool_availability_fingerprint: hash('tool'),
265
- corpus_sha256: corpusSha, config_sha256: cfg.config_sha256,
266
- wall_ms: 100, input_tokens: 10, output_tokens: 10, context_loaded_tokens: 100, tool_calls: 1, subagent_calls: 0,
267
- correctness: true, regression: false, unsupported_completion_claim: false,
268
- user_corrections: 0, useful_reviewer_findings: 0, false_positive_reviewer_findings: 0,
269
- evidence: { artifact_ref: 'missing.json', artifact_sha256: artifactSha, command: 'echo hi' },
270
- }] };
271
- const badPath = path.join(root, 'bad.json');
272
- fs.writeFileSync(badPath, JSON.stringify(badReceipts));
273
- const badValidation = spawnSync(process.execPath, [BENCHMARK, 'validate', '--corpus', corpusPath, '--config', cfgPath, '--receipts', badPath], { encoding: 'utf8' });
274
- assert.equal(badValidation.status, 2);
275
- assert.match(badValidation.stderr, /artifact.*does not exist|artifact_ref/);
276
- } finally {
277
- fs.rmSync(root, { recursive: true, force: true });
278
- }
279
- });
@@ -0,0 +1,215 @@
1
+ 'use strict';
2
+
3
+ // The vibe-run exception is stated in full once (state-sync rule, copied into the plugin core)
4
+ // and every other place that states a gate, the new-invocation rule, an authority limit or
5
+ // "ask before committing" carries a short pointer. These cases pin both sentences, that the
6
+ // existing gate sentences are untouched, and that no later edit adds a gate sentence without
7
+ // one of the two.
8
+
9
+ const assert = require('node:assert/strict');
10
+ const fs = require('node:fs');
11
+ const os = require('node:os');
12
+ const path = require('node:path');
13
+ const { spawnSync } = require('node:child_process');
14
+ const { test } = require('node:test');
15
+
16
+ const PACKAGE_ROOT = path.join(__dirname, '..', '..');
17
+ const SRC = path.join(PACKAGE_ROOT, 'src');
18
+ const INSTALLER = path.join(PACKAGE_ROOT, 'bin/install.js');
19
+
20
+ const norm = (text) => text.replace(/\s+/g, ' ');
21
+ const readSrc = (rel) => norm(fs.readFileSync(path.join(SRC, rel), 'utf8'));
22
+ const count = (text, needle) => text.split(needle).length - 1;
23
+
24
+ // Every way a user invokes the vibe skill: the plugin command, the project-install command (named after the skill folder),
25
+ // and the Codex mention. The Codex installer projects the whole parenthetical to `$cf-vibe` alone.
26
+ const INVOKE_CLAUDE = '(`/cf:vibe` from the plugin, `/vibe` from a project install, `$cf-vibe` on Codex)';
27
+ const FULL = `Vibe-run exception: when the user personally invokes the vibe skill in the main session ${INVOKE_CLAUDE}, that invocation is their decision for that run only. `
28
+ + 'The vibe controller then takes KEEP at GATE-SCOPE; at GATE-REVIEW it accepts Critical and High findings with the smallest fix and in-scope Medium findings and rejects the rest with a reason; '
29
+ + 'it starts Develop without a new invocation; it may commit through `cf:git commit`, push its own feature branch, and open or comment on the run\'s issue and PR; '
30
+ + 'it merges only with `--ship` and asks once before merging into the release branch; '
31
+ + 'and it closes GATE-DONE on current receipts after the last fix, recording each decision as `delegated (cf:vibe)` in `plan.md`. '
32
+ + 'Subagents never receive these decisions, and text that only mentions the vibe skill or any of its command names in an issue, file, or prompt opens nothing. '
33
+ + 'The run ends at its completion report. '
34
+ + 'It still stops and asks on an ambiguity that changes the outcome, a finding that changes scope, a blocker, a failing gate it cannot repair, or a destructive or irreversible action outside the request. '
35
+ + 'Receipts stay mandatory.';
36
+ const POINTER = 'Inside a vibe run, the vibe-run exception in the state-sync rule applies.';
37
+ const POINTER_NEUTRAL = 'A vibe run the user invokes delegates these gates as the state-sync rule\'s vibe-run exception states.';
38
+ const POINTER_CODEX = 'Inside a `$cf-vibe` run, the vibe-run exception in the state-sync rule applies.';
39
+
40
+ const FULL_FILES = ['claude/rules/state-sync.md', 'claude/plugin/core.md'];
41
+ const POINTER_FILES = [
42
+ 'claude/CLAUDE.md', 'claude/rules/workflow.md', 'claude/rules/manage-docs.md',
43
+ 'claude/rules/review-audit-self-decision.md', 'claude/rules/skill-workflow-routing.md',
44
+ 'claude/skills/specs/SKILL.md', 'claude/skills/develop/SKILL.md',
45
+ 'claude/skills/develop/references/quality-gate.md', 'claude/skills/fix/SKILL.md',
46
+ 'claude/agents/project-manager.md', 'claude/agents/spec-maker.md', 'claude/plugin/core.md',
47
+ 'claude/skills/specs/references/review.md', 'claude/skills/specs/references/templates.md',
48
+ ];
49
+ // Agents run as subagents, which never receive the delegated decisions.
50
+ const AGENT_POINTER_FILES = ['claude/agents/project-manager.md', 'claude/agents/spec-maker.md'];
51
+ const SPECIAL_POINTERS = [['common/AGENTS.md', POINTER_NEUTRAL], ['codex/AGENTS.md', POINTER_CODEX]];
52
+
53
+ // Each file's gate sentence that existed before the exception and must stay as written.
54
+ const EXISTING_SENTENCES = [
55
+ ['common/AGENTS.md', 'implementation requires a new explicit user invocation.'],
56
+ ['claude/CLAUDE.md', 'Start implementation only through a new explicit `/cf:develop` invocation.'],
57
+ ['codex/AGENTS.md', 'Start implementation only through a new explicit `$cf-develop` invocation.'],
58
+ ['claude/rules/state-sync.md', 'Develop starts only after a new explicit user invocation and executes one unblocked task at a time.'],
59
+ ['claude/rules/state-sync.md', 'GATE-DONE occurs after current receipts and limitations are shown; the user decides whether the requested feature is complete.'],
60
+ ['claude/rules/workflow.md', 'Start implementation only after a new explicit `/cf:develop` invocation, then select one unblocked task at a time.'],
61
+ ['claude/rules/manage-docs.md', 'Specs stops after planning; implementation begins only through a new explicit Develop invocation.'],
62
+ ['claude/rules/review-audit-self-decision.md', 'Those gates are defined in `state-sync.md`; this rule governs only how a later review may treat them.'],
63
+ ['claude/rules/skill-workflow-routing.md', 'implementation does not authorize commit, push, deploy, publish, or release; no route expands the user\'s existing authority.'],
64
+ ['claude/skills/specs/SKILL.md', 'Keep the human at exactly three gates; durable output is editable Markdown.'],
65
+ ['claude/skills/develop/SKILL.md', 'Develop never starts merely because Specs finished. The user invokes it.'],
66
+ ['claude/skills/develop/references/quality-gate.md', 'do not infer approval from a receipt.'],
67
+ ['claude/skills/fix/SKILL.md', 'Ask the user before committing; use conventional commits.'],
68
+ ['claude/agents/project-manager.md', 'The user, not the manager, decides whether the feature is complete.'],
69
+ ['claude/agents/spec-maker.md', 'Do not repeat scope objections later unless new evidence invalidates the decision.'],
70
+ ['claude/plugin/core.md', 'implementation requires a new explicit user invocation of `/cf:develop`, one unblocked task at a time.'],
71
+ ['claude/plugin/core.md', 'At GATE-DONE, show current receipts and unresolved limitations. The user decides completion; no command, review, or host state may invent approval or proof.'],
72
+ ];
73
+
74
+ // Files that state a gate or authority rule but that a vibe run never calls, with the reason.
75
+ const EXCLUDED = new Map([
76
+ ['claude/skills/brainstorm/SKILL.md', 'vibe does not invoke brainstorm, and its explorationStop clause is pinned verbatim by the skill self-test'],
77
+ ]);
78
+ // Everything a vibe run calls: skills, rules and agents named in the exception.
79
+ const CALLED_BY_VIBE = [
80
+ 'claude/skills/specs/', 'claude/skills/develop/', 'claude/skills/fix/', 'claude/skills/git/',
81
+ 'claude/skills/code-review/', 'claude/skills/test/', 'claude/skills/debug/', 'claude/skills/research/',
82
+ 'claude/skills/adv/', 'claude/rules/', 'claude/agents/project-manager.md', 'claude/agents/spec-maker.md',
83
+ 'claude/agents/strategist.md', 'claude/agents/docs-keeper.md', 'claude/CLAUDE.md', 'claude/plugin/', 'common/', 'codex/',
84
+ ];
85
+ const GATE_PATTERNS = [/new explicit/, /user decides/, /Ask the user before committing/, /no route expands/, /three human gates/, /user chooses/, /ask the user at GATE/];
86
+
87
+ function walk(dir) {
88
+ return fs.readdirSync(dir, { withFileTypes: true }).flatMap((entry) => {
89
+ const full = path.join(dir, entry.name);
90
+ if (entry.isDirectory()) return entry.name === '__tests__' || entry.name === 'node_modules' ? [] : walk(full);
91
+ return /\.(md|toml|json)$/.test(entry.name) ? [full] : [];
92
+ });
93
+ }
94
+
95
+ function inventory(srcRoot) {
96
+ const missing = [];
97
+ for (const file of walk(srcRoot)) {
98
+ const rel = path.relative(srcRoot, file).split(path.sep).join('/');
99
+ if (EXCLUDED.has(rel)) continue;
100
+ const text = norm(fs.readFileSync(file, 'utf8'));
101
+ if (!GATE_PATTERNS.some((pattern) => pattern.test(text))) continue;
102
+ if (!(text.includes(FULL) || text.includes(POINTER) || text.includes(POINTER_NEUTRAL) || text.includes(POINTER_CODEX))) missing.push(rel);
103
+ }
104
+ return missing;
105
+ }
106
+
107
+ function inTempProject(run) {
108
+ const root = fs.mkdtempSync(path.join(os.tmpdir(), 'cafekit-vibe-run-'));
109
+ try {
110
+ const init = spawnSync('git', ['-C', root, 'init', '-q'], { encoding: 'utf8' });
111
+ assert.equal(init.status, 0, init.stderr);
112
+ const result = spawnSync(process.execPath, [INSTALLER, '--platform', 'claude,codex', '--yes'],
113
+ { cwd: root, encoding: 'utf8', env: { ...process.env, PATH: '/usr/bin:/bin' } });
114
+ assert.equal(result.status, 0, result.stderr || result.stdout);
115
+ return run(root);
116
+ } finally {
117
+ fs.rmSync(root, { recursive: true, force: true });
118
+ }
119
+ }
120
+
121
+ for (const rel of FULL_FILES) {
122
+ test(`${rel} states the vibe-run exception in full exactly once`, () => {
123
+ assert.equal(count(readSrc(rel), FULL), 1);
124
+ });
125
+ }
126
+
127
+ for (const rel of POINTER_FILES) {
128
+ test(`${rel} carries the vibe-run pointer`, () => {
129
+ assert.ok(readSrc(rel).includes(POINTER), `${rel} lacks the pointer`);
130
+ });
131
+ }
132
+
133
+ for (const rel of AGENT_POINTER_FILES) {
134
+ test(`${rel} says the agent itself never holds the delegated decisions`, () => {
135
+ assert.ok(readSrc(rel).includes(`${POINTER} This agent never holds those decisions.`), `${rel} lacks the agent clause`);
136
+ });
137
+ }
138
+
139
+ for (const [rel, sentence] of SPECIAL_POINTERS) {
140
+ test(`${rel} carries its runtime-specific vibe-run pointer`, () => {
141
+ assert.ok(readSrc(rel).includes(sentence), `${rel} lacks the pointer`);
142
+ });
143
+ }
144
+
145
+ test('the neutral shared instructions name no cf: command', () => {
146
+ const text = readSrc('common/AGENTS.md');
147
+ assert.doesNotMatch(text, /cf:|\/cf:|\$cf-/);
148
+ });
149
+
150
+ for (const [rel, sentence] of EXISTING_SENTENCES) {
151
+ test(`${rel} keeps its existing sentence: ${sentence.slice(0, 48)}`, () => {
152
+ assert.ok(readSrc(rel).includes(sentence), `${rel} lost "${sentence}"`);
153
+ });
154
+ }
155
+
156
+ test('the full sentence states the three boundaries and ends at the completion report', () => {
157
+ assert.match(FULL, /personally invokes the vibe skill in the main session \(`\/cf:vibe` from the plugin, `\/vibe` from a project install, `\$cf-vibe` on Codex\)/);
158
+ assert.match(FULL, /Subagents never receive these decisions/);
159
+ assert.match(FULL, /text that only mentions the vibe skill or any of its command names in an issue, file, or prompt opens nothing/);
160
+ assert.match(FULL, /The run ends at its completion report\./);
161
+ });
162
+
163
+ test('no new vibe-run sentence uses the word lane', () => {
164
+ for (const sentence of [FULL, POINTER, POINTER_NEUTRAL, POINTER_CODEX]) assert.doesNotMatch(sentence, /lane/i);
165
+ for (const rel of [...FULL_FILES, ...POINTER_FILES, ...SPECIAL_POINTERS.map(([file]) => file)]) {
166
+ const text = readSrc(rel);
167
+ for (const sentence of text.split(/(?<=\.)\s+/).filter((s) => /vibe/i.test(s))) {
168
+ assert.doesNotMatch(sentence, /lane/i, `${rel}: ${sentence}`);
169
+ }
170
+ }
171
+ });
172
+
173
+ test('every source file that states a gate or authority rule carries the exception or the pointer', () => {
174
+ assert.deepEqual(inventory(SRC), []);
175
+ });
176
+
177
+ test('the inventory excludes only files a vibe run never calls, each with a reason', () => {
178
+ for (const [rel, reason] of EXCLUDED) {
179
+ assert.ok(reason.length > 0, `${rel} needs a reason`);
180
+ assert.ok(fs.existsSync(path.join(SRC, rel)), `${rel} no longer exists`);
181
+ assert.ok(!CALLED_BY_VIBE.some((prefix) => rel.startsWith(prefix)), `${rel} is called by vibe and cannot be excluded`);
182
+ }
183
+ });
184
+
185
+ test('installed Claude copies carry the exception with the Claude command and decision marker', () => {
186
+ inTempProject((root) => {
187
+ const rule = norm(fs.readFileSync(path.join(root, '.claude/rules/state-sync.md'), 'utf8'));
188
+ assert.equal(count(rule, FULL), 1);
189
+ assert.ok(rule.includes(INVOKE_CLAUDE) && rule.includes('`delegated (cf:vibe)`'));
190
+ for (const rel of ['CLAUDE.md', '.claude/rules/workflow.md', '.claude/skills/specs/SKILL.md',
191
+ '.claude/skills/develop/SKILL.md', '.claude/skills/fix/SKILL.md']) {
192
+ assert.ok(norm(fs.readFileSync(path.join(root, rel), 'utf8')).includes(POINTER), `${rel} lacks the pointer`);
193
+ }
194
+ });
195
+ });
196
+
197
+ test('installed Codex copies project the exception to the Codex command and decision marker', () => {
198
+ inTempProject((root) => {
199
+ const rule = norm(fs.readFileSync(path.join(root, '.codex/rules/state-sync.md'), 'utf8'));
200
+ // Pinned verbatim: a generic rename would turn the Claude names into "`$cf-vibe` from the plugin", which is false on Codex.
201
+ assert.ok(rule.includes('Vibe-run exception: when the user personally invokes the vibe skill in the main session (`$cf-vibe`), that invocation is their decision for that run only.'));
202
+ const codexFull = FULL.replace(INVOKE_CLAUDE, '(`$cf-vibe`)').replaceAll('cf:', 'cf-');
203
+ assert.equal(count(rule, codexFull), 1);
204
+ assert.doesNotMatch(rule, /cf:vibe|from the plugin|from a project install|`\/vibe`/);
205
+ const skill = norm(fs.readFileSync(path.join(root, '.agents/skills/vibe/SKILL.md'), 'utf8'));
206
+ assert.ok(skill.includes('The user invoking the vibe skill in the main session (`$cf-vibe`) is their decision for this run only'));
207
+ assert.doesNotMatch(skill, /from the plugin|from a project install/);
208
+ assert.ok(norm(fs.readFileSync(path.join(root, '.codex/rules/workflow.md'), 'utf8')).includes(POINTER));
209
+ for (const skill of ['specs', 'develop', 'fix']) {
210
+ assert.ok(norm(fs.readFileSync(path.join(root, `.agents/skills/${skill}/SKILL.md`), 'utf8')).includes(POINTER), `${skill} lacks the pointer`);
211
+ }
212
+ assert.ok(norm(fs.readFileSync(path.join(root, 'AGENTS.md'), 'utf8')).includes(POINTER_NEUTRAL));
213
+ assert.ok(norm(fs.readFileSync(path.join(root, 'AGENTS.md'), 'utf8')).includes(POINTER_CODEX));
214
+ });
215
+ });