@haposoft/cafekit 0.16.6 → 0.16.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 +1 -1
- package/package.json +1 -1
- package/src/claude/hooks/__tests__/spec-gate.test.js +38 -0
- package/src/claude/hooks/spec-gate.cjs +3 -2
- package/src/claude/scripts/spec-receipt.cjs +30 -1
- package/src/claude/scripts/workflow-policy.cjs +22 -10
- package/src/claude/skills/specs/references/templates.md +4 -3
- package/src/codex/hooks/lib/spec-receipt.cjs +5 -0
- package/src/codex/hooks/spec-gate.cjs +3 -2
package/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
> Native spec-driven workflow and runtime bundle for Claude Code and Codex CLI.
|
|
4
4
|
|
|
5
|
-
[](https://github.com/haposoft/cafekit)
|
|
6
6
|
[](LICENSE)
|
|
7
7
|
[](https://claude.ai/code)
|
|
8
8
|
[](https://developers.openai.com/codex)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@haposoft/cafekit",
|
|
3
|
-
"version": "0.16.
|
|
3
|
+
"version": "0.16.7",
|
|
4
4
|
"description": "Native spec-driven workflows for Claude Code and Codex CLI. Bundles CafeKit skills, runtime hooks, agents, and installer scaffolding.",
|
|
5
5
|
"author": "Haposoft <nghialt@haposoft.com>",
|
|
6
6
|
"license": "MIT",
|
|
@@ -1842,6 +1842,44 @@ test('47. completed-set branch accepts committed receipts and blocks an edited o
|
|
|
1842
1842
|
}
|
|
1843
1843
|
});
|
|
1844
1844
|
|
|
1845
|
+
test('51. Receipt fields written as list items are accepted', () => {
|
|
1846
|
+
// Every neighbouring section writes its fields as list items — the Verification Plan's
|
|
1847
|
+
// own `- Command:` sits a few lines above — so continuing that house style inside the
|
|
1848
|
+
// Receipt is the natural mistake. It used to fail verification_state, command,
|
|
1849
|
+
// exit_result and command_identity at once, with a message telling the user to rewrite
|
|
1850
|
+
// a Receipt whose content was already correct.
|
|
1851
|
+
const bulleted = boundReceipt().map((line) => (
|
|
1852
|
+
/^(Verification|Command|Exit|Base|Head):/.test(line) ? `- ${line}` : line
|
|
1853
|
+
));
|
|
1854
|
+
assert.ok(bulleted.some((line) => line.startsWith('- Verification:')), 'the fixture must be bulleted');
|
|
1855
|
+
const dir = makeWorkflowFixture(bulleted);
|
|
1856
|
+
try {
|
|
1857
|
+
clearCache();
|
|
1858
|
+
assert.strictEqual(runHook({}, dir).stdout, '', 'a bulleted Receipt must be read, not rejected');
|
|
1859
|
+
} finally {
|
|
1860
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
1861
|
+
}
|
|
1862
|
+
});
|
|
1863
|
+
|
|
1864
|
+
test('52. the block names what to change, not just which check failed', () => {
|
|
1865
|
+
// The same sentence used to answer every receipt failure, including the ones where
|
|
1866
|
+
// the Receipt was present and one field simply could not be read.
|
|
1867
|
+
const dir = makeWorkflowFixture([
|
|
1868
|
+
'## Receipt', '', 'Command: node --test', 'Exit: 0',
|
|
1869
|
+
`Base: ${VALID_BASE}`, `Head: ${VALID_HEAD}`, '```text', '$ node --test', 'pass: 1', '```',
|
|
1870
|
+
]);
|
|
1871
|
+
try {
|
|
1872
|
+
clearCache();
|
|
1873
|
+
const body = parseBlock(runHook({}, dir).stdout);
|
|
1874
|
+
assert.ok(body && body.decision === 'block');
|
|
1875
|
+
assert.match(body.reason, /add a `Verification: PASS` line/, 'the fix must name the missing field');
|
|
1876
|
+
assert.doesNotMatch(body.reason, /write a runtime-bound `## Receipt` with command output/,
|
|
1877
|
+
'the generic instruction must not stand in for a specific one');
|
|
1878
|
+
} finally {
|
|
1879
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
1880
|
+
}
|
|
1881
|
+
});
|
|
1882
|
+
|
|
1845
1883
|
test('49. an unversioned specs root does not keep every receipt bound', () => {
|
|
1846
1884
|
// Committing specs/ is the user's choice, not the tool's requirement. Without a
|
|
1847
1885
|
// committed copy of the task file there is no baseline, so the selector used to keep
|
|
@@ -150,7 +150,8 @@ try {
|
|
|
150
150
|
const lines = [`⚠️ Completion gate: ${checked.failures.length} done task(s) lack a verification receipt.`];
|
|
151
151
|
for (const failure of checked.failures) {
|
|
152
152
|
lines.push(`- \`${failure.featureName}/${failure.taskPath}\`: failed check(s) ${failure.failures.join(', ')}`);
|
|
153
|
-
|
|
153
|
+
const hint = typeof RECEIPT.receiptFixHint === 'function' ? RECEIPT.receiptFixHint(failure.failures) : null;
|
|
154
|
+
lines.push(` Fix in \`specs/${failure.featureName}/${failure.taskPath}\`: ${hint || 'write a runtime-bound `## Receipt` with the planned command'}.`);
|
|
154
155
|
}
|
|
155
156
|
emitBlock(lines.slice(0, 8).join('\n'));
|
|
156
157
|
process.exit(0);
|
|
@@ -302,7 +303,7 @@ try {
|
|
|
302
303
|
lines.push(taskPath === 'feature-receipt.md'
|
|
303
304
|
? ` Fix: run final integration proof, then write \`specs/${featureName}/feature-receipt.md\`.`
|
|
304
305
|
: processWorkflow
|
|
305
|
-
? ` Fix
|
|
306
|
+
? ` Fix in \`specs/${featureName}/${path.posix.basename(taskPath)}\`: ${(typeof RECEIPT.receiptFixHint === 'function' && RECEIPT.receiptFixHint(fails)) || 'write a runtime-bound `## Receipt` with command output'}.`
|
|
306
307
|
: ` Fix: write canonical proof to \`specs/${featureName}/receipts/${path.posix.basename(taskPath)}\`; legacy \`## Evidence\` remains read-compatible.`);
|
|
307
308
|
}
|
|
308
309
|
if (completionBlocked) {
|
|
@@ -113,7 +113,10 @@ function hasNonEmptyFencedBlock(body) {
|
|
|
113
113
|
|
|
114
114
|
function fieldValues(body, name) {
|
|
115
115
|
const escaped = name.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
116
|
-
|
|
116
|
+
// A leading list marker is accepted: every neighbouring section of a task file writes
|
|
117
|
+
// its fields as list items, so continuing that style inside the Receipt is the
|
|
118
|
+
// natural mistake rather than a malformed receipt.
|
|
119
|
+
const matcher = new RegExp(`^\\s*(?:[-*+]\\s+)?${escaped}\\s*:\\s*(.*?)\\s*$`, 'i');
|
|
117
120
|
return String(body || '').split('\n').map((line) => line.match(matcher)).filter(Boolean).map((match) => match[1]);
|
|
118
121
|
}
|
|
119
122
|
|
|
@@ -335,6 +338,31 @@ function checkWorkflowTaskReceipt(featureDir, taskPath, runtimeContext, policy)
|
|
|
335
338
|
};
|
|
336
339
|
}
|
|
337
340
|
|
|
341
|
+
// The gate used to answer every receipt failure with the same sentence — write a
|
|
342
|
+
// runtime-bound Receipt — which is the wrong instruction whenever the Receipt is
|
|
343
|
+
// present and one field simply could not be read. Name what to change instead.
|
|
344
|
+
const RECEIPT_FIX_HINTS = Object.freeze([
|
|
345
|
+
['missing_receipt', 'add exactly one `## Receipt` section at the end of the file'],
|
|
346
|
+
['task_status', 'the file needs exactly one `Status: done` line outside any fence'],
|
|
347
|
+
['verification_state', 'add a `Verification: PASS` line'],
|
|
348
|
+
['command', 'add a `Command:` line holding the exact command that was run'],
|
|
349
|
+
['command_identity', 'the `Command:` line must match the Verification Plan command exactly'],
|
|
350
|
+
['exit_result', 'add `Exit: 0`; a non-zero exit or a failure marker does not close a task'],
|
|
351
|
+
['command_output', 'add a non-empty fenced block holding the real command output'],
|
|
352
|
+
['placeholder', 'replace the placeholder values with real ones'],
|
|
353
|
+
['provenance', 'add runtime-derived `Base:` and `Head:` lines matching the current runtime'],
|
|
354
|
+
]);
|
|
355
|
+
|
|
356
|
+
/**
|
|
357
|
+
* Turn receipt check codes into one instruction naming what to change.
|
|
358
|
+
*/
|
|
359
|
+
function receiptFixHint(failures) {
|
|
360
|
+
const codes = new Set(Array.isArray(failures) ? failures : []);
|
|
361
|
+
const hints = RECEIPT_FIX_HINTS.filter(([code]) => codes.has(code)).map(([, hint]) => hint);
|
|
362
|
+
if (hints.length === 0) return null;
|
|
363
|
+
return hints.length === 1 ? hints[0] : `${hints.slice(0, -1).join('; ')}; and ${hints[hints.length - 1]}`;
|
|
364
|
+
}
|
|
365
|
+
|
|
338
366
|
function workflowProofSignature(taskPath, proof) {
|
|
339
367
|
const hash = crypto.createHash('sha256');
|
|
340
368
|
hash.update('cafekit-workflow-proof-v1\0');
|
|
@@ -404,6 +432,7 @@ module.exports = {
|
|
|
404
432
|
checkTaskReceipt,
|
|
405
433
|
checkWorkflowReceiptSet,
|
|
406
434
|
checkWorkflowTaskReceipt,
|
|
435
|
+
receiptFixHint,
|
|
407
436
|
evidenceBody,
|
|
408
437
|
readTaskProof,
|
|
409
438
|
safeRead,
|
|
@@ -1884,6 +1884,18 @@ function flashState(taskRegistry = {}) {
|
|
|
1884
1884
|
.map(([taskPath]) => taskPath);
|
|
1885
1885
|
}
|
|
1886
1886
|
|
|
1887
|
+
// Every other section of a task file writes its fields as list items — the Verification
|
|
1888
|
+
// Plan's own `- Command:` sits a few lines above the Receipt — so continuing that house
|
|
1889
|
+
// style inside the Receipt is the natural mistake, and it used to fail five checks at
|
|
1890
|
+
// once with a message that pointed at the content instead of the dash. Accept either
|
|
1891
|
+
// form. The receipt body is cut to its own `## Receipt` section, so this cannot pick up
|
|
1892
|
+
// the Verification Plan's fields.
|
|
1893
|
+
const RECEIPT_FIELD_PREFIX = '\\s*(?:[-*+]\\s+)?';
|
|
1894
|
+
|
|
1895
|
+
function receiptFieldPattern(name, tail = '\\s*:', flags = '') {
|
|
1896
|
+
return new RegExp(`^${RECEIPT_FIELD_PREFIX}${name}${tail}`, flags);
|
|
1897
|
+
}
|
|
1898
|
+
|
|
1887
1899
|
function validateCanonicalReceipt(body, options = {}) {
|
|
1888
1900
|
if (typeof body !== 'string') return ['verification_state'];
|
|
1889
1901
|
const failures = [];
|
|
@@ -1900,15 +1912,15 @@ function validateCanonicalReceipt(body, options = {}) {
|
|
|
1900
1912
|
addFailure('verification_state');
|
|
1901
1913
|
}
|
|
1902
1914
|
// Unambiguous verification state: must be Verification: PASS exactly
|
|
1903
|
-
if (
|
|
1915
|
+
if (!receiptFieldPattern('Verification', '\\s*:\\s*PASS\\s*$', 'm').test(body)) {
|
|
1904
1916
|
addFailure('verification_state');
|
|
1905
1917
|
}
|
|
1906
1918
|
// Command must be present with non-empty concrete value on same line, not placeholder
|
|
1907
|
-
const cmdLine = body.split('\n').find((l) =>
|
|
1908
|
-
if (!cmdLine ||
|
|
1919
|
+
const cmdLine = body.split('\n').find((l) => receiptFieldPattern('Command(?:\\(s\\))?', '\\s*:', 'm').test(l)) || null;
|
|
1920
|
+
if (!cmdLine || !receiptFieldPattern('Command(?:\\(s\\))?', '\\s*:[ \\t]*\\S', 'm').test(cmdLine)) {
|
|
1909
1921
|
addFailure('command');
|
|
1910
1922
|
} else {
|
|
1911
|
-
const m = cmdLine.match(
|
|
1923
|
+
const m = cmdLine.match(receiptFieldPattern('Command(?:\\(s\\))?', '\\s*:[ \\t]*(.*)$', 'm'));
|
|
1912
1924
|
const val = m ? m[1].trim() : '';
|
|
1913
1925
|
if (isPlaceholderToken(val)) {
|
|
1914
1926
|
addFailure('command');
|
|
@@ -1921,15 +1933,15 @@ function validateCanonicalReceipt(body, options = {}) {
|
|
|
1921
1933
|
const exitValues = [];
|
|
1922
1934
|
const resultValues = [];
|
|
1923
1935
|
for (const line of lines) {
|
|
1924
|
-
if (
|
|
1925
|
-
const m = line.match(
|
|
1936
|
+
if (receiptFieldPattern('Exit', '\\s*:', 'i').test(line)) {
|
|
1937
|
+
const m = line.match(receiptFieldPattern('Exit', '\\s*:\\s*(.*)$', 'i'));
|
|
1926
1938
|
exitValues.push(m ? m[1].trim() : '');
|
|
1927
1939
|
} else if (/exit\s+code\s*[:=]/i.test(line)) {
|
|
1928
1940
|
const m = line.match(/exit\s+code\s*[:=]\s*(.*)$/i);
|
|
1929
1941
|
if (m) exitValues.push(m[1].trim());
|
|
1930
1942
|
}
|
|
1931
|
-
if (
|
|
1932
|
-
const m = line.match(
|
|
1943
|
+
if (receiptFieldPattern('Result', '\\s*:', 'i').test(line)) {
|
|
1944
|
+
const m = line.match(receiptFieldPattern('Result', '\\s*:\\s*(.+?)\\s*$', 'i'));
|
|
1933
1945
|
resultValues.push(m ? m[1].trim() : '');
|
|
1934
1946
|
}
|
|
1935
1947
|
}
|
|
@@ -1954,8 +1966,8 @@ function validateCanonicalReceipt(body, options = {}) {
|
|
|
1954
1966
|
// Without expected values this validates schema only; it does not claim identity binding.
|
|
1955
1967
|
const receiptLines = body.split('\n');
|
|
1956
1968
|
const readField = (name) => receiptLines
|
|
1957
|
-
.filter((line) =>
|
|
1958
|
-
.map((line) => line.replace(
|
|
1969
|
+
.filter((line) => receiptFieldPattern(name, '\\s*:', 'i').test(line))
|
|
1970
|
+
.map((line) => line.replace(receiptFieldPattern(name, '\\s*:\\s*', 'i'), '').trim());
|
|
1959
1971
|
const baseValues = readField('Base');
|
|
1960
1972
|
const headValues = readField('Head');
|
|
1961
1973
|
const baseShaValues = readField('base_sha');
|
|
@@ -131,9 +131,10 @@ Tests: 3 passed, 3 total
|
|
|
131
131
|
```
|
|
132
132
|
````
|
|
133
133
|
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
134
|
+
Each field is its own line; a leading `- ` reads identically. The receipt is
|
|
135
|
+
invalid when it has a placeholder, missing output fence, zero executed tests
|
|
136
|
+
where tests are required, a failure marker, nonzero exit, stale Base/Head, or a
|
|
137
|
+
bare PASS claim without the command output.
|
|
137
138
|
|
|
138
139
|
## EARS sentence patterns
|
|
139
140
|
|
|
@@ -99,6 +99,10 @@ function checkFeatureReceipt(featureDir, runtimeContext) {
|
|
|
99
99
|
if (!policy) return { failures: ['shared_validator'], status: 'missing' };
|
|
100
100
|
return receiptHelper().checkFeatureReceipt(featureDir, runtimeContext, policy);
|
|
101
101
|
}
|
|
102
|
+
function receiptFixHint(failures) {
|
|
103
|
+
const helper = receiptHelper();
|
|
104
|
+
return typeof helper.receiptFixHint === 'function' ? helper.receiptFixHint(failures) : null;
|
|
105
|
+
}
|
|
102
106
|
function readTaskProof(featureDir, taskPath) {
|
|
103
107
|
return receiptHelper().readTaskProof(featureDir, taskPath, getSharedPolicy() || {});
|
|
104
108
|
}
|
|
@@ -115,6 +119,7 @@ module.exports = {
|
|
|
115
119
|
loadSharedPolicy,
|
|
116
120
|
loadSharedReceipt,
|
|
117
121
|
readTaskProof,
|
|
122
|
+
receiptFixHint,
|
|
118
123
|
safeTaskFile,
|
|
119
124
|
validateCanonicalReceipt,
|
|
120
125
|
};
|
|
@@ -16,6 +16,7 @@ const {
|
|
|
16
16
|
checkWorkflowReceiptDetails,
|
|
17
17
|
checkWorkflowReceiptSet,
|
|
18
18
|
loadSharedPolicy,
|
|
19
|
+
receiptFixHint,
|
|
19
20
|
} = require('./lib/spec-receipt.cjs');
|
|
20
21
|
const { taskStatusMap } = require('./lib/spec-utils.cjs');
|
|
21
22
|
const FINAL_STATE = require('./completion-authority-check.cjs');
|
|
@@ -75,7 +76,7 @@ try {
|
|
|
75
76
|
const lines = [`Completion gate: ${checked.failures.length} done task(s) lack a verification receipt.`];
|
|
76
77
|
for (const failure of checked.failures) {
|
|
77
78
|
lines.push(`- \`${failure.featureName}/${failure.taskPath}\`: failed check(s) ${failure.failures.join(', ')}`);
|
|
78
|
-
lines.push(`
|
|
79
|
+
lines.push(` In \`specs/${failure.featureName}/${failure.taskPath}\`: ${receiptFixHint(failure.failures) || 'write a runtime-bound `## Receipt` with the planned command'}.`);
|
|
79
80
|
}
|
|
80
81
|
emitBlock(lines.slice(0, 8).join('\n'));
|
|
81
82
|
process.exit(0);
|
|
@@ -228,7 +229,7 @@ try {
|
|
|
228
229
|
lines.push(result.taskPath === 'feature-receipt.md'
|
|
229
230
|
? ` Run final integration proof, then write \`specs/${active.featureName}/feature-receipt.md\`.`
|
|
230
231
|
: processWorkflow
|
|
231
|
-
? `
|
|
232
|
+
? ` In \`specs/${active.featureName}/${path.posix.basename(result.taskPath)}\`: ${receiptFixHint(result.failures) || 'write a runtime-bound `## Receipt` with command output'}.`
|
|
232
233
|
: ` Write canonical proof to \`specs/${active.featureName}/receipts/${path.posix.basename(result.taskPath)}\`; legacy \`## Evidence\` remains read-compatible.`);
|
|
233
234
|
}
|
|
234
235
|
if (completionBlocked) {
|