@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.
- package/README.md +21 -58
- package/bin/__tests__/change-firewall.semantic-firewall.test.js +4 -5
- package/bin/__tests__/codex-hooks.test.js +137 -0
- package/bin/__tests__/codex-native.test.js +130 -74
- package/bin/__tests__/develop-contract.test.js +155 -76
- package/bin/__tests__/installer-safety.test.js +19 -154
- package/bin/__tests__/package-inventory.test.js +10 -20
- package/bin/__tests__/release-preflight.semantic-firewall.test.js +13 -5
- package/bin/__tests__/validator-grounding.test.js +11 -11
- package/bin/install.js +2 -7
- package/bin/lib/context.js +0 -45
- package/bin/lib/copy-utils.js +1 -1
- package/bin/lib/i18n.js +0 -3
- package/bin/lib/manifest.js +1 -1
- package/bin/phases/claude-runtime.js +1 -3
- package/bin/phases/codex-runtime.js +4 -2
- package/bin/phases/copy-payload.js +3 -15
- package/bin/phases/post-install.js +5 -17
- package/bin/phases/root-config.js +2 -3
- package/bin/phases/select-platform.js +2 -5
- package/bin/phases/summary.js +0 -1
- package/package.json +3 -8
- package/src/claude/CLAUDE.md +19 -6
- package/src/claude/agents/project-manager.md +41 -9
- package/src/claude/agents/spec-maker.md +76 -155
- package/src/claude/gitignore +1 -1
- package/src/claude/hooks/__tests__/spec-gate.test.js +162 -0
- package/src/claude/hooks/lib/context.cjs +1 -1
- package/src/claude/hooks/lib/detect.cjs +1 -1
- package/src/claude/hooks/spec-gate.cjs +44 -9
- package/src/claude/hooks/spec-state.cjs +22 -11
- package/src/claude/hooks/task-scaffold-guard.cjs +1 -1
- package/src/claude/migration-manifest.json +17 -3
- package/src/claude/rules/manage-docs.md +43 -24
- package/src/claude/rules/state-sync.md +57 -62
- package/src/claude/rules/workflow.md +30 -13
- package/src/claude/scripts/change-firewall.cjs +2 -2
- package/src/claude/scripts/generate-skill-catalog.cjs +0 -1
- package/src/claude/scripts/spec-receipt.cjs +141 -1
- package/src/claude/scripts/spec-resolver.cjs +288 -0
- package/src/claude/skills/develop/SKILL.md +89 -125
- package/src/claude/skills/develop/references/parallel-waves.md +75 -162
- package/src/claude/skills/develop/references/quality-gate.md +61 -121
- package/src/claude/skills/develop/references/subagent-patterns.md +31 -38
- package/src/claude/skills/question/SKILL.md +1 -1
- package/src/claude/skills/specs/SKILL.md +142 -204
- package/src/claude/skills/specs/references/review.md +109 -243
- package/src/claude/skills/specs/references/templates.md +173 -0
- package/src/claude/skills/sync/SKILL.md +66 -90
- package/src/claude/skills/sync/references/sync-protocols.md +62 -83
- package/src/codex/AGENTS.md +20 -7
- package/src/codex/hooks/lib/spec-receipt.cjs +18 -0
- package/src/codex/hooks/lib/spec-utils.cjs +25 -0
- package/src/codex/hooks/spec-gate.cjs +46 -16
- package/src/codex/hooks/spec-state.cjs +22 -12
- package/src/codex/rules/state-sync.md +57 -61
- package/src/common/AGENTS.md +8 -1
- package/bin/__tests__/opencode-spec-gate.test.js +0 -799
- package/bin/lib/opencode-install.js +0 -899
- package/bin/phases/opencode-runtime.js +0 -38
- package/src/claude/skills/specs/references/archive-workflow.md +0 -74
- package/src/claude/skills/specs/references/ask-user-question-gates.md +0 -70
- package/src/claude/skills/specs/references/codebase-analysis.md +0 -102
- package/src/claude/skills/specs/references/cross-spec-dependency.md +0 -76
- package/src/claude/skills/specs/references/research-strategy.md +0 -107
- package/src/claude/skills/specs/references/scope-inquiry.md +0 -107
- package/src/claude/skills/specs/references/translation-mirror.md +0 -83
- package/src/claude/skills/specs/rules/design-discovery-full.md +0 -45
- package/src/claude/skills/specs/rules/design-discovery-light.md +0 -28
- package/src/claude/skills/specs/rules/design-principles.md +0 -138
- package/src/claude/skills/specs/rules/design-review.md +0 -55
- package/src/claude/skills/specs/rules/ears-format.md +0 -42
- package/src/claude/skills/specs/rules/phase-decision-matrix.md +0 -56
- package/src/claude/skills/specs/rules/task-scoring-rubric.md +0 -52
- package/src/claude/skills/specs/rules/tasks-generation.md +0 -189
- package/src/opencode/AGENTS.md +0 -41
- package/src/opencode/plugins/docs-sync.ts +0 -175
- package/src/opencode/plugins/inspect-block.ts +0 -164
- package/src/opencode/plugins/privacy-block.ts +0 -182
- package/src/opencode/plugins/rules.ts +0 -155
- package/src/opencode/plugins/session.ts +0 -179
- package/src/opencode/plugins/spec-gate.ts +0 -457
- package/src/opencode/plugins/spec-state.ts +0 -311
- package/src/opencode/plugins/state.ts +0 -253
- package/src/opencode/plugins/task-scaffold-guard.ts +0 -161
- 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
|
|
3
|
+
> Native spec-driven workflow and runtime bundle for Claude Code and Codex CLI.
|
|
4
4
|
|
|
5
5
|
[](https://github.com/haposoft/cafekit)
|
|
6
6
|
[](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
|
|
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
|
-
|
|
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
|
-
├──
|
|
261
|
-
├──
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
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
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
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
|
-
|
|
158
|
-
|
|
159
|
-
|
|
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
|
-
|
|
166
|
-
issues.push(
|
|
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 (
|
|
169
|
-
|
|
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 (
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
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 (
|
|
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('
|
|
185
|
-
|| !files.codex.includes('
|
|
186
|
-
|| !files.codex.includes('
|
|
187
|
-
|
|
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
|
|
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('
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
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
|
-
['
|
|
215
|
-
['review', (value) =>
|
|
216
|
-
['
|
|
217
|
-
['
|
|
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', '.
|
|
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', '
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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']);
|