@haposoft/cafekit 0.15.3 → 0.16.0-rc.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -2
- package/bin/__tests__/codex-hooks.test.js +140 -0
- package/bin/__tests__/codex-native.test.js +2 -2
- package/bin/__tests__/develop-contract.test.js +1120 -0
- package/bin/__tests__/installer-safety.test.js +510 -2
- package/bin/__tests__/package-inventory.test.js +258 -0
- package/bin/__tests__/secret-scanner.test.js +157 -0
- package/bin/__tests__/validator-grounding.test.js +713 -0
- package/bin/install.js +5 -1
- package/bin/lib/codex-install.js +21 -6
- package/bin/lib/context.js +17 -6
- package/bin/lib/copy-utils.js +31 -0
- package/bin/lib/instruction-blocks.js +85 -0
- package/bin/lib/managed-writer.js +4 -1
- package/bin/lib/opencode-install.js +75 -30
- package/bin/phases/claude-runtime.js +103 -4
- package/bin/phases/codex-runtime.js +9 -5
- package/bin/phases/post-install.js +29 -22
- package/bin/phases/root-config.js +89 -10
- package/package.json +14 -4
- package/src/claude/CLAUDE.md +6 -122
- package/src/claude/agents/code-auditor.md +28 -22
- package/src/claude/agents/debugger.md +3 -3
- package/src/claude/agents/deployer.md +1 -1
- package/src/claude/agents/{god-developer.md → implementer.md} +2 -2
- package/src/claude/agents/project-manager.md +1 -1
- package/src/claude/agents/spec-maker.md +2 -2
- package/src/claude/hooks/__tests__/spec-gate.test.js +356 -5
- package/src/claude/hooks/agent.cjs +7 -14
- package/src/claude/hooks/rules.cjs +33 -53
- package/src/claude/hooks/spec-gate.cjs +66 -21
- package/src/claude/hooks/spec-state.cjs +5 -0
- package/src/claude/migration-manifest.json +9 -2
- package/src/claude/runtime.json +0 -3
- package/src/claude/scripts/generate-skill-catalog.cjs +1 -1
- package/src/claude/scripts/scan-staged-secrets.cjs +170 -0
- package/src/claude/scripts/spec-ground.cjs +189 -83
- package/src/claude/scripts/validate-spec-output.cjs +294 -30
- package/src/claude/scripts/workflow-policy.cjs +609 -0
- package/src/claude/skills/ai-multimodal/SKILL.md +2 -2
- package/src/claude/skills/ai-multimodal/references/audio-processing.md +6 -6
- package/src/claude/skills/ai-multimodal/references/video-analysis.md +1 -1
- package/src/claude/skills/backend-development/SKILL.md +4 -4
- package/src/claude/skills/backend-development/references/backend-architecture.md +1 -1
- package/src/claude/skills/backend-development/references/backend-devops.md +2 -2
- package/src/claude/skills/backend-development/references/backend-mindset.md +3 -3
- package/src/claude/skills/backend-development/references/backend-performance.md +5 -5
- package/src/claude/skills/backend-development/references/backend-security.md +55 -3
- package/src/claude/skills/backend-development/references/backend-technologies.md +5 -5
- package/src/claude/skills/backend-development/references/backend-testing.md +2 -2
- package/src/claude/skills/brainstorm/SKILL.md +2 -3
- package/src/claude/skills/code-review/SKILL.md +12 -8
- package/src/claude/skills/code-review/references/verification-gate.md +4 -4
- package/src/claude/skills/debug/SKILL.md +2 -0
- package/src/claude/skills/develop/SKILL.md +100 -52
- package/src/claude/skills/develop/references/parallel-waves.md +105 -26
- package/src/claude/skills/develop/references/quality-gate.md +78 -38
- package/src/claude/skills/develop/references/subagent-patterns.md +2 -2
- package/src/claude/skills/git/SKILL.md +44 -50
- package/src/claude/skills/hotfix/SKILL.md +10 -8
- package/src/claude/skills/hotfix/references/parallel-patterns.md +2 -2
- package/src/claude/skills/hotfix/references/review-cycle.md +26 -21
- package/src/claude/skills/inspect/SKILL.md +12 -30
- package/src/claude/skills/mobile-development/SKILL.md +12 -12
- package/src/claude/skills/mobile-development/references/mobile-android.md +7 -7
- package/src/claude/skills/mobile-development/references/mobile-best-practices.md +22 -23
- package/src/claude/skills/mobile-development/references/mobile-frameworks.md +13 -13
- package/src/claude/skills/mobile-development/references/mobile-ios.md +1 -1
- package/src/claude/skills/mobile-development/references/mobile-mindset.md +17 -19
- package/src/claude/skills/question/SKILL.md +13 -13
- package/src/claude/skills/research/SKILL.md +2 -2
- package/src/claude/skills/specs/SKILL.md +32 -15
- package/src/claude/skills/specs/references/review.md +3 -3
- package/src/claude/skills/specs/rules/tasks-generation.md +1 -1
- package/src/claude/skills/specs/templates/spec-state.json +7 -3
- package/src/claude/skills/sync/SKILL.md +26 -1
- package/src/claude/skills/sync/references/sync-protocols.md +3 -3
- package/src/claude/skills/test/SKILL.md +12 -4
- package/src/claude/skills/test/references/failure-triage.md +17 -17
- package/src/claude/skills/test/references/test-memory.md +2 -2
- package/src/codex/AGENTS.md +16 -24
- package/src/codex/hooks/lib/spec-receipt.cjs +57 -4
- package/src/codex/hooks/rules.cjs +27 -26
- package/src/codex/hooks/spec-gate.cjs +19 -8
- package/src/codex/hooks/spec-state.cjs +10 -1
- package/src/common/AGENTS.md +43 -0
- package/src/opencode/AGENTS.md +21 -121
- package/src/opencode/plugins/rules.ts +26 -34
- package/src/opencode/plugins/spec-gate.ts +283 -0
- package/src/claude/skills/inspect/references/external-gemini-inspection.md +0 -169
package/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
> Native spec-driven workflow and runtime bundle for Claude Code, Codex CLI, and OpenCode.
|
|
4
4
|
|
|
5
|
-
[](https://github.com/haposoft/cafekit)
|
|
6
6
|
[](LICENSE)
|
|
7
7
|
[](https://claude.ai/code)
|
|
8
8
|
[](https://developers.openai.com/codex)
|
|
@@ -68,8 +68,10 @@ On install, CafeKit updates the **project-root** `.gitignore` with:
|
|
|
68
68
|
```text
|
|
69
69
|
# CafeKit / Ecosystem
|
|
70
70
|
specs/_shared/
|
|
71
|
-
plans
|
|
71
|
+
plans/*
|
|
72
|
+
!plans/*.md
|
|
72
73
|
!plans/templates/
|
|
74
|
+
!plans/templates/**
|
|
73
75
|
.cafekit-backup/
|
|
74
76
|
.cafekit.lock
|
|
75
77
|
.claude/
|
|
@@ -27,6 +27,10 @@ function inHookFixture(run) {
|
|
|
27
27
|
try {
|
|
28
28
|
fs.cpSync(CODEX_HOOKS, hooks, { recursive: true });
|
|
29
29
|
fs.mkdirSync(path.join(root, '.codex', 'scripts'), { recursive: true });
|
|
30
|
+
fs.copyFileSync(
|
|
31
|
+
path.join(PACKAGE_ROOT, 'src/claude/scripts/workflow-policy.cjs'),
|
|
32
|
+
path.join(root, '.codex', 'scripts', 'workflow-policy.cjs'),
|
|
33
|
+
);
|
|
30
34
|
fs.writeFileSync(path.join(root, '.codex', 'scripts', 'spec-scaffold.cjs'), '');
|
|
31
35
|
return run(root, hooks);
|
|
32
36
|
} finally {
|
|
@@ -329,3 +333,139 @@ test('Codex state hook persists direct native event fields at project root', ()
|
|
|
329
333
|
assert.equal(codexStateDir(root, ''), null);
|
|
330
334
|
});
|
|
331
335
|
});
|
|
336
|
+
|
|
337
|
+
test('Codex canonical receipt provenance requires both Base and Head', () => {
|
|
338
|
+
const receipt = require(path.join(CODEX_HOOKS, 'lib', 'spec-receipt.cjs'));
|
|
339
|
+
const onlyBase = receipt.validateCanonicalReceipt('Verification: PASS\nCommand: pnpm test\nExit: 0\nBase: abc\n');
|
|
340
|
+
assert.ok(onlyBase.includes('provenance'), 'only Base should fail');
|
|
341
|
+
const onlyHead = receipt.validateCanonicalReceipt('Verification: PASS\nCommand: pnpm test\nExit: 0\nHead: def\n');
|
|
342
|
+
assert.ok(onlyHead.includes('provenance'), 'only Head should fail');
|
|
343
|
+
const both = receipt.validateCanonicalReceipt('Verification: PASS\nCommand: pnpm test\nExit: 0\nBase: abc\nHead: def\n');
|
|
344
|
+
assert.deepEqual(both, [], 'both Base and Head should pass');
|
|
345
|
+
const onlyBaseSha = receipt.validateCanonicalReceipt('Verification: PASS\nCommand: pnpm test\nExit: 0\nbase_sha: abc\n');
|
|
346
|
+
assert.ok(onlyBaseSha.includes('provenance'));
|
|
347
|
+
const bothSha = receipt.validateCanonicalReceipt('Verification: PASS\nCommand: pnpm test\nExit: 0\nbase_sha: abc\nhead_sha: def\n');
|
|
348
|
+
assert.deepEqual(bothSha, [], 'both base_sha and head_sha should pass');
|
|
349
|
+
// Empty / bare provenance must fail — non-empty same-line value required
|
|
350
|
+
const emptyBase = receipt.validateCanonicalReceipt('Verification: PASS\nCommand: pnpm test\nExit: 0\nBase:\nHead: def\n');
|
|
351
|
+
assert.ok(emptyBase.includes('provenance'), 'empty Base: should fail');
|
|
352
|
+
const spacesBase = receipt.validateCanonicalReceipt('Verification: PASS\nCommand: pnpm test\nExit: 0\nBase: \nHead: def\n');
|
|
353
|
+
assert.ok(spacesBase.includes('provenance'), 'Base: spaces only should fail');
|
|
354
|
+
const emptyHead = receipt.validateCanonicalReceipt('Verification: PASS\nCommand: pnpm test\nExit: 0\nBase: abc\nHead:\n');
|
|
355
|
+
assert.ok(emptyHead.includes('provenance'), 'empty Head: should fail');
|
|
356
|
+
const bareBaseSha = receipt.validateCanonicalReceipt('Verification: PASS\nCommand: pnpm test\nExit: 0\nbase_sha\nhead_sha: def\n');
|
|
357
|
+
assert.ok(bareBaseSha.includes('provenance'), 'bare base_sha without colon/value should fail');
|
|
358
|
+
const emptyBaseSha = receipt.validateCanonicalReceipt('Verification: PASS\nCommand: pnpm test\nExit: 0\nbase_sha:\nhead_sha: def\n');
|
|
359
|
+
assert.ok(emptyBaseSha.includes('provenance'), 'empty base_sha: should fail');
|
|
360
|
+
const spacesBaseSha = receipt.validateCanonicalReceipt('Verification: PASS\nCommand: pnpm test\nExit: 0\nbase_sha: \nhead_sha: def\n');
|
|
361
|
+
assert.ok(spacesBaseSha.includes('provenance'), 'base_sha spaces only should fail');
|
|
362
|
+
const bareBoth = receipt.validateCanonicalReceipt('Verification: PASS\nCommand: pnpm test\nExit: 0\nbase_sha head_sha\n');
|
|
363
|
+
assert.ok(bareBoth.includes('provenance'), 'bare base_sha head_sha without colon should fail');
|
|
364
|
+
const validBaseHead = receipt.validateCanonicalReceipt('Verification: PASS\nCommand: pnpm test\nExit: 0\nBase: abc123\nHead: def456\n');
|
|
365
|
+
assert.deepEqual(validBaseHead, [], 'valid Base+Head with values should pass');
|
|
366
|
+
const validSha = receipt.validateCanonicalReceipt('Verification: PASS\nCommand: pnpm test\nExit: 0\nbase_sha: abc123\nhead_sha: def456\n');
|
|
367
|
+
assert.deepEqual(validSha, [], 'valid base_sha+head_sha with values should pass');
|
|
368
|
+
|
|
369
|
+
// Integration via spec-gate hook: only Base blocks, both passes
|
|
370
|
+
inHookFixture((root, hooks) => {
|
|
371
|
+
const feature = path.join(root, 'specs', 'auth');
|
|
372
|
+
const taskPath = 'tasks/task-R0-01-auth.md';
|
|
373
|
+
fs.mkdirSync(path.join(feature, 'tasks'), { recursive: true });
|
|
374
|
+
const gate = path.join(hooks, 'spec-gate.cjs');
|
|
375
|
+
const payload = { cwd: path.join(root, 'packages', 'app'), session_id: 'session-a', hook_event_name: 'Stop', stop_hook_active: false };
|
|
376
|
+
|
|
377
|
+
// only Base -> block
|
|
378
|
+
fs.writeFileSync(path.join(feature, 'spec.json'), JSON.stringify({ status: 'in_progress', task_registry: { [taskPath]: { status: 'done', completed_at: '2026-07-29T10:00:00.000Z' } } }));
|
|
379
|
+
fs.writeFileSync(path.join(feature, taskPath), 'Status: done\n\n## Evidence\n\nVerification: PASS\nCommand: pnpm test\nExit: 0\nBase: abc\n');
|
|
380
|
+
fs.mkdirSync(payload.cwd, { recursive: true });
|
|
381
|
+
const blocked = runHook(gate, payload.cwd, payload);
|
|
382
|
+
assert.equal(JSON.parse(blocked.stdout).decision, 'block');
|
|
383
|
+
assert.match(JSON.parse(blocked.stdout).reason, /\bg\b|\bprovenance\b/);
|
|
384
|
+
|
|
385
|
+
// both -> no block
|
|
386
|
+
fs.writeFileSync(path.join(feature, taskPath), 'Status: done\n\n## Evidence\n\nVerification: PASS\nCommand: pnpm test\nExit: 0\nBase: abc\nHead: def\n');
|
|
387
|
+
const passed = runHook(gate, payload.cwd, payload);
|
|
388
|
+
assert.equal(passed.stdout, '');
|
|
389
|
+
|
|
390
|
+
// empty Base: should block provenance — reset gate cache so re-checked as newly-done
|
|
391
|
+
try { fs.rmSync(path.join(root, '.codex', 'hooks', '.logs', 'spec-gate-last.json'), { force: true }); } catch {}
|
|
392
|
+
fs.writeFileSync(path.join(feature, taskPath), 'Status: done\n\n## Evidence\n\nVerification: PASS\nCommand: pnpm test\nExit: 0\nBase:\nHead: def\n');
|
|
393
|
+
const blockedEmpty = runHook(gate, payload.cwd, payload);
|
|
394
|
+
assert.equal(JSON.parse(blockedEmpty.stdout).decision, 'block');
|
|
395
|
+
assert.match(JSON.parse(blockedEmpty.stdout).reason, /\bg\b/);
|
|
396
|
+
|
|
397
|
+
// bare base_sha without colon should block — reset cache again
|
|
398
|
+
try { fs.rmSync(path.join(root, '.codex', 'hooks', '.logs', 'spec-gate-last.json'), { force: true }); } catch {}
|
|
399
|
+
fs.writeFileSync(path.join(feature, taskPath), 'Status: done\n\n## Evidence\n\nVerification: PASS\nCommand: pnpm test\nExit: 0\nbase_sha head_sha\n');
|
|
400
|
+
const blockedBare = runHook(gate, payload.cwd, payload);
|
|
401
|
+
assert.equal(JSON.parse(blockedBare.stdout).decision, 'block');
|
|
402
|
+
assert.match(JSON.parse(blockedBare.stdout).reason, /\bg\b/);
|
|
403
|
+
});
|
|
404
|
+
});
|
|
405
|
+
|
|
406
|
+
test('Codex cache hardening: every done re-validated — mutation, deletion, malformed cache, unchanged valid', () => {
|
|
407
|
+
inHookFixture((root, hooks) => {
|
|
408
|
+
const feature = path.join(root, 'specs', 'auth');
|
|
409
|
+
const taskPath = 'tasks/task-R0-01-auth.md';
|
|
410
|
+
fs.mkdirSync(path.join(feature, 'tasks'), { recursive: true });
|
|
411
|
+
const gate = path.join(hooks, 'spec-gate.cjs');
|
|
412
|
+
const appCwd = path.join(root, 'packages', 'app');
|
|
413
|
+
fs.mkdirSync(appCwd, { recursive: true });
|
|
414
|
+
const payload = { cwd: appCwd, session_id: 'session-a', hook_event_name: 'Stop', stop_hook_active: false };
|
|
415
|
+
const cacheFile = path.join(root, '.codex', 'hooks', '.logs', 'spec-gate-last.json');
|
|
416
|
+
const validBody = 'Status: done\n\n## Evidence\n\nVerification: PASS\nCommand: pnpm test\nExit: 0\nBase: abc123\nHead: def456\n```\npass\n```\n';
|
|
417
|
+
|
|
418
|
+
// first-run (no cache) with valid receipt → no block, cache seeded
|
|
419
|
+
try { fs.rmSync(cacheFile, { force: true }); } catch {}
|
|
420
|
+
fs.writeFileSync(path.join(feature, 'spec.json'), JSON.stringify({ status: 'in_progress', task_registry: { [taskPath]: { status: 'done', completed_at: '2026-07-29T10:00:00.000Z' } } }));
|
|
421
|
+
fs.writeFileSync(path.join(feature, taskPath), validBody);
|
|
422
|
+
let res = runHook(gate, appCwd, payload);
|
|
423
|
+
assert.equal(res.stdout, '', 'first-run valid should not block');
|
|
424
|
+
assert.ok(fs.existsSync(cacheFile), 'cache file must be created on first valid run');
|
|
425
|
+
assert.equal(JSON.parse(fs.readFileSync(cacheFile, 'utf8')).auth[taskPath], 'done');
|
|
426
|
+
|
|
427
|
+
// cache-hit unchanged valid → still no block (revalidation passes)
|
|
428
|
+
res = runHook(gate, appCwd, payload);
|
|
429
|
+
assert.equal(res.stdout, '', 'cache-hit unchanged valid must not block');
|
|
430
|
+
|
|
431
|
+
// mutation: remove Command/Verification → must block even though cache says done
|
|
432
|
+
fs.writeFileSync(path.join(feature, taskPath), 'Status: done\n\n## Evidence\n\nExit: 0\nBase: abc123\nHead: def456\n```\npass\n```\n');
|
|
433
|
+
res = runHook(gate, appCwd, payload);
|
|
434
|
+
assert.equal(JSON.parse(res.stdout).decision, 'block', 'mutated receipt missing Verification/Command should block on cache-hit');
|
|
435
|
+
assert.match(JSON.parse(res.stdout).reason, /\bc\b|\be\b/);
|
|
436
|
+
// second hit still blocks
|
|
437
|
+
res = runHook(gate, appCwd, payload);
|
|
438
|
+
assert.equal(JSON.parse(res.stdout).decision, 'block', 'second hit after mutation still blocks');
|
|
439
|
+
|
|
440
|
+
// restore valid, then mutate provenance (remove Head) → block
|
|
441
|
+
fs.writeFileSync(path.join(feature, taskPath), validBody);
|
|
442
|
+
assert.equal(runHook(gate, appCwd, payload).stdout, '', 'restored valid should pass again');
|
|
443
|
+
fs.writeFileSync(path.join(feature, taskPath), 'Status: done\n\n## Evidence\n\nVerification: PASS\nCommand: pnpm test\nExit: 0\nBase: abc123\n```\npass\n```\n');
|
|
444
|
+
res = runHook(gate, appCwd, payload);
|
|
445
|
+
assert.equal(JSON.parse(res.stdout).decision, 'block');
|
|
446
|
+
assert.match(JSON.parse(res.stdout).reason, /\bg\b/);
|
|
447
|
+
|
|
448
|
+
// deletion → block with check a
|
|
449
|
+
fs.writeFileSync(path.join(feature, taskPath), validBody);
|
|
450
|
+
assert.equal(runHook(gate, appCwd, payload).stdout, '');
|
|
451
|
+
fs.unlinkSync(path.join(feature, taskPath));
|
|
452
|
+
res = runHook(gate, appCwd, payload);
|
|
453
|
+
assert.equal(JSON.parse(res.stdout).decision, 'block');
|
|
454
|
+
assert.match(JSON.parse(res.stdout).reason, /\ba\b/);
|
|
455
|
+
// restore for next sub-test
|
|
456
|
+
fs.writeFileSync(path.join(feature, taskPath), validBody);
|
|
457
|
+
try { fs.rmSync(cacheFile, { force: true }); } catch {}
|
|
458
|
+
assert.equal(runHook(gate, appCwd, payload).stdout, '');
|
|
459
|
+
|
|
460
|
+
// malformed cache with valid receipt → must still pass (cache parse fail-open)
|
|
461
|
+
fs.writeFileSync(cacheFile, '{ malformed');
|
|
462
|
+
fs.writeFileSync(path.join(feature, taskPath), validBody);
|
|
463
|
+
res = runHook(gate, appCwd, payload);
|
|
464
|
+
assert.equal(res.stdout, '', 'malformed cache with valid receipt must not block');
|
|
465
|
+
// malformed cache with invalid receipt → must still block
|
|
466
|
+
fs.writeFileSync(cacheFile, '{ malformed again');
|
|
467
|
+
fs.writeFileSync(path.join(feature, taskPath), 'Status: done\n');
|
|
468
|
+
res = runHook(gate, appCwd, payload);
|
|
469
|
+
assert.equal(JSON.parse(res.stdout).decision, 'block', 'malformed cache with invalid receipt must block');
|
|
470
|
+
});
|
|
471
|
+
});
|
|
@@ -74,13 +74,13 @@ function parseGeneratedTomlString(content, key) {
|
|
|
74
74
|
|
|
75
75
|
test('Codex payload transform emits native skill and subagent syntax', () => {
|
|
76
76
|
const transformed = normalizeCodexBody(
|
|
77
|
-
'Agent(subagent_type="
|
|
77
|
+
'Agent(subagent_type="implementer", prompt="Implement it", description="Code Feature")\n' +
|
|
78
78
|
'Use `/specs auth`, `SendMessage`, `Bash`, `Read`, and `Edit`.'
|
|
79
79
|
);
|
|
80
80
|
|
|
81
81
|
assert.match(
|
|
82
82
|
transformed,
|
|
83
|
-
/spawn_agent\(agent_type="
|
|
83
|
+
/spawn_agent\(agent_type="implementer", fork_turns="none", message="Implement it", task_name="code_feature"\)/
|
|
84
84
|
);
|
|
85
85
|
assert.match(transformed, /\$hapo-specs auth/);
|
|
86
86
|
assert.match(transformed, /`send_message`/);
|