@haposoft/cafekit 0.16.0-rc.7 → 0.16.0-rc.8
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/bin/__tests__/codex-hooks.test.js +245 -0
- package/bin/__tests__/codex-native.test.js +463 -20
- package/bin/__tests__/package-inventory.test.js +484 -6
- package/package.json +1 -1
- package/src/claude/agents/spec-maker.md +14 -8
- package/src/claude/hooks/__tests__/spec-gate.test.js +77 -1
- package/src/claude/hooks/__tests__/state.test.js +249 -3
- package/src/claude/hooks/spec-state.cjs +30 -3
- package/src/claude/scripts/spec-receipt.cjs +76 -21
- package/src/claude/scripts/spec-resolver.cjs +144 -4
- package/src/claude/skills/specs/SKILL.md +17 -7
- package/src/claude/skills/specs/references/review.md +15 -0
- package/src/claude/skills/specs/references/templates.md +49 -20
- package/src/codex/hooks/spec-state.cjs +32 -4
|
@@ -138,6 +138,219 @@ function inHookFixture(run, options = {}) {
|
|
|
138
138
|
}
|
|
139
139
|
}
|
|
140
140
|
|
|
141
|
+
function processTaskContent(title, status, dependency = 'none') {
|
|
142
|
+
return [
|
|
143
|
+
`# ${title}`, '', `Status: ${status}`, '',
|
|
144
|
+
'## Dependencies', '', `- ${dependency}`, '',
|
|
145
|
+
'## Verification Plan', '', '- Command: node --test', '',
|
|
146
|
+
].join('\n');
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function canonicalProcessTask(root, title, dependency = 'none') {
|
|
150
|
+
const context = workflowRuntimeContext(root, 'auth', 'session-auth-state');
|
|
151
|
+
return [
|
|
152
|
+
processTaskContent(title, 'done', dependency),
|
|
153
|
+
'## Receipt', '', 'Verification: PASS', 'Command: node --test', 'Exit: 0',
|
|
154
|
+
`Base: ${context.base}`, `Head: ${context.head}`,
|
|
155
|
+
'```text', 'node --test', '1 test passed', '```', '',
|
|
156
|
+
].join('\n');
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function inInstalledProcessSpecStateFixture(run) {
|
|
160
|
+
return inHookFixture((root, hooks) => {
|
|
161
|
+
const feature = path.join(root, 'specs', 'auth');
|
|
162
|
+
fs.mkdirSync(feature, { recursive: true });
|
|
163
|
+
fs.writeFileSync(path.join(feature, 'plan.md'), '# Auth plan\nSpecs-Contract: process-first-ready-v1\n');
|
|
164
|
+
|
|
165
|
+
const writeTask = (basename, title, status, dependency = 'none') => {
|
|
166
|
+
fs.writeFileSync(
|
|
167
|
+
path.join(feature, basename),
|
|
168
|
+
processTaskContent(title, status, dependency),
|
|
169
|
+
);
|
|
170
|
+
};
|
|
171
|
+
const runState = () => runHook(path.join(hooks, 'spec-state.cjs'), root, {
|
|
172
|
+
cwd: root,
|
|
173
|
+
featureName: 'auth',
|
|
174
|
+
session_id: 'session-auth-state',
|
|
175
|
+
hook_event_name: 'UserPromptSubmit',
|
|
176
|
+
prompt: 'Continue',
|
|
177
|
+
});
|
|
178
|
+
const findCacheFile = () => {
|
|
179
|
+
const cacheDir = path.join(hooks, '.logs');
|
|
180
|
+
const cacheNames = fs.existsSync(cacheDir)
|
|
181
|
+
? fs.readdirSync(cacheDir).filter((file) => file.startsWith('tollgate-'))
|
|
182
|
+
: [];
|
|
183
|
+
assert.equal(cacheNames.length, 1, 'one installed session cache must exist');
|
|
184
|
+
return path.join(cacheDir, cacheNames[0]);
|
|
185
|
+
};
|
|
186
|
+
return run({
|
|
187
|
+
feature,
|
|
188
|
+
findCacheFile,
|
|
189
|
+
hooks,
|
|
190
|
+
root,
|
|
191
|
+
runState,
|
|
192
|
+
writePlan: (content) => fs.writeFileSync(path.join(feature, 'plan.md'), content),
|
|
193
|
+
writeTask,
|
|
194
|
+
});
|
|
195
|
+
});
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
test('Codex installed spec-state re-evaluates a standalone task when blocked becomes pending', () => {
|
|
199
|
+
inInstalledProcessSpecStateFixture(({ findCacheFile, hooks, runState, writeTask }) => {
|
|
200
|
+
writeTask('task-01-auth.md', 'Task 01: auth', 'blocked');
|
|
201
|
+
|
|
202
|
+
const blocked = runState();
|
|
203
|
+
assert.equal(blocked.status, 0, blocked.stderr);
|
|
204
|
+
assert.match(blocked.stdout, /Spec state changed: `auth`/);
|
|
205
|
+
assert.doesNotMatch(blocked.stdout, /Next unblocked/);
|
|
206
|
+
const cacheFile = findCacheFile();
|
|
207
|
+
assert.ok(cacheFile.startsWith(`${hooks}${path.sep}`), 'cache must stay inside temporary installed hooks');
|
|
208
|
+
const blockedCache = fs.readFileSync(cacheFile, 'utf8');
|
|
209
|
+
|
|
210
|
+
writeTask('task-01-auth.md', 'Task 01: auth', 'pending');
|
|
211
|
+
const pending = runState();
|
|
212
|
+
assert.equal(pending.status, 0, pending.stderr);
|
|
213
|
+
assert.match(pending.stdout, /Spec state changed: `auth`/);
|
|
214
|
+
assert.match(pending.stdout, /Next unblocked: `task-01-auth\.md`/);
|
|
215
|
+
assert.notEqual(fs.readFileSync(cacheFile, 'utf8'), blockedCache, 'state cache must change');
|
|
216
|
+
});
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
test('Codex installed spec-state requires current canonical dependency proof and keys receipt mutations', () => {
|
|
220
|
+
inInstalledProcessSpecStateFixture(({ feature, findCacheFile, hooks, root, runState, writeTask }) => {
|
|
221
|
+
const predecessor = 'task-01-semantic-review.md';
|
|
222
|
+
const dependent = 'task-02-implementation.md';
|
|
223
|
+
writeTask(predecessor, 'Task 01: semantic review', 'blocked');
|
|
224
|
+
writeTask(dependent, 'Task 02: implementation', 'pending', predecessor);
|
|
225
|
+
const dependentPath = path.join(feature, dependent);
|
|
226
|
+
const unchangedDependent = fs.readFileSync(dependentPath, 'utf8');
|
|
227
|
+
|
|
228
|
+
const blocked = runState();
|
|
229
|
+
assert.equal(blocked.status, 0, blocked.stderr);
|
|
230
|
+
assert.match(blocked.stdout, /Tasks: 0 done \/ 2 total/);
|
|
231
|
+
assert.doesNotMatch(blocked.stdout, /Next unblocked/);
|
|
232
|
+
const cacheFile = findCacheFile();
|
|
233
|
+
assert.ok(cacheFile.startsWith(`${hooks}${path.sep}`), 'cache must stay inside temporary installed hooks');
|
|
234
|
+
const blockedCache = fs.readFileSync(cacheFile, 'utf8');
|
|
235
|
+
|
|
236
|
+
writeTask(predecessor, 'Task 01: semantic review', 'done');
|
|
237
|
+
const missing = runState();
|
|
238
|
+
assert.equal(missing.status, 0, missing.stderr);
|
|
239
|
+
assert.doesNotMatch(missing.stdout, /Next unblocked/);
|
|
240
|
+
const missingCache = fs.readFileSync(cacheFile, 'utf8');
|
|
241
|
+
assert.notEqual(missingCache, blockedCache, 'done without proof must change cached proof state');
|
|
242
|
+
|
|
243
|
+
fs.appendFileSync(path.join(feature, predecessor), '\n## Receipt\n\nVerification: PASS\n');
|
|
244
|
+
const malformed = runState();
|
|
245
|
+
assert.equal(malformed.status, 0, malformed.stderr);
|
|
246
|
+
assert.doesNotMatch(malformed.stdout, /Next unblocked/);
|
|
247
|
+
|
|
248
|
+
fs.writeFileSync(path.join(feature, predecessor), canonicalProcessTask(root, 'Task 01: semantic review'));
|
|
249
|
+
const ready = runState();
|
|
250
|
+
assert.equal(ready.status, 0, ready.stderr);
|
|
251
|
+
assert.match(ready.stdout, /Next unblocked: `task-02-implementation\.md`/);
|
|
252
|
+
const readyCache = fs.readFileSync(cacheFile, 'utf8');
|
|
253
|
+
assert.notEqual(readyCache, missingCache, 'canonical proof must change cached proof state');
|
|
254
|
+
|
|
255
|
+
fs.writeFileSync(
|
|
256
|
+
path.join(feature, predecessor),
|
|
257
|
+
fs.readFileSync(path.join(feature, predecessor), 'utf8').replace('Verification: PASS', 'Verification: FAIL'),
|
|
258
|
+
);
|
|
259
|
+
const mutated = runState();
|
|
260
|
+
assert.equal(mutated.status, 0, mutated.stderr);
|
|
261
|
+
assert.doesNotMatch(mutated.stdout, /Next unblocked/);
|
|
262
|
+
assert.notEqual(fs.readFileSync(cacheFile, 'utf8'), readyCache, 'receipt-only mutation must change cache');
|
|
263
|
+
assert.equal(fs.readFileSync(dependentPath, 'utf8'), unchangedDependent, 'dependent must remain pending');
|
|
264
|
+
});
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
test('Codex installed spec-state keeps unversioned pending packets out of Next with migration guidance', () => {
|
|
268
|
+
inInstalledProcessSpecStateFixture(({ feature, runState, writePlan, writeTask }) => {
|
|
269
|
+
const fencedMarkerPlan = '# Auth plan\n\n```markdown\nSpecs-Contract: process-first-ready-v1\n```\n';
|
|
270
|
+
writePlan(fencedMarkerPlan);
|
|
271
|
+
writeTask('task-01-auth.md', 'Task 01: auth', 'pending');
|
|
272
|
+
const taskBytes = fs.readFileSync(path.join(feature, 'task-01-auth.md'));
|
|
273
|
+
const result = runState();
|
|
274
|
+
assert.equal(result.status, 0, result.stderr);
|
|
275
|
+
assert.doesNotMatch(result.stdout, /Next unblocked/);
|
|
276
|
+
assert.match(result.stdout, /Migration required: add `Specs-Contract: process-first-ready-v1`/);
|
|
277
|
+
const cached = runState();
|
|
278
|
+
assert.equal(cached.status, 0, cached.stderr);
|
|
279
|
+
assert.doesNotMatch(cached.stdout, /Next unblocked/);
|
|
280
|
+
assert.match(cached.stdout, /Add `Specs-Contract: process-first-ready-v1`/);
|
|
281
|
+
|
|
282
|
+
writePlan(fencedMarkerPlan.replace('# Auth plan\n', '# Auth plan\nSpecs-Contract: process-first-ready-v1\n'));
|
|
283
|
+
const marked = runState();
|
|
284
|
+
assert.equal(marked.status, 0, marked.stderr);
|
|
285
|
+
assert.match(marked.stdout, /Spec state changed: `auth`/);
|
|
286
|
+
assert.match(marked.stdout, /Next unblocked: `task-01-auth\.md`/);
|
|
287
|
+
assert.deepEqual(fs.readFileSync(path.join(feature, 'task-01-auth.md')), taskBytes, 'marker-only transition must not change task bytes');
|
|
288
|
+
});
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
test('Codex resolver ignores fenced workflow annotations and rejects fenced-only Status', () => {
|
|
292
|
+
inInstalledProcessSpecStateFixture(({ feature, root, runState, writePlan, writeTask }) => {
|
|
293
|
+
const resolver = require(path.join(root, '.codex', 'scripts', 'spec-resolver.cjs'));
|
|
294
|
+
const trailingFence = resolver.annotatedMarkdownLines([
|
|
295
|
+
'```markdown', '``` trailing', 'Specs-Contract: process-first-ready-v1',
|
|
296
|
+
'Status: done', '## Receipt', '```',
|
|
297
|
+
].join('\n'));
|
|
298
|
+
assert.deepEqual(trailingFence.slice(2, 5).map(({ outsideFence }) => outsideFence), [false, false, false]);
|
|
299
|
+
const fourSpaceFence = resolver.annotatedMarkdownLines(' ```markdown\nStatus: pending');
|
|
300
|
+
assert.equal(fourSpaceFence[1].outsideFence, true);
|
|
301
|
+
writePlan('# Auth plan\nSpecs-Contract: process-first-ready-v1\n');
|
|
302
|
+
fs.writeFileSync(path.join(feature, 'task-01-fenced-only.md'), [
|
|
303
|
+
'# Task 01', '', '```markdown', 'Status: done', '```', '',
|
|
304
|
+
'## Dependencies', '', '- none', '', '## Verification Plan', '', '- Command: node --test', '',
|
|
305
|
+
].join('\n'));
|
|
306
|
+
const malformed = resolver.resolveWorkflowCandidate({ projectRoot: root, runtime: {}, explicitFeature: 'auth' });
|
|
307
|
+
assert.equal(malformed.error, 'explicit_malformed');
|
|
308
|
+
assert.match(malformed.reason, /must contain exactly one supported Status field/);
|
|
309
|
+
|
|
310
|
+
fs.rmSync(path.join(feature, 'task-01-fenced-only.md'));
|
|
311
|
+
writeTask('task-01-dependent.md', 'Task 01: dependent', 'pending', 'task-02-predecessor.md');
|
|
312
|
+
fs.writeFileSync(path.join(feature, 'task-02-predecessor.md'), [
|
|
313
|
+
'# Task 02', '', 'Status: pending', '', '## Dependencies', '', '- none', '',
|
|
314
|
+
'## Verification Plan', '', '- Command: node --test', '',
|
|
315
|
+
'~~~~markdown', 'Status: done', '## Dependencies', '- task-01-dependent.md', '## Receipt',
|
|
316
|
+
'Verification: PASS', 'Command: node --test', 'Exit: 0', '```text', 'fake pass', '```', '~~~~', '',
|
|
317
|
+
].join('\n'));
|
|
318
|
+
const result = runState();
|
|
319
|
+
assert.equal(result.status, 0, result.stderr);
|
|
320
|
+
assert.match(result.stdout, /Next unblocked: `task-02-predecessor\.md`/);
|
|
321
|
+
assert.doesNotMatch(result.stdout, /Next unblocked: `task-01-dependent\.md`/);
|
|
322
|
+
});
|
|
323
|
+
});
|
|
324
|
+
|
|
325
|
+
test('Codex resolver accepts unique legacy task numbers, rejects ambiguity, and rejects exact cycles', () => {
|
|
326
|
+
inInstalledProcessSpecStateFixture(({ feature, root, writePlan, writeTask }) => {
|
|
327
|
+
const resolver = require(path.join(root, '.codex', 'scripts', 'spec-resolver.cjs'));
|
|
328
|
+
writePlan('# Auth plan\n');
|
|
329
|
+
writeTask('task-01-a.md', 'Task 01: a', 'pending');
|
|
330
|
+
writeTask('task-02-b.md', 'Task 02: b', 'pending', '01');
|
|
331
|
+
fs.writeFileSync(path.join(feature, 'task-03-c.md'), '# Task 03: c\n\nStatus: pending\n');
|
|
332
|
+
const unique = resolver.resolveWorkflowCandidate({ projectRoot: root, runtime: {}, explicitFeature: 'auth' });
|
|
333
|
+
assert.deepEqual(unique.taskRegistry['task-02-b.md'].dependencies, ['task-01-a.md']);
|
|
334
|
+
assert.deepEqual(unique.taskRegistry['task-03-c.md'].dependencies, []);
|
|
335
|
+
assert.equal(unique.workflowContract, null);
|
|
336
|
+
assert.equal(unique.queueReady, false);
|
|
337
|
+
|
|
338
|
+
writeTask('task-01-second.md', 'Task 01: second', 'pending');
|
|
339
|
+
const ambiguous = resolver.resolveWorkflowCandidate({ projectRoot: root, runtime: {}, explicitFeature: 'auth' });
|
|
340
|
+
assert.equal(ambiguous.error, 'explicit_malformed');
|
|
341
|
+
assert.match(ambiguous.reason, /legacy dependency 01 maps to 2 task basenames/);
|
|
342
|
+
|
|
343
|
+
fs.rmSync(path.join(feature, 'task-01-second.md'));
|
|
344
|
+
fs.rmSync(path.join(feature, 'task-03-c.md'));
|
|
345
|
+
writePlan('# Auth plan\nSpecs-Contract: process-first-ready-v1\n');
|
|
346
|
+
writeTask('task-01-a.md', 'Task 01: a', 'pending', 'task-02-b.md');
|
|
347
|
+
writeTask('task-02-b.md', 'Task 02: b', 'pending', 'task-01-a.md');
|
|
348
|
+
const cycle = resolver.resolveWorkflowCandidate({ projectRoot: root, runtime: {}, explicitFeature: 'auth' });
|
|
349
|
+
assert.equal(cycle.error, 'explicit_malformed');
|
|
350
|
+
assert.match(cycle.reason, /dependency cycle detected: task-01-a\.md -> task-02-b\.md -> task-01-a\.md/);
|
|
351
|
+
});
|
|
352
|
+
});
|
|
353
|
+
|
|
141
354
|
test('Codex privacy approval is exact, session-bound, and reusable within its window', () => {
|
|
142
355
|
inHookFixture((root, hooks) => {
|
|
143
356
|
const nested = path.join(root, 'packages', 'app');
|
|
@@ -434,6 +647,7 @@ test('Codex completion gate validates explicit process-v3 inline Receipt', () =>
|
|
|
434
647
|
fs.writeFileSync(path.join(feature, 'plan.md'), '# Auth plan\n');
|
|
435
648
|
fs.writeFileSync(taskFile, [
|
|
436
649
|
'# Task 01: auth', '', 'Status: done', '',
|
|
650
|
+
'## Dependencies', '', '- none', '',
|
|
437
651
|
'## Verification Plan', '', '- Command: node --test', '',
|
|
438
652
|
].join('\n'));
|
|
439
653
|
|
|
@@ -452,6 +666,33 @@ test('Codex completion gate validates explicit process-v3 inline Receipt', () =>
|
|
|
452
666
|
const context = workflowRuntimeContext(root);
|
|
453
667
|
fs.writeFileSync(taskFile, [
|
|
454
668
|
'# Task 01: auth', '', 'Status: done', '',
|
|
669
|
+
'## Dependencies', '', '- none', '',
|
|
670
|
+
'## Verification Plan', '', '- Command: node --test', '',
|
|
671
|
+
'## Receipt', '', '```text', 'Verification: PASS', 'Command: node --test', 'Exit: 0',
|
|
672
|
+
`Base: ${context.base}`, `Head: ${context.head}`, 'pass: 1', '```', '',
|
|
673
|
+
].join('\n'));
|
|
674
|
+
const fencedFields = runHook(gate, nested, payload);
|
|
675
|
+
assert.equal(fencedFields.status, 0, fencedFields.stderr);
|
|
676
|
+
assert.match(JSON.parse(fencedFields.stdout).reason, /verification_state/);
|
|
677
|
+
assert.match(JSON.parse(fencedFields.stdout).reason, /command/);
|
|
678
|
+
assert.match(JSON.parse(fencedFields.stdout).reason, /provenance/);
|
|
679
|
+
|
|
680
|
+
fs.writeFileSync(taskFile, [
|
|
681
|
+
'# Task 01: auth', '', 'Status: done', '',
|
|
682
|
+
'## Dependencies', '', '- none', '',
|
|
683
|
+
'## Verification Plan', '', '- Command: node --test', '',
|
|
684
|
+
'## Receipt', '',
|
|
685
|
+
'Verification: PASS', 'Command: node --test', 'Exit: 0',
|
|
686
|
+
`Base: ${context.base}`, `Head: ${context.head}`,
|
|
687
|
+
'````text', '$ node --test', 'pass: 1', '```', '',
|
|
688
|
+
].join('\n'));
|
|
689
|
+
const malformedOutputFence = runHook(gate, nested, payload);
|
|
690
|
+
assert.equal(malformedOutputFence.status, 0, malformedOutputFence.stderr);
|
|
691
|
+
assert.match(JSON.parse(malformedOutputFence.stdout).reason, /command_output/);
|
|
692
|
+
|
|
693
|
+
fs.writeFileSync(taskFile, [
|
|
694
|
+
'# Task 01: auth', '', 'Status: done', '',
|
|
695
|
+
'## Dependencies', '', '- none', '',
|
|
455
696
|
'## Verification Plan', '', '- Command: node --test', '',
|
|
456
697
|
'## Receipt', '',
|
|
457
698
|
'Verification: PASS', 'Command: node --test', 'Exit: 0',
|
|
@@ -472,6 +713,7 @@ test('Codex process-v3 Receipt command must match the exact Verification Plan co
|
|
|
472
713
|
const context = workflowRuntimeContext(root);
|
|
473
714
|
fs.writeFileSync(path.join(feature, 'task-01-auth.md'), [
|
|
474
715
|
'# Task 01: auth', '', 'Status: done', '',
|
|
716
|
+
'## Dependencies', '', '- none', '',
|
|
475
717
|
'## Verification Plan', '', '- Command: pnpm test', '',
|
|
476
718
|
'## Receipt', '', 'Verification: PASS', 'Command: true', 'Exit: 0',
|
|
477
719
|
`Base: ${context.base}`, `Head: ${context.head}`,
|
|
@@ -501,6 +743,7 @@ test('Codex process-v3 Stop ignores a completed packet when one packet remains a
|
|
|
501
743
|
const context = workflowRuntimeContext(root);
|
|
502
744
|
fs.writeFileSync(path.join(completed, 'task-01-auth.md'), [
|
|
503
745
|
'# Task 01: auth', '', 'Status: done', '',
|
|
746
|
+
'## Dependencies', '', '- none', '',
|
|
504
747
|
'## Verification Plan', '', '- Command: node --test', '',
|
|
505
748
|
'## Receipt', '',
|
|
506
749
|
'Verification: PASS', 'Command: node --test', 'Exit: 0',
|
|
@@ -509,6 +752,7 @@ test('Codex process-v3 Stop ignores a completed packet when one packet remains a
|
|
|
509
752
|
].join('\n'));
|
|
510
753
|
fs.writeFileSync(path.join(active, 'task-01-active.md'), [
|
|
511
754
|
'# Task 01: active', '', 'Status: pending', '',
|
|
755
|
+
'## Dependencies', '', '- none', '',
|
|
512
756
|
'## Verification Plan', '', '- Command: node --test', '',
|
|
513
757
|
].join('\n'));
|
|
514
758
|
|
|
@@ -532,6 +776,7 @@ test('Codex process-v3 Stop accepts two completed packets with valid Receipts',
|
|
|
532
776
|
const context = workflowRuntimeContext(root, featureName);
|
|
533
777
|
fs.writeFileSync(path.join(feature, `task-01-${featureName}.md`), [
|
|
534
778
|
`# Task 01: ${featureName}`, '', 'Status: done', '',
|
|
779
|
+
'## Dependencies', '', '- none', '',
|
|
535
780
|
'## Verification Plan', '', '- Command: node --test', '',
|
|
536
781
|
'## Receipt', '',
|
|
537
782
|
'Verification: PASS', 'Command: node --test', 'Exit: 0',
|