@haposoft/cafekit 0.18.0 → 0.18.1
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/bin/__tests__/codex-hooks.test.js +169 -236
- package/bin/__tests__/codex-native.test.js +54 -114
- package/bin/__tests__/develop-contract.test.js +101 -1361
- package/bin/__tests__/legacy-authority-decoupled.test.js +14 -19
- package/bin/__tests__/package-inventory.test.js +1 -395
- package/bin/__tests__/spec-narrowing.test.js +30 -134
- package/bin/__tests__/specs-v2-execution-closeout.test.js +0 -372
- package/bin/__tests__/test-runtime-dependency-closure.cjs +1 -2
- package/bin/__tests__/validator-grounding.test.js +17 -2929
- package/bin/lib/managed-writer.js +2 -3
- package/bin/phases/codex-runtime.js +6 -3
- package/bin/phases/copy-payload.js +13 -28
- package/bin/phases/omp-runtime.js +6 -3
- package/package.json +2 -3
- package/src/claude/agents/code-auditor.md +9 -6
- package/src/claude/agents/debugger.md +3 -9
- package/src/claude/agents/docs-keeper.md +4 -7
- package/src/claude/agents/git-ops.md +2 -4
- package/src/claude/agents/implementer.md +6 -7
- package/src/claude/agents/inspector.md +2 -0
- package/src/claude/agents/project-manager.md +2 -2
- package/src/claude/agents/spec-maker.md +4 -4
- package/src/claude/agents/test-runner.md +8 -11
- package/src/claude/agents/ui-ux-designer.md +0 -1
- package/src/claude/hooks/__tests__/spec-gate.test.js +170 -330
- package/src/claude/hooks/__tests__/spec-state-touched.test.js +26 -0
- package/src/claude/hooks/spec-gate.cjs +8 -63
- package/src/claude/hooks/spec-state.cjs +47 -38
- package/src/claude/migration-manifest.json +12 -7
- package/src/claude/output-styles/coding-level-0-eli5.md +9 -9
- package/src/claude/output-styles/coding-level-1-junior.md +3 -3
- package/src/claude/output-styles/coding-level-2-mid.md +3 -3
- package/src/claude/output-styles/coding-level-3-senior.md +2 -2
- package/src/claude/output-styles/coding-level-4-lead.md +5 -5
- package/src/claude/output-styles/coding-level-5-god.md +6 -6
- package/src/claude/rules/ai-dev-rules.md +1 -2
- package/src/claude/rules/workflow.md +1 -1
- package/src/claude/scripts/spec-receipt.cjs +0 -114
- package/src/claude/scripts/spec-resolver.cjs +41 -457
- package/src/claude/scripts/workflow-policy.cjs +3 -1801
- package/src/claude/skills/agent-browser/SKILL.md +1 -1
- package/src/claude/skills/chrome-devtools/SKILL.md +14 -31
- package/src/claude/skills/code-review/SKILL.md +25 -108
- package/src/claude/skills/code-review/references/spec-compliance-review.md +4 -7
- package/src/claude/skills/develop/SKILL.md +22 -70
- package/src/claude/skills/develop/references/subagent-patterns.md +0 -6
- package/src/claude/skills/research/SKILL.md +24 -55
- package/src/claude/skills/sync/SKILL.md +12 -34
- package/src/claude/skills/sync/references/rebind-and-audit.md +2 -2
- package/src/claude/skills/test/SKILL.md +39 -123
- package/src/claude/skills/test/references/execution-strategy.md +11 -22
- package/src/claude/skills/ui-ux/SKILL.md +1 -1
- package/src/claude/skills/ui-ux-pro-max/SKILL.md +24 -79
- package/src/claude/skills/web-testing/SKILL.md +2 -20
- package/src/codex/hooks/lib/spec-receipt.cjs +2 -30
- package/src/codex/hooks/lib/spec-utils.cjs +6 -45
- package/src/codex/hooks/spec-gate.cjs +11 -68
- package/src/codex/hooks/spec-state.cjs +37 -41
- package/src/common/AGENTS.md +4 -10
- package/bin/__tests__/change-firewall.semantic-firewall.test.js +0 -2133
- package/bin/__tests__/finalizer-pass-gate.semantic-firewall.test.js +0 -342
- package/bin/__tests__/release-preflight.semantic-firewall.test.js +0 -586
- package/bin/__tests__/review-receipt-c2.semantic-firewall.test.js +0 -378
- package/bin/__tests__/specs-v2-policy-and-scaffold.test.js +0 -665
- package/bin/__tests__/specs-v2-validator-grounder-contract.test.js +0 -1132
- package/src/claude/hooks/lib/context.cjs +0 -527
- package/src/claude/hooks/lib/detect.cjs +0 -474
- package/src/claude/scripts/change-firewall.cjs +0 -1748
- package/src/claude/scripts/spec-authoring-digest.cjs +0 -151
- package/src/claude/scripts/spec-authoring-validation.cjs +0 -126
- package/src/claude/scripts/spec-final-state.cjs +0 -290
- package/src/claude/scripts/spec-ground.cjs +0 -650
- package/src/claude/scripts/spec-readiness.cjs +0 -425
- package/src/claude/scripts/spec-scaffold.cjs +0 -2317
- package/src/claude/scripts/spec-semantic-model.cjs +0 -396
- package/src/claude/scripts/validate-spec-output.cjs +0 -3914
- package/src/claude/skills/code-review/references/verification-gate.md +0 -41
- package/src/claude/skills/specs/templates/design.md +0 -82
- package/src/claude/skills/specs/templates/requirements-init.md +0 -8
- package/src/claude/skills/specs/templates/requirements.md +0 -42
- package/src/claude/skills/specs/templates/spec-state.json +0 -56
- package/src/claude/skills/specs/templates/task.md +0 -47
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)
|
|
@@ -19,10 +19,6 @@ const { stateDir: codexStateDir } = require(
|
|
|
19
19
|
);
|
|
20
20
|
|
|
21
21
|
function runHook(file, cwd, payload) {
|
|
22
|
-
if (path.basename(file) === 'spec-gate.cjs') {
|
|
23
|
-
const rootResult = spawnSync('git', ['-C', cwd, 'rev-parse', '--show-toplevel'], { encoding: 'utf8' });
|
|
24
|
-
if (rootResult.status === 0) installFeatureReceipts(rootResult.stdout.trim(), payload.session_id || 'session-a');
|
|
25
|
-
}
|
|
26
22
|
return spawnSync(process.execPath, [file], {
|
|
27
23
|
cwd,
|
|
28
24
|
input: JSON.stringify(payload),
|
|
@@ -34,7 +30,7 @@ function runtimeContext(root, feature = 'auth', session = 'session-a') {
|
|
|
34
30
|
return PROVENANCE.deriveRuntimeContext({
|
|
35
31
|
projectRoot: root,
|
|
36
32
|
specsRoot: path.join(root, 'specs'),
|
|
37
|
-
specFile: path.join(root, 'specs', feature, '
|
|
33
|
+
specFile: path.join(root, 'specs', feature, 'plan.md'),
|
|
38
34
|
featureName: feature,
|
|
39
35
|
runtimeSession: session,
|
|
40
36
|
});
|
|
@@ -55,34 +51,18 @@ function workflowRuntimeContext(root, feature = 'auth', session = 'session-a') {
|
|
|
55
51
|
});
|
|
56
52
|
}
|
|
57
53
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
if (!explicitCloseout) continue;
|
|
71
|
-
const tasks = Object.values(spec.task_registry || {});
|
|
72
|
-
if (tasks.length === 0 || tasks.some((task) => task.status !== 'done')) continue;
|
|
73
|
-
const context = runtimeContext(root, feature, session);
|
|
74
|
-
fs.writeFileSync(path.join(featureDir, 'feature-receipt.md'), [
|
|
75
|
-
`Feature: ${feature}`,
|
|
76
|
-
'Expected: final integration verification passes',
|
|
77
|
-
'Observed: final integration verification passed',
|
|
78
|
-
'Verification: PASS',
|
|
79
|
-
'Command: node --test',
|
|
80
|
-
'Exit: 0',
|
|
81
|
-
`Base: ${context.base}`,
|
|
82
|
-
`Head: ${context.head}`,
|
|
83
|
-
'',
|
|
84
|
-
].join('\n'));
|
|
85
|
-
}
|
|
54
|
+
/** A process-first packet with one flat task whose Verification Plan runs `command`. */
|
|
55
|
+
function writeProcessTask(featureDir, status, receiptLines = null, command = 'pnpm test', taskName = 'task-01-auth.md') {
|
|
56
|
+
fs.mkdirSync(featureDir, { recursive: true });
|
|
57
|
+
fs.writeFileSync(path.join(featureDir, 'plan.md'), '# Plan\nSpecs-Contract: process-first-ready-v1\n');
|
|
58
|
+
const lines = [
|
|
59
|
+
'# Task 01', '', `Status: ${status}`, '',
|
|
60
|
+
'## Dependencies', '', '- none', '',
|
|
61
|
+
'## Verification Plan', '', `- Command: ${command}`, '',
|
|
62
|
+
];
|
|
63
|
+
if (receiptLines) lines.push('## Receipt', '', ...receiptLines, '');
|
|
64
|
+
fs.writeFileSync(path.join(featureDir, taskName), lines.join('\n'));
|
|
65
|
+
return path.join(featureDir, taskName);
|
|
86
66
|
}
|
|
87
67
|
|
|
88
68
|
function inHookFixture(run, options = {}) {
|
|
@@ -121,12 +101,6 @@ function inHookFixture(run, options = {}) {
|
|
|
121
101
|
path.join(PACKAGE_ROOT, 'src/claude/scripts/spec-resolver.cjs'),
|
|
122
102
|
path.join(root, '.codex', 'scripts', 'spec-resolver.cjs'),
|
|
123
103
|
);
|
|
124
|
-
for (const file of ['validate-spec-output.cjs', 'spec-ground.cjs', 'spec-semantic-model.cjs', 'spec-final-state.cjs']) {
|
|
125
|
-
fs.copyFileSync(
|
|
126
|
-
path.join(PACKAGE_ROOT, 'src/claude/scripts', file),
|
|
127
|
-
path.join(root, '.codex', 'scripts', file),
|
|
128
|
-
);
|
|
129
|
-
}
|
|
130
104
|
fs.writeFileSync(path.join(root, '.codex', 'scripts', 'spec-scaffold.cjs'), '');
|
|
131
105
|
for (const args of [['init', '-q'], ['config', 'user.email', 'cafekit@example.invalid'], ['config', 'user.name', 'CafeKit Test'], ['commit', '--allow-empty', '-qm', 'fixture']]) {
|
|
132
106
|
const result = spawnSync('git', ['-C', root, ...args], { encoding: 'utf8' });
|
|
@@ -245,6 +219,87 @@ function inInstalledProcessSpecStateFixture(run) {
|
|
|
245
219
|
});
|
|
246
220
|
}
|
|
247
221
|
|
|
222
|
+
function writeCodexLegacyPacket(root, name = 'old') {
|
|
223
|
+
const dir = path.join(root, 'specs', name);
|
|
224
|
+
fs.mkdirSync(path.join(dir, 'tasks'), { recursive: true });
|
|
225
|
+
fs.writeFileSync(path.join(dir, 'spec.json'), JSON.stringify({
|
|
226
|
+
feature_name: name,
|
|
227
|
+
status: 'in_progress',
|
|
228
|
+
task_registry: { 'tasks/task-R1-01-x.md': { status: 'done' } },
|
|
229
|
+
}));
|
|
230
|
+
fs.writeFileSync(path.join(dir, 'tasks', 'task-R1-01-x.md'), '# Task\n\n**Status:** done\n');
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
const CODEX_LEGACY_NOTICE = /`specs\/old` is a legacy Specs packet without plan\.md; CafeKit no longer reads it/g;
|
|
234
|
+
|
|
235
|
+
test('Codex spec-state notices a leftover legacy packet once per session, also when it is the only packet', () => {
|
|
236
|
+
inHookFixture((root, hooks) => {
|
|
237
|
+
writeCodexLegacyPacket(root);
|
|
238
|
+
const prompt = (session) => runHook(path.join(hooks, 'spec-state.cjs'), root, {
|
|
239
|
+
cwd: root, session_id: session, hook_event_name: 'UserPromptSubmit', prompt: 'Continue',
|
|
240
|
+
});
|
|
241
|
+
const first = prompt('session-a');
|
|
242
|
+
assert.equal(first.status, 0, first.stderr);
|
|
243
|
+
assert.equal((first.stdout.match(CODEX_LEGACY_NOTICE) || []).length, 1);
|
|
244
|
+
assert.equal((prompt('session-a').stdout.match(CODEX_LEGACY_NOTICE) || []).length, 0, 'the same session is not told twice');
|
|
245
|
+
assert.equal((prompt('session-b').stdout.match(CODEX_LEGACY_NOTICE) || []).length, 1, 'a new session is told once');
|
|
246
|
+
});
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
test('Codex Stop ignores a leftover legacy packet and still blocks a malformed process-first packet', () => {
|
|
250
|
+
inHookFixture((root, hooks) => {
|
|
251
|
+
const gate = path.join(hooks, 'spec-gate.cjs');
|
|
252
|
+
const payload = { cwd: root, session_id: 'session-a', hook_event_name: 'Stop', stop_hook_active: false };
|
|
253
|
+
writeCodexLegacyPacket(root);
|
|
254
|
+
const legacyOnly = runHook(gate, root, payload);
|
|
255
|
+
assert.equal(legacyOnly.status, 0, legacyOnly.stderr);
|
|
256
|
+
assert.equal(legacyOnly.stdout, '', 'a legacy packet alone never blocks');
|
|
257
|
+
|
|
258
|
+
const shared = path.join(root, 'specs', '_shared');
|
|
259
|
+
fs.mkdirSync(shared, { recursive: true });
|
|
260
|
+
fs.writeFileSync(path.join(shared, 'active-feature.json'), '{"featureName": "old"}\n');
|
|
261
|
+
assert.equal(runHook(gate, root, payload).stdout, '', 'a recorded legacy packet never blocks');
|
|
262
|
+
fs.rmSync(shared, { recursive: true, force: true });
|
|
263
|
+
|
|
264
|
+
const feature = path.join(root, 'specs', 'cycle');
|
|
265
|
+
writeProcessTask(feature, 'done', null, 'pnpm test', 'task-01-a.md');
|
|
266
|
+
writeProcessTask(feature, 'pending', null, 'pnpm test', 'task-02-b.md');
|
|
267
|
+
for (const [name, dependency] of [['task-01-a.md', 'task-02-b.md'], ['task-02-b.md', 'task-01-a.md']]) {
|
|
268
|
+
const file = path.join(feature, name);
|
|
269
|
+
fs.writeFileSync(file, fs.readFileSync(file, 'utf8').replace('- none', `- ${dependency}`));
|
|
270
|
+
}
|
|
271
|
+
const cycle = runHook(gate, root, payload);
|
|
272
|
+
assert.equal(cycle.status, 0, cycle.stderr);
|
|
273
|
+
const block = JSON.parse(cycle.stdout);
|
|
274
|
+
assert.equal(block.decision, 'block');
|
|
275
|
+
assert.match(block.reason, /invalid workflow packet/);
|
|
276
|
+
});
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
test('Codex Stop still blocks a recorded missing packet and reads a plan.md + spec.json packet as process-first', () => {
|
|
280
|
+
inHookFixture((root, hooks) => {
|
|
281
|
+
const gate = path.join(hooks, 'spec-gate.cjs');
|
|
282
|
+
const payload = { cwd: root, session_id: 'session-a', hook_event_name: 'Stop', stop_hook_active: false };
|
|
283
|
+
const feature = path.join(root, 'specs', 'auth');
|
|
284
|
+
writeProcessTask(feature, 'done');
|
|
285
|
+
fs.writeFileSync(path.join(feature, 'spec.json'), JSON.stringify({ feature_name: 'auth' }));
|
|
286
|
+
const hybrid = runHook(gate, root, payload);
|
|
287
|
+
assert.equal(hybrid.status, 0, hybrid.stderr);
|
|
288
|
+
const hybridBlock = JSON.parse(hybrid.stdout);
|
|
289
|
+
assert.equal(hybridBlock.decision, 'block');
|
|
290
|
+
assert.match(hybridBlock.reason, /task-01-auth\.md/);
|
|
291
|
+
|
|
292
|
+
const shared = path.join(root, 'specs', '_shared');
|
|
293
|
+
fs.mkdirSync(shared, { recursive: true });
|
|
294
|
+
fs.writeFileSync(path.join(shared, 'active-feature.json'), '{"featureName": "nope"}\n');
|
|
295
|
+
const missing = runHook(gate, root, payload);
|
|
296
|
+
assert.equal(missing.status, 0, missing.stderr);
|
|
297
|
+
const missingBlock = JSON.parse(missing.stdout);
|
|
298
|
+
assert.equal(missingBlock.decision, 'block');
|
|
299
|
+
assert.match(missingBlock.reason, /explicit_not_found/);
|
|
300
|
+
});
|
|
301
|
+
});
|
|
302
|
+
|
|
248
303
|
test('Codex installed spec-state re-evaluates a standalone task when blocked becomes pending', () => {
|
|
249
304
|
inInstalledProcessSpecStateFixture(({ findCacheFile, hooks, runState, writeTask }) => {
|
|
250
305
|
writeTask('task-01-auth.md', 'Task 01: auth', 'blocked');
|
|
@@ -615,18 +670,8 @@ test('Codex completion gate cannot be bypassed from a nested cwd', () => {
|
|
|
615
670
|
inHookFixture((root, hooks) => {
|
|
616
671
|
const nested = path.join(root, 'packages', 'app');
|
|
617
672
|
const feature = path.join(root, 'specs', 'auth');
|
|
618
|
-
const taskPath = 'tasks/task-R0-01-auth.md';
|
|
619
|
-
fs.mkdirSync(path.join(feature, 'tasks'), { recursive: true });
|
|
620
673
|
fs.mkdirSync(nested, { recursive: true });
|
|
621
|
-
|
|
622
|
-
status: 'in_progress',
|
|
623
|
-
feature_name: 'auth',
|
|
624
|
-
task_registry: {
|
|
625
|
-
[taskPath]: { status: 'pending', completed_at: null }
|
|
626
|
-
}
|
|
627
|
-
};
|
|
628
|
-
fs.writeFileSync(path.join(feature, 'spec.json'), `${JSON.stringify(spec)}\n`);
|
|
629
|
-
fs.writeFileSync(path.join(feature, taskPath), 'Status: pending\n');
|
|
674
|
+
writeProcessTask(feature, 'pending', null, 'node --test');
|
|
630
675
|
const gate = path.join(hooks, 'spec-gate.cjs');
|
|
631
676
|
const payload = {
|
|
632
677
|
cwd: nested,
|
|
@@ -640,32 +685,19 @@ test('Codex completion gate cannot be bypassed from a nested cwd', () => {
|
|
|
640
685
|
assert.equal(seeded.status, 0);
|
|
641
686
|
assert.equal(seeded.stdout, '');
|
|
642
687
|
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
'Status: done', '', '## Evidence', '', 'Verification: PASS',
|
|
657
|
-
'Command: node --test', 'Exit: 0', `Base: ${VALID_BASE}`, `Head: ${VALID_HEAD}`,
|
|
658
|
-
].join('\n'), 'auth'));
|
|
659
|
-
const validBeforeCloseout = runHook(gate, nested, payload);
|
|
660
|
-
assert.equal(validBeforeCloseout.status, 0);
|
|
661
|
-
assert.equal(validBeforeCloseout.stdout, '');
|
|
662
|
-
assert.equal(fs.existsSync(path.join(feature, 'feature-receipt.md')), false);
|
|
663
|
-
|
|
664
|
-
spec.current_phase = 'closeout';
|
|
665
|
-
fs.writeFileSync(path.join(feature, 'spec.json'), `${JSON.stringify(spec)}\n`);
|
|
666
|
-
const closeout = runHook(gate, nested, payload);
|
|
667
|
-
assert.equal(closeout.status, 0);
|
|
668
|
-
assert.equal(closeout.stdout, '');
|
|
688
|
+
writeProcessTask(feature, 'done', null, 'node --test');
|
|
689
|
+
const blocked = runHook(gate, nested, payload);
|
|
690
|
+
assert.equal(blocked.status, 0);
|
|
691
|
+
assert.equal(JSON.parse(blocked.stdout).decision, 'block');
|
|
692
|
+
assert.match(JSON.parse(blocked.stdout).reason, /verification receipt/);
|
|
693
|
+
|
|
694
|
+
writeProcessTask(feature, 'done', bindReceipt(root, [
|
|
695
|
+
'Verification: PASS', 'Command: node --test', 'Exit: 0', `Base: ${VALID_BASE}`, `Head: ${VALID_HEAD}`,
|
|
696
|
+
'```', 'node --test', 'ok 1', '```',
|
|
697
|
+
].join('\n')).split('\n'), 'node --test');
|
|
698
|
+
const valid = runHook(gate, nested, payload);
|
|
699
|
+
assert.equal(valid.status, 0);
|
|
700
|
+
assert.equal(valid.stdout, '');
|
|
669
701
|
});
|
|
670
702
|
});
|
|
671
703
|
|
|
@@ -765,20 +797,15 @@ test('Codex process-v3 Receipt command must match the exact Verification Plan co
|
|
|
765
797
|
});
|
|
766
798
|
|
|
767
799
|
test('Codex recorded active feature turns the identity block into receipt validation', () => {
|
|
768
|
-
// Issue #79 end to end on this runtime. Two
|
|
769
|
-
//
|
|
770
|
-
//
|
|
800
|
+
// Issue #79 end to end on this runtime. Two unfinished packets make the gate answer a
|
|
801
|
+
// missing-receipt violation with an identity complaint; naming the feature resolves it
|
|
802
|
+
// and the violation surfaces.
|
|
771
803
|
inHookFixture((root, hooks) => {
|
|
772
804
|
for (const name of ['alpha', 'beta']) {
|
|
773
805
|
const feature = path.join(root, 'specs', name);
|
|
774
|
-
|
|
775
|
-
fs.writeFileSync(path.join(feature, '
|
|
776
|
-
|
|
777
|
-
status: 'done',
|
|
778
|
-
current_phase: 'closeout',
|
|
779
|
-
task_registry: { 'tasks/task-R0-01-x.md': { status: 'done', completed_at: '2026-01-01T00:00:00.000Z' } },
|
|
780
|
-
}, null, 2)}\n`);
|
|
781
|
-
fs.writeFileSync(path.join(feature, 'tasks', 'task-R0-01-x.md'), '# Task\n\nStatus: done\n');
|
|
806
|
+
writeProcessTask(feature, 'done', null, 'node --test', 'task-01-x.md');
|
|
807
|
+
fs.writeFileSync(path.join(feature, 'task-02-y.md'),
|
|
808
|
+
'# Task 02\n\nStatus: pending\n\n## Dependencies\n\n- none\n\n## Verification Plan\n\n- Command: node --test\n');
|
|
782
809
|
}
|
|
783
810
|
const payload = { cwd: root, session_id: 'session-a', hook_event_name: 'Stop', stop_hook_active: false };
|
|
784
811
|
|
|
@@ -880,16 +907,7 @@ test('empty or forged Codex hook authority fails closed', () => {
|
|
|
880
907
|
assert.match(block.reason, /hook payload (is empty|must be a JSON object)/i);
|
|
881
908
|
}
|
|
882
909
|
|
|
883
|
-
|
|
884
|
-
const taskPath = 'tasks/task.md';
|
|
885
|
-
fs.mkdirSync(path.join(feature, 'tasks'), { recursive: true });
|
|
886
|
-
fs.writeFileSync(path.join(feature, 'spec.json'), JSON.stringify({
|
|
887
|
-
status: 'in_progress',
|
|
888
|
-
current_phase: 'closeout',
|
|
889
|
-
feature_name: 'auth',
|
|
890
|
-
task_registry: { [taskPath]: { status: 'done', completed_at: '2026-08-11T00:00:00.000Z' } },
|
|
891
|
-
}));
|
|
892
|
-
fs.writeFileSync(path.join(feature, taskPath), 'Status: done\n');
|
|
910
|
+
writeProcessTask(path.join(root, 'specs', 'auth'), 'done');
|
|
893
911
|
fs.writeFileSync(path.join(root, '.codex', 'runtime.json'), JSON.stringify({
|
|
894
912
|
spec: { completion_gate: false },
|
|
895
913
|
}));
|
|
@@ -911,40 +929,6 @@ test('empty or forged Codex hook authority fails closed', () => {
|
|
|
911
929
|
});
|
|
912
930
|
});
|
|
913
931
|
|
|
914
|
-
test('explicit Strict Codex completion ignores worker-writable proof strings and remains blocked', () => {
|
|
915
|
-
inHookFixture((root, hooks) => {
|
|
916
|
-
const feature = path.join(root, 'specs', 'auth');
|
|
917
|
-
const taskPath = 'tasks/task.md';
|
|
918
|
-
fs.mkdirSync(path.join(feature, 'tasks'), { recursive: true });
|
|
919
|
-
fs.writeFileSync(path.join(feature, 'spec.json'), JSON.stringify({
|
|
920
|
-
status: 'in_progress',
|
|
921
|
-
current_phase: 'closeout',
|
|
922
|
-
feature_name: 'auth',
|
|
923
|
-
workflow_policy: POLICY.workflowPolicySnapshot({ riskSignals: { auth: true }, assurance_level: 'Strict' }),
|
|
924
|
-
proofs: {
|
|
925
|
-
needsInspection: 'inspection completed',
|
|
926
|
-
needsIndependentAudit: 'PASS',
|
|
927
|
-
needsResearchGrounding: 'research completed',
|
|
928
|
-
},
|
|
929
|
-
task_registry: { [taskPath]: { status: 'done', completed_at: '2026-08-11T00:00:00.000Z' } },
|
|
930
|
-
}));
|
|
931
|
-
fs.writeFileSync(path.join(feature, taskPath), bindReceipt(root, [
|
|
932
|
-
'Status: done', '', '## Evidence', '', 'Verification: PASS',
|
|
933
|
-
'Command: node --test', 'Exit: 0', `Base: ${VALID_BASE}`, `Head: ${VALID_HEAD}`,
|
|
934
|
-
].join('\n'), 'auth'));
|
|
935
|
-
const result = runHook(path.join(hooks, 'spec-gate.cjs'), root, {
|
|
936
|
-
cwd: root,
|
|
937
|
-
session_id: 'session-a',
|
|
938
|
-
hook_event_name: 'Stop',
|
|
939
|
-
stop_hook_active: false,
|
|
940
|
-
});
|
|
941
|
-
assert.equal(result.status, 0, result.stderr);
|
|
942
|
-
const block = JSON.parse(result.stdout);
|
|
943
|
-
assert.equal(block.decision, 'block');
|
|
944
|
-
assert.match(block.reason, /needsInspection|needsIndependentAudit|needsResearchGrounding/);
|
|
945
|
-
});
|
|
946
|
-
});
|
|
947
|
-
|
|
948
932
|
test('Codex state hook persists direct native event fields at project root', () => {
|
|
949
933
|
inHookFixture((root, hooks) => {
|
|
950
934
|
const nested = path.join(root, 'packages', 'app');
|
|
@@ -1053,61 +1037,55 @@ test('Codex canonical receipt provenance requires both Base and Head', () => {
|
|
|
1053
1037
|
// Integration via spec-gate hook: only Base blocks, both passes
|
|
1054
1038
|
inHookFixture((root, hooks) => {
|
|
1055
1039
|
const feature = path.join(root, 'specs', 'auth');
|
|
1056
|
-
const taskPath = 'tasks/task-R0-01-auth.md';
|
|
1057
|
-
fs.mkdirSync(path.join(feature, 'tasks'), { recursive: true });
|
|
1058
1040
|
const gate = path.join(hooks, 'spec-gate.cjs');
|
|
1059
1041
|
const payload = { cwd: path.join(root, 'packages', 'app'), session_id: 'session-a', hook_event_name: 'Stop', stop_hook_active: false };
|
|
1042
|
+
const output = ['```', 'pnpm test', 'ok 1', '```'];
|
|
1043
|
+
fs.mkdirSync(payload.cwd, { recursive: true });
|
|
1060
1044
|
|
|
1061
1045
|
// only Base -> block
|
|
1062
|
-
|
|
1063
|
-
fs.writeFileSync(path.join(feature, taskPath), 'Status: done\n\n## Evidence\n\nVerification: PASS\nCommand: pnpm test\nExit: 0\nBase: abc\n');
|
|
1064
|
-
fs.mkdirSync(payload.cwd, { recursive: true });
|
|
1046
|
+
writeProcessTask(feature, 'done', ['Verification: PASS', 'Command: pnpm test', 'Exit: 0', 'Base: abc', ...output]);
|
|
1065
1047
|
const blocked = runHook(gate, payload.cwd, payload);
|
|
1066
1048
|
assert.equal(JSON.parse(blocked.stdout).decision, 'block');
|
|
1067
1049
|
assert.match(JSON.parse(blocked.stdout).reason, /\bprovenance\b/);
|
|
1068
1050
|
|
|
1069
1051
|
// both -> no block
|
|
1070
|
-
|
|
1052
|
+
writeProcessTask(feature, 'done', bindReceipt(root, ['Verification: PASS', 'Command: pnpm test', 'Exit: 0', `Base: ${VALID_BASE}`, `Head: ${VALID_HEAD}`, ...output].join('\n')).split('\n'));
|
|
1071
1053
|
const passed = runHook(gate, payload.cwd, payload);
|
|
1072
1054
|
assert.equal(passed.stdout, '');
|
|
1073
1055
|
|
|
1074
1056
|
// empty Base: should block provenance — reset gate cache so re-checked as newly-done
|
|
1075
1057
|
try { fs.rmSync(path.join(root, '.codex', 'hooks', '.logs', 'spec-gate-last.json'), { force: true }); } catch {}
|
|
1076
|
-
|
|
1058
|
+
writeProcessTask(feature, 'done', ['Verification: PASS', 'Command: pnpm test', 'Exit: 0', 'Base:', 'Head: def', ...output]);
|
|
1077
1059
|
const blockedEmpty = runHook(gate, payload.cwd, payload);
|
|
1078
1060
|
assert.equal(JSON.parse(blockedEmpty.stdout).decision, 'block');
|
|
1079
1061
|
assert.match(JSON.parse(blockedEmpty.stdout).reason, /\bprovenance\b/);
|
|
1080
1062
|
|
|
1081
1063
|
// bare base_sha without colon should block — reset cache again
|
|
1082
1064
|
try { fs.rmSync(path.join(root, '.codex', 'hooks', '.logs', 'spec-gate-last.json'), { force: true }); } catch {}
|
|
1083
|
-
|
|
1065
|
+
writeProcessTask(feature, 'done', ['Verification: PASS', 'Command: pnpm test', 'Exit: 0', 'base_sha head_sha', ...output]);
|
|
1084
1066
|
const blockedBare = runHook(gate, payload.cwd, payload);
|
|
1085
1067
|
assert.equal(JSON.parse(blockedBare.stdout).decision, 'block');
|
|
1086
1068
|
assert.match(JSON.parse(blockedBare.stdout).reason, /\bprovenance\b/);
|
|
1087
1069
|
});
|
|
1088
1070
|
});
|
|
1089
1071
|
|
|
1090
|
-
test('Codex cache hardening: every done re-validated — mutation,
|
|
1072
|
+
test('Codex cache hardening: every done re-validated — mutation, malformed cache, unchanged valid', () => {
|
|
1091
1073
|
inHookFixture((root, hooks) => {
|
|
1092
1074
|
const feature = path.join(root, 'specs', 'auth');
|
|
1093
|
-
const taskPath = '
|
|
1094
|
-
fs.mkdirSync(path.join(feature, 'tasks'), { recursive: true });
|
|
1075
|
+
const taskPath = 'task-01-auth.md';
|
|
1095
1076
|
const gate = path.join(hooks, 'spec-gate.cjs');
|
|
1096
1077
|
const appCwd = path.join(root, 'packages', 'app');
|
|
1097
1078
|
fs.mkdirSync(appCwd, { recursive: true });
|
|
1098
1079
|
const payload = { cwd: appCwd, session_id: 'session-a', hook_event_name: 'Stop', stop_hook_active: false };
|
|
1099
1080
|
const cacheFile = path.join(root, '.codex', 'hooks', '.logs', 'spec-gate-last.json');
|
|
1100
|
-
const
|
|
1101
|
-
|
|
1102
|
-
'Command: pnpm test', 'Exit: 0', `Base: ${VALID_BASE}`, `Head: ${VALID_HEAD}`,
|
|
1103
|
-
|
|
1104
|
-
].join('\n');
|
|
1081
|
+
const output = ['```', 'pass', '```'];
|
|
1082
|
+
const writeValid = () => writeProcessTask(feature, 'done', (writeProcessTask(feature, 'done'), bindReceipt(root, [
|
|
1083
|
+
'Verification: PASS', 'Command: pnpm test', 'Exit: 0', `Base: ${VALID_BASE}`, `Head: ${VALID_HEAD}`, ...output,
|
|
1084
|
+
].join('\n'))).split('\n'));
|
|
1105
1085
|
|
|
1106
1086
|
// first-run (no cache) with valid receipt → no block, cache seeded
|
|
1107
1087
|
try { fs.rmSync(cacheFile, { force: true }); } catch {}
|
|
1108
|
-
|
|
1109
|
-
const validBody = bindReceipt(root, validBodyTemplate);
|
|
1110
|
-
fs.writeFileSync(path.join(feature, taskPath), validBody);
|
|
1088
|
+
writeValid();
|
|
1111
1089
|
let res = runHook(gate, appCwd, payload);
|
|
1112
1090
|
assert.equal(res.stdout, '', 'first-run valid should not block');
|
|
1113
1091
|
assert.ok(fs.existsSync(cacheFile), 'cache file must be created on first valid run');
|
|
@@ -1119,7 +1097,7 @@ test('Codex cache hardening: every done re-validated — mutation, deletion, mal
|
|
|
1119
1097
|
assert.equal(res.stdout, '', 'cache-hit unchanged valid must not block');
|
|
1120
1098
|
|
|
1121
1099
|
// mutation: remove Command/Verification → must block even though cache says done
|
|
1122
|
-
|
|
1100
|
+
writeProcessTask(feature, 'done', ['Exit: 0', 'Base: abc123', 'Head: def456', ...output]);
|
|
1123
1101
|
res = runHook(gate, appCwd, payload);
|
|
1124
1102
|
assert.equal(JSON.parse(res.stdout).decision, 'block', 'mutated receipt missing Verification/Command should block on cache-hit');
|
|
1125
1103
|
assert.match(JSON.parse(res.stdout).reason, /\bverification_state\b|\bcommand\b/);
|
|
@@ -1128,33 +1106,21 @@ test('Codex cache hardening: every done re-validated — mutation, deletion, mal
|
|
|
1128
1106
|
assert.equal(JSON.parse(res.stdout).decision, 'block', 'second hit after mutation still blocks');
|
|
1129
1107
|
|
|
1130
1108
|
// restore valid, then mutate provenance (remove Head) → block
|
|
1131
|
-
|
|
1109
|
+
writeValid();
|
|
1132
1110
|
assert.equal(runHook(gate, appCwd, payload).stdout, '', 'restored valid should pass again');
|
|
1133
|
-
|
|
1111
|
+
writeProcessTask(feature, 'done', ['Verification: PASS', 'Command: pnpm test', 'Exit: 0', 'Base: abc123', ...output]);
|
|
1134
1112
|
res = runHook(gate, appCwd, payload);
|
|
1135
1113
|
assert.equal(JSON.parse(res.stdout).decision, 'block');
|
|
1136
1114
|
assert.match(JSON.parse(res.stdout).reason, /\bprovenance\b/);
|
|
1137
1115
|
|
|
1138
|
-
// deletion → block with check a
|
|
1139
|
-
fs.writeFileSync(path.join(feature, taskPath), validBody);
|
|
1140
|
-
assert.equal(runHook(gate, appCwd, payload).stdout, '');
|
|
1141
|
-
fs.unlinkSync(path.join(feature, taskPath));
|
|
1142
|
-
res = runHook(gate, appCwd, payload);
|
|
1143
|
-
assert.equal(JSON.parse(res.stdout).decision, 'block');
|
|
1144
|
-
assert.match(JSON.parse(res.stdout).reason, /\ba\b/);
|
|
1145
|
-
// restore for next sub-test
|
|
1146
|
-
fs.writeFileSync(path.join(feature, taskPath), validBody);
|
|
1147
|
-
try { fs.rmSync(cacheFile, { force: true }); } catch {}
|
|
1148
|
-
assert.equal(runHook(gate, appCwd, payload).stdout, '');
|
|
1149
|
-
|
|
1150
1116
|
// malformed cache with valid receipt → must still pass (cache parse fail-open)
|
|
1117
|
+
writeValid();
|
|
1151
1118
|
fs.writeFileSync(cacheFile, '{ malformed');
|
|
1152
|
-
fs.writeFileSync(path.join(feature, taskPath), validBody);
|
|
1153
1119
|
res = runHook(gate, appCwd, payload);
|
|
1154
1120
|
assert.equal(res.stdout, '', 'malformed cache with valid receipt must not block');
|
|
1155
1121
|
// malformed cache with invalid receipt → must still block
|
|
1156
1122
|
fs.writeFileSync(cacheFile, '{ malformed again');
|
|
1157
|
-
|
|
1123
|
+
writeProcessTask(feature, 'done');
|
|
1158
1124
|
res = runHook(gate, appCwd, payload);
|
|
1159
1125
|
assert.equal(JSON.parse(res.stdout).decision, 'block', 'malformed cache with invalid receipt must block');
|
|
1160
1126
|
});
|
|
@@ -1164,18 +1130,11 @@ test('Codex cache identity separates same-feature receipts from different roots'
|
|
|
1164
1130
|
const contexts = [];
|
|
1165
1131
|
for (let index = 0; index < 2; index += 1) {
|
|
1166
1132
|
const context = inHookFixture((root, hooks) => {
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
feature_name: 'auth',
|
|
1173
|
-
task_registry: { [taskPath]: { status: 'done', completed_at: '2026-08-11T00:00:00.000Z' } },
|
|
1174
|
-
}));
|
|
1175
|
-
fs.writeFileSync(path.join(feature, taskPath), bindReceipt(root, [
|
|
1176
|
-
'Status: done', '', '## Evidence', '', 'Verification: PASS',
|
|
1177
|
-
'Command: node --test', 'Exit: 0', `Base: ${VALID_BASE}`, `Head: ${VALID_HEAD}`,
|
|
1178
|
-
].join('\n')));
|
|
1133
|
+
writeProcessTask(path.join(root, 'specs', 'auth'), 'done', null, 'node --test');
|
|
1134
|
+
writeProcessTask(path.join(root, 'specs', 'auth'), 'done', bindReceipt(root, [
|
|
1135
|
+
'Verification: PASS', 'Command: node --test', 'Exit: 0', `Base: ${VALID_BASE}`, `Head: ${VALID_HEAD}`,
|
|
1136
|
+
'```', 'node --test', 'ok 1', '```',
|
|
1137
|
+
].join('\n')).split('\n'), 'node --test');
|
|
1179
1138
|
const result = runHook(path.join(hooks, 'spec-gate.cjs'), root, {
|
|
1180
1139
|
cwd: root,
|
|
1181
1140
|
session_id: 'session-a',
|
|
@@ -1212,33 +1171,32 @@ test('Codex P0 regression: placeholder, explicit failure, artifact, symlink and
|
|
|
1212
1171
|
assert.ok(specReceipt.validateCanonicalReceipt('Verification: PASS\nCommand: pnpm test\nExit: 0\nBase: a\nHead: b\nArtifact: x\nsha256: TBD\n').includes('artifact_hash'));
|
|
1213
1172
|
assert.ok(specReceipt.validateCanonicalReceipt('Verification: PASS\nCommand: pnpm test\nExit: 0\nBase: a\nHead: b\nArtifact: x\nsha256: \n').includes('artifact_hash'));
|
|
1214
1173
|
assert.deepEqual(specReceipt.validateCanonicalReceipt(`Verification: PASS\nCommand: pnpm test\nExit: 0\nBase: ${VALID_BASE}\nHead: ${VALID_HEAD}\nArtifact: bundle\nsha256: ${'a'.repeat(64)}\n`), []);
|
|
1215
|
-
// Probe D/E: symlink
|
|
1174
|
+
// Probe D/E: symlink packet and task
|
|
1216
1175
|
const tmp = fs2.mkdtempSync(path2.join(os2.tmpdir(), 'cafekit-codex-reg-'));
|
|
1217
1176
|
try {
|
|
1218
1177
|
const specsDir = path2.join(tmp, 'specs');
|
|
1219
|
-
|
|
1220
|
-
fs2.mkdirSync(path2.join(specsDir, 'valid'), { recursive: true });
|
|
1221
|
-
fs2.writeFileSync(path2.join(specsDir, 'valid', 'spec.json'), JSON.stringify({ status: 'in_progress' }));
|
|
1178
|
+
writeProcessTask(path2.join(specsDir, 'valid'), 'pending');
|
|
1222
1179
|
const outside = path2.join(tmp, 'outside');
|
|
1223
|
-
|
|
1224
|
-
fs2.
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
// task symlink
|
|
1180
|
+
writeProcessTask(outside, 'pending');
|
|
1181
|
+
fs2.symlinkSync(outside, path2.join(specsDir, 'linked'));
|
|
1182
|
+
assert.equal(specUtils.resolveWorkflowCandidate(tmp, {}, 'linked', null).error, 'explicit_malformed');
|
|
1183
|
+
assert.equal(specUtils.resolveWorkflowCandidate(tmp, {}, null, path2.join(specsDir, 'linked', 'plan.md')).error, 'explicit_malformed');
|
|
1184
|
+
fs2.rmSync(path2.join(specsDir, 'linked'));
|
|
1185
|
+
// task symlink to a file outside the packet
|
|
1230
1186
|
const featDir = path2.join(specsDir, 'valid');
|
|
1231
|
-
fs2.mkdirSync(path2.join(featDir, 'tasks'), { recursive: true });
|
|
1232
1187
|
const outsideTask = path2.join(tmp, 'outside-task.md');
|
|
1233
|
-
fs2.
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
assert.
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1188
|
+
fs2.copyFileSync(path2.join(featDir, 'task-01-auth.md'), outsideTask);
|
|
1189
|
+
fs2.symlinkSync(outsideTask, path2.join(featDir, 'task-02-link.md'));
|
|
1190
|
+
const proof = specReceipt.checkWorkflowReceiptDetails(featDir, 'task-02-link.md', null);
|
|
1191
|
+
assert.ok(proof.failures.includes('unsafe_path'), `task symlink must be unsafe, got ${proof.failures}`);
|
|
1192
|
+
assert.equal(specUtils.resolveWorkflowCandidate(tmp, {}, null, null).error, 'invalid_specs');
|
|
1193
|
+
fs2.rmSync(path2.join(featDir, 'task-02-link.md'));
|
|
1194
|
+
// malformed packet: dependency on a missing task
|
|
1195
|
+
const bad = path2.join(specsDir, 'bad');
|
|
1196
|
+
writeProcessTask(bad, 'pending');
|
|
1197
|
+
fs2.writeFileSync(path2.join(bad, 'task-01-auth.md'),
|
|
1198
|
+
fs2.readFileSync(path2.join(bad, 'task-01-auth.md'), 'utf8').replace('- none', '- task-09-missing.md'));
|
|
1199
|
+
const mal = specUtils.resolveWorkflowCandidate(tmp, {}, null, null);
|
|
1242
1200
|
assert.equal(mal.error, 'invalid_specs');
|
|
1243
1201
|
} finally {
|
|
1244
1202
|
fs2.rmSync(tmp, { recursive: true, force: true });
|
|
@@ -1289,17 +1247,10 @@ test('Codex adapter parity - phantom vectors via shared validator (r3)', () => {
|
|
|
1289
1247
|
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'cafekit-codex-tap-heading-'));
|
|
1290
1248
|
try {
|
|
1291
1249
|
const featureDir = path.join(tmp, 'feature');
|
|
1292
|
-
const
|
|
1293
|
-
|
|
1294
|
-
const
|
|
1295
|
-
|
|
1296
|
-
const tapFailure = receipt.checkReceipt(featureDir, taskPath, { status: 'done', completed_at: '2026-08-11T00:00:00.000Z' });
|
|
1297
|
-
assert.ok(tapFailure.includes('c'), 'unindented TAP # fail 1 must reach the shared validator');
|
|
1298
|
-
|
|
1299
|
-
fs.writeFileSync(path.join(featureDir, taskPath), [...taskPrefix, '# Notes', 'FAILED tests/test_demo.py', ''].join('\n'));
|
|
1300
|
-
const markdownBody = receipt.evidenceBody(fs.readFileSync(path.join(featureDir, taskPath), 'utf8'));
|
|
1301
|
-
assert.doesNotMatch(markdownBody, /FAILED tests\/test_demo\.py/);
|
|
1302
|
-
assert.deepEqual(receipt.validateCanonicalReceipt(markdownBody), [], 'a real Markdown heading must still end Evidence');
|
|
1250
|
+
const receiptFields = base.trim().split('\n');
|
|
1251
|
+
writeProcessTask(featureDir, 'done', [...receiptFields, '```', 'pnpm test', '# fail 1', '```']);
|
|
1252
|
+
const tapFailure = receipt.checkWorkflowReceiptDetails(featureDir, 'task-01-auth.md', null);
|
|
1253
|
+
assert.ok(tapFailure.failures.includes('verification_state'), `unindented TAP # fail 1 must reach the shared validator, got ${tapFailure.failures}`);
|
|
1303
1254
|
} finally {
|
|
1304
1255
|
fs.rmSync(tmp, { recursive: true, force: true });
|
|
1305
1256
|
}
|
|
@@ -1329,38 +1280,20 @@ test('Codex adapter enforces declared task artifact SHA-256 with missing, invali
|
|
|
1329
1280
|
const digest = crypto.createHash('sha256').update(artifact).digest('hex');
|
|
1330
1281
|
const cases = [
|
|
1331
1282
|
['', true],
|
|
1332
|
-
['sha256: abc123
|
|
1333
|
-
[`sha256: ${'0'.repeat(64)}
|
|
1334
|
-
[`sha256: ${digest}
|
|
1283
|
+
['sha256: abc123', true],
|
|
1284
|
+
[`sha256: ${'0'.repeat(64)}`, true],
|
|
1285
|
+
[`sha256: ${digest}`, false],
|
|
1335
1286
|
];
|
|
1336
1287
|
for (const [hashLine, shouldBlock] of cases) {
|
|
1337
1288
|
inHookFixture((root, hooks) => {
|
|
1338
1289
|
const feature = path.join(root, 'specs', 'artifact-demo');
|
|
1339
|
-
const taskPath = 'tasks/task.md';
|
|
1340
|
-
fs.mkdirSync(path.join(feature, 'tasks'), { recursive: true });
|
|
1341
1290
|
fs.mkdirSync(path.join(root, 'output'), { recursive: true });
|
|
1342
1291
|
fs.writeFileSync(path.join(root, 'output', 'bundle.js'), artifact);
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
current_phase: 'closeout',
|
|
1346
|
-
feature_name: 'artifact-demo',
|
|
1347
|
-
task_registry: {
|
|
1348
|
-
[taskPath]: {
|
|
1349
|
-
status: 'done',
|
|
1350
|
-
completed_at: '2026-08-11T00:00:00.000Z',
|
|
1351
|
-
artifacts: ['output/bundle.js'],
|
|
1352
|
-
},
|
|
1353
|
-
},
|
|
1354
|
-
}));
|
|
1355
|
-
fs.writeFileSync(path.join(feature, taskPath), [
|
|
1356
|
-
'# Task', '', '**Status:** done', '', '## Evidence', '',
|
|
1292
|
+
writeProcessTask(feature, 'done', null, 'node --test');
|
|
1293
|
+
writeProcessTask(feature, 'done', bindReceipt(root, [
|
|
1357
1294
|
'Verification: PASS', 'Command: node --test', 'Exit: 0', `Base: ${VALID_BASE}`, `Head: ${VALID_HEAD}`,
|
|
1358
|
-
'Artifact: output/bundle.js', hashLine,
|
|
1359
|
-
].join('\n'));
|
|
1360
|
-
fs.writeFileSync(
|
|
1361
|
-
path.join(feature, taskPath),
|
|
1362
|
-
bindReceipt(root, fs.readFileSync(path.join(feature, taskPath), 'utf8'), 'artifact-demo'),
|
|
1363
|
-
);
|
|
1295
|
+
'Artifact: output/bundle.js', hashLine, '```', 'node --test', 'ok 1', '```',
|
|
1296
|
+
].join('\n'), 'artifact-demo').split('\n'), 'node --test');
|
|
1364
1297
|
const result = runHook(path.join(hooks, 'spec-gate.cjs'), root, {
|
|
1365
1298
|
cwd: root,
|
|
1366
1299
|
session_id: 'session-a',
|