@haposoft/cafekit 0.16.7 → 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 +14 -22
- package/bin/__tests__/codex-hooks-ownership.test.js +2 -2
- package/bin/__tests__/codex-hooks.test.js +169 -272
- package/bin/__tests__/codex-native.test.js +100 -202
- package/bin/__tests__/develop-contract.test.js +122 -1413
- package/bin/__tests__/docs-sync-on-demand.test.js +93 -0
- package/bin/__tests__/grok-envelope.test.js +0 -56
- package/bin/__tests__/hook-payload.test.js +4 -6
- package/bin/__tests__/legacy-authority-decoupled.test.js +154 -0
- package/bin/__tests__/legacy-hooks-retired.test.js +221 -0
- package/bin/__tests__/omp-bridge.test.js +20 -17
- package/bin/__tests__/omp-hooks.test.js +0 -19
- package/bin/__tests__/optional-skill-inventory.test.js +112 -4
- package/bin/__tests__/orca-session.test.js +0 -2
- package/bin/__tests__/package-inventory.test.js +90 -925
- package/bin/__tests__/runtime-dir.test.js +0 -3
- package/bin/__tests__/runtime-schema.test.js +3 -2
- package/bin/__tests__/skill-routing-source.test.js +27 -15
- package/bin/__tests__/spec-narrowing.test.js +30 -161
- package/bin/__tests__/specs-v2-execution-closeout.test.js +0 -437
- package/bin/__tests__/test-runtime-dependency-closure.cjs +1 -2
- package/bin/__tests__/ui-ux-skill.test.js +101 -0
- package/bin/__tests__/usage-hook-retired.test.js +114 -0
- package/bin/__tests__/validator-grounding.test.js +18 -2949
- package/bin/lib/managed-writer.js +2 -3
- package/bin/phases/codex-runtime.js +27 -0
- package/bin/phases/copy-payload.js +13 -28
- package/bin/phases/omp-runtime.js +25 -0
- package/package.json +2 -3
- package/src/claude/CLAUDE.md +3 -10
- package/src/claude/agents/brainstormer.md +7 -3
- package/src/claude/agents/code-auditor.md +44 -56
- package/src/claude/agents/debugger.md +8 -88
- package/src/claude/agents/deployer.md +3 -5
- package/src/claude/agents/docs-keeper.md +6 -14
- package/src/claude/agents/git-ops.md +2 -4
- package/src/claude/agents/implementer.md +12 -16
- package/src/claude/agents/inspector.md +4 -3
- package/src/claude/agents/project-manager.md +9 -17
- package/src/claude/agents/researcher.md +9 -3
- package/src/claude/agents/spec-maker.md +15 -15
- package/src/claude/agents/test-runner.md +8 -11
- package/src/claude/agents/ui-ux-designer.md +0 -1
- package/src/claude/hooks/__tests__/fixtures/statusline-default.golden +1 -1
- package/src/claude/hooks/__tests__/spec-gate.test.js +244 -333
- package/src/claude/hooks/__tests__/spec-state-touched.test.js +160 -0
- package/src/claude/hooks/__tests__/state.test.js +68 -0
- package/src/claude/hooks/__tests__/statusline.test.js +140 -11
- package/src/claude/hooks/agent.cjs +2 -1
- package/src/claude/hooks/lib/hook-payload.cjs +2 -2
- package/src/claude/hooks/rules.cjs +2 -1
- package/src/claude/hooks/session.cjs +0 -1
- package/src/claude/hooks/spec-gate.cjs +17 -83
- package/src/claude/hooks/spec-state.cjs +142 -39
- package/src/claude/hooks/state.cjs +21 -2
- package/src/claude/migration-manifest.json +42 -22
- 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 +2 -3
- package/src/claude/rules/hook-protocols.md +8 -15
- package/src/claude/rules/manage-docs.md +9 -23
- package/src/claude/rules/orchestrator.md +3 -4
- package/src/claude/rules/review-audit-self-decision.md +3 -3
- package/src/claude/rules/skill-domain-routing.md +1 -3
- package/src/claude/rules/skill-workflow-routing.md +4 -5
- package/src/claude/rules/state-sync.md +3 -12
- package/src/claude/rules/workflow.md +11 -19
- package/src/claude/runtime.schema.json +5 -3
- package/src/claude/scripts/generate-skill-catalog.cjs +7 -6
- package/src/claude/scripts/provenance.cjs +23 -5
- package/src/claude/scripts/spec-receipt.cjs +24 -121
- package/src/claude/scripts/spec-resolver.cjs +41 -458
- package/src/claude/scripts/workflow-policy.cjs +3 -1801
- package/src/claude/settings/settings.json +1 -32
- package/src/claude/skills/agent-browser/SKILL.md +1 -1
- package/src/claude/skills/{question → ask}/SKILL.md +9 -75
- package/src/claude/skills/brainstorm/SKILL.md +43 -131
- package/src/claude/skills/chrome-devtools/SKILL.md +14 -31
- package/src/claude/skills/code-review/SKILL.md +29 -81
- package/src/claude/skills/code-review/references/spec-compliance-review.md +4 -8
- package/src/claude/skills/debug/SKILL.md +29 -149
- package/src/claude/skills/develop/SKILL.md +26 -83
- package/src/claude/skills/develop/references/parallel-waves.md +0 -8
- package/src/claude/skills/develop/references/quality-gate.md +15 -13
- package/src/claude/skills/develop/references/subagent-patterns.md +0 -6
- package/src/claude/skills/docs/SKILL.md +2 -2
- package/src/claude/skills/docs/references/update-workflow.md +1 -1
- package/src/claude/skills/fix/SKILL.md +153 -0
- package/src/claude/skills/{hotfix → fix}/references/diagnosis-protocol.md +2 -2
- package/src/claude/skills/{hotfix → fix}/references/parallel-patterns.md +2 -2
- package/src/claude/skills/{hotfix → fix}/references/prevention-gate.md +3 -3
- package/src/claude/skills/{hotfix → fix}/references/review-cycle.md +1 -1
- package/src/claude/skills/git/SKILL.md +48 -9
- package/src/claude/skills/git/references/commit-protocols.md +35 -16
- package/src/claude/skills/git/references/worktree-blueprint.md +42 -6
- package/src/claude/skills/orca/SKILL.md +9 -8
- package/src/claude/skills/research/SKILL.md +17 -46
- package/src/claude/skills/scout/SKILL.md +61 -0
- package/src/claude/skills/{inspect → scout}/references/internal-inspection.md +26 -10
- package/src/claude/skills/specs/SKILL.md +22 -29
- package/src/claude/skills/specs/references/review.md +8 -6
- package/src/claude/skills/specs/references/templates.md +34 -41
- package/src/claude/skills/sync/SKILL.md +16 -45
- package/src/claude/skills/sync/references/rebind-and-audit.md +59 -0
- package/src/claude/skills/sync/references/sync-protocols.md +4 -13
- package/src/claude/skills/test/SKILL.md +39 -104
- package/src/claude/skills/test/references/execution-strategy.md +93 -21
- package/src/claude/skills/test/references/failure-triage.md +1 -1
- package/src/claude/skills/ui-ux/LICENSE +21 -0
- package/src/claude/skills/ui-ux/SKILL.md +497 -0
- package/src/claude/skills/ui-ux/references/brand-tokens.md +90 -0
- package/src/claude/skills/ui-ux/references/budgets.md +113 -0
- package/src/claude/skills/ui-ux/references/checklist.md +271 -0
- package/src/claude/skills/ui-ux/references/components/accordion.md +246 -0
- package/src/claude/skills/ui-ux/references/components/avatar.md +153 -0
- package/src/claude/skills/ui-ux/references/components/banner.md +43 -0
- package/src/claude/skills/ui-ux/references/components/breadcrumb.md +201 -0
- package/src/claude/skills/ui-ux/references/components/button.md +142 -0
- package/src/claude/skills/ui-ux/references/components/card.md +113 -0
- package/src/claude/skills/ui-ux/references/components/charts.md +376 -0
- package/src/claude/skills/ui-ux/references/components/chat.md +132 -0
- package/src/claude/skills/ui-ux/references/components/choice-controls.md +369 -0
- package/src/claude/skills/ui-ux/references/components/comment-thread.md +17 -0
- package/src/claude/skills/ui-ux/references/components/description-list.md +116 -0
- package/src/claude/skills/ui-ux/references/components/empty-state.md +98 -0
- package/src/claude/skills/ui-ux/references/components/file-upload.md +107 -0
- package/src/claude/skills/ui-ux/references/components/inline-edit.md +128 -0
- package/src/claude/skills/ui-ux/references/components/input.md +161 -0
- package/src/claude/skills/ui-ux/references/components/list-row.md +56 -0
- package/src/claude/skills/ui-ux/references/components/loading.md +198 -0
- package/src/claude/skills/ui-ux/references/components/logo.md +147 -0
- package/src/claude/skills/ui-ux/references/components/otp-input.md +43 -0
- package/src/claude/skills/ui-ux/references/components/quantity-input.md +100 -0
- package/src/claude/skills/ui-ux/references/components/range-slider.md +16 -0
- package/src/claude/skills/ui-ux/references/components/small-controls.md +340 -0
- package/src/claude/skills/ui-ux/references/components/sortable-header.md +86 -0
- package/src/claude/skills/ui-ux/references/components/tag-input.md +14 -0
- package/src/claude/skills/ui-ux/references/components/timeline.md +38 -0
- package/src/claude/skills/ui-ux/references/components/tree.md +30 -0
- package/src/claude/skills/ui-ux/references/design-process.md +650 -0
- package/src/claude/skills/ui-ux/references/layouts/app-kanban.html +303 -0
- package/src/claude/skills/ui-ux/references/layouts/app.md +1369 -0
- package/src/claude/skills/ui-ux/references/layouts/form.md +430 -0
- package/src/claude/skills/ui-ux/references/layouts/overlay.md +552 -0
- package/src/claude/skills/ui-ux/references/layouts/pricing.md +204 -0
- package/src/claude/skills/ui-ux/references/locked-rules.md +35 -0
- package/src/claude/skills/ui-ux/references/principles.md +408 -0
- package/src/claude/skills/ui-ux/references/refactor.md +245 -0
- package/src/claude/skills/ui-ux/references/responsive.md +195 -0
- package/src/claude/skills/ui-ux/references/review.md +528 -0
- package/src/claude/skills/ui-ux/references/rules-color.md +623 -0
- package/src/claude/skills/ui-ux/references/rules-form.md +235 -0
- package/src/claude/skills/ui-ux/references/rules-state.md +680 -0
- package/src/claude/skills/ui-ux/references/rules-type.md +369 -0
- package/src/claude/skills/ui-ux/references/styles.md +311 -0
- package/src/claude/skills/ui-ux/references/system.md +219 -0
- package/src/claude/skills/ui-ux/references/tailwind-v4-traps.md +237 -0
- package/src/claude/skills/ui-ux/references/tokens.css +362 -0
- package/src/claude/skills/ui-ux/scripts/lint-skill.mjs +216 -0
- package/src/claude/skills/ui-ux/scripts/package.json +9 -0
- package/src/claude/skills/ui-ux/scripts/probe.mjs +4447 -0
- package/src/claude/skills/ui-ux-pro-max/SKILL.md +25 -80
- package/src/claude/skills/web-testing/SKILL.md +2 -20
- package/src/claude/status.cjs +149 -144
- package/src/codex/AGENTS.md +8 -13
- package/src/codex/hooks/agent.cjs +2 -1
- package/src/codex/hooks/lib/spec-receipt.cjs +4 -32
- package/src/codex/hooks/lib/spec-utils.cjs +6 -52
- package/src/codex/hooks/rules.cjs +2 -1
- package/src/codex/hooks/session.cjs +0 -2
- package/src/codex/hooks/spec-gate.cjs +13 -81
- package/src/codex/hooks/spec-state.cjs +37 -41
- package/src/codex/hooks.json +0 -20
- package/src/codex/rules/state-sync.md +3 -12
- package/src/common/AGENTS.md +6 -12
- package/src/omp/extensions/cafekit-bridge.mjs +6 -6
- 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/__tests__/completion-authority.test.js +0 -990
- package/src/claude/hooks/__tests__/semantic-review-authority.test.js +0 -259
- package/src/claude/hooks/completion-authority-check.cjs +0 -56
- package/src/claude/hooks/completion-authority-state.cjs +0 -527
- package/src/claude/hooks/completion-authority.cjs +0 -163
- package/src/claude/hooks/lib/context.cjs +0 -616
- package/src/claude/hooks/lib/detect.cjs +0 -474
- package/src/claude/hooks/semantic-review-authority.cjs +0 -154
- package/src/claude/hooks/task-scaffold-guard.cjs +0 -114
- package/src/claude/hooks/usage.cjs +0 -197
- 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 -3941
- package/src/claude/skills/code-review/references/verification-gate.md +0 -46
- package/src/claude/skills/delegate/SKILL.md +0 -124
- package/src/claude/skills/delegate/references/codex-delegation.md +0 -71
- package/src/claude/skills/delegate/references/grok-delegation.md +0 -84
- package/src/claude/skills/hotfix/SKILL.md +0 -289
- package/src/claude/skills/inspect/SKILL.md +0 -142
- package/src/claude/skills/loop/SKILL.md +0 -114
- package/src/claude/skills/loop/references/bounded-loop-protocol.md +0 -104
- package/src/claude/skills/loop/references/metric-and-guard-contract.md +0 -76
- package/src/claude/skills/route/SKILL.md +0 -94
- package/src/claude/skills/route/references/agent-timing.md +0 -46
- package/src/claude/skills/route/references/chaining-patterns.md +0 -53
- package/src/claude/skills/route/references/task-taxonomy.md +0 -48
- 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/src/codex/hooks/completion-authority-check.cjs +0 -67
- package/src/codex/hooks/completion-authority-state.cjs +0 -527
- package/src/codex/hooks/completion-authority.cjs +0 -109
- package/src/codex/hooks/semantic-review-authority.cjs +0 -130
- package/src/codex/hooks/task-scaffold-guard.cjs +0 -63
- /package/src/claude/skills/{question → ask}/templates/question.md +0 -0
- /package/src/claude/skills/{hotfix → fix}/references/escalation-tactics.md +0 -0
- /package/src/claude/skills/{hotfix → fix}/references/workflow-specialized.md +0 -0
|
@@ -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');
|
|
@@ -611,40 +666,12 @@ test('Codex inspect hook scans native shell commands and structured paths', () =
|
|
|
611
666
|
});
|
|
612
667
|
});
|
|
613
668
|
|
|
614
|
-
test('Codex task scaffold hook blocks apply_patch Add File with stderr guidance', () => {
|
|
615
|
-
inHookFixture((root, hooks) => {
|
|
616
|
-
const result = runHook(path.join(hooks, 'task-scaffold-guard.cjs'), root, {
|
|
617
|
-
cwd: root,
|
|
618
|
-
session_id: 'session-a',
|
|
619
|
-
hook_event_name: 'PreToolUse',
|
|
620
|
-
tool_name: 'apply_patch',
|
|
621
|
-
tool_input: {
|
|
622
|
-
command: '*** Begin Patch\n*** Add File: specs/auth/tasks/task-R0-01-auth.md\n+x\n*** End Patch\n'
|
|
623
|
-
}
|
|
624
|
-
});
|
|
625
|
-
|
|
626
|
-
assert.equal(result.status, 2);
|
|
627
|
-
assert.match(result.stderr, /TASK SCAFFOLD REQUIRED/);
|
|
628
|
-
assert.match(result.stderr, /node \.codex\/scripts\/spec-scaffold\.cjs auth/);
|
|
629
|
-
});
|
|
630
|
-
});
|
|
631
|
-
|
|
632
669
|
test('Codex completion gate cannot be bypassed from a nested cwd', () => {
|
|
633
670
|
inHookFixture((root, hooks) => {
|
|
634
671
|
const nested = path.join(root, 'packages', 'app');
|
|
635
672
|
const feature = path.join(root, 'specs', 'auth');
|
|
636
|
-
const taskPath = 'tasks/task-R0-01-auth.md';
|
|
637
|
-
fs.mkdirSync(path.join(feature, 'tasks'), { recursive: true });
|
|
638
673
|
fs.mkdirSync(nested, { recursive: true });
|
|
639
|
-
|
|
640
|
-
status: 'in_progress',
|
|
641
|
-
feature_name: 'auth',
|
|
642
|
-
task_registry: {
|
|
643
|
-
[taskPath]: { status: 'pending', completed_at: null }
|
|
644
|
-
}
|
|
645
|
-
};
|
|
646
|
-
fs.writeFileSync(path.join(feature, 'spec.json'), `${JSON.stringify(spec)}\n`);
|
|
647
|
-
fs.writeFileSync(path.join(feature, taskPath), 'Status: pending\n');
|
|
674
|
+
writeProcessTask(feature, 'pending', null, 'node --test');
|
|
648
675
|
const gate = path.join(hooks, 'spec-gate.cjs');
|
|
649
676
|
const payload = {
|
|
650
677
|
cwd: nested,
|
|
@@ -658,32 +685,19 @@ test('Codex completion gate cannot be bypassed from a nested cwd', () => {
|
|
|
658
685
|
assert.equal(seeded.status, 0);
|
|
659
686
|
assert.equal(seeded.stdout, '');
|
|
660
687
|
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
'Status: done', '', '## Evidence', '', 'Verification: PASS',
|
|
675
|
-
'Command: node --test', 'Exit: 0', `Base: ${VALID_BASE}`, `Head: ${VALID_HEAD}`,
|
|
676
|
-
].join('\n'), 'auth'));
|
|
677
|
-
const validBeforeCloseout = runHook(gate, nested, payload);
|
|
678
|
-
assert.equal(validBeforeCloseout.status, 0);
|
|
679
|
-
assert.equal(validBeforeCloseout.stdout, '');
|
|
680
|
-
assert.equal(fs.existsSync(path.join(feature, 'feature-receipt.md')), false);
|
|
681
|
-
|
|
682
|
-
spec.current_phase = 'closeout';
|
|
683
|
-
fs.writeFileSync(path.join(feature, 'spec.json'), `${JSON.stringify(spec)}\n`);
|
|
684
|
-
const closeout = runHook(gate, nested, payload);
|
|
685
|
-
assert.equal(closeout.status, 0);
|
|
686
|
-
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, '');
|
|
687
701
|
});
|
|
688
702
|
});
|
|
689
703
|
|
|
@@ -783,20 +797,15 @@ test('Codex process-v3 Receipt command must match the exact Verification Plan co
|
|
|
783
797
|
});
|
|
784
798
|
|
|
785
799
|
test('Codex recorded active feature turns the identity block into receipt validation', () => {
|
|
786
|
-
// Issue #79 end to end on this runtime. Two
|
|
787
|
-
//
|
|
788
|
-
//
|
|
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.
|
|
789
803
|
inHookFixture((root, hooks) => {
|
|
790
804
|
for (const name of ['alpha', 'beta']) {
|
|
791
805
|
const feature = path.join(root, 'specs', name);
|
|
792
|
-
|
|
793
|
-
fs.writeFileSync(path.join(feature, '
|
|
794
|
-
|
|
795
|
-
status: 'done',
|
|
796
|
-
current_phase: 'closeout',
|
|
797
|
-
task_registry: { 'tasks/task-R0-01-x.md': { status: 'done', completed_at: '2026-01-01T00:00:00.000Z' } },
|
|
798
|
-
}, null, 2)}\n`);
|
|
799
|
-
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');
|
|
800
809
|
}
|
|
801
810
|
const payload = { cwd: root, session_id: 'session-a', hook_event_name: 'Stop', stop_hook_active: false };
|
|
802
811
|
|
|
@@ -898,16 +907,7 @@ test('empty or forged Codex hook authority fails closed', () => {
|
|
|
898
907
|
assert.match(block.reason, /hook payload (is empty|must be a JSON object)/i);
|
|
899
908
|
}
|
|
900
909
|
|
|
901
|
-
|
|
902
|
-
const taskPath = 'tasks/task.md';
|
|
903
|
-
fs.mkdirSync(path.join(feature, 'tasks'), { recursive: true });
|
|
904
|
-
fs.writeFileSync(path.join(feature, 'spec.json'), JSON.stringify({
|
|
905
|
-
status: 'in_progress',
|
|
906
|
-
current_phase: 'closeout',
|
|
907
|
-
feature_name: 'auth',
|
|
908
|
-
task_registry: { [taskPath]: { status: 'done', completed_at: '2026-08-11T00:00:00.000Z' } },
|
|
909
|
-
}));
|
|
910
|
-
fs.writeFileSync(path.join(feature, taskPath), 'Status: done\n');
|
|
910
|
+
writeProcessTask(path.join(root, 'specs', 'auth'), 'done');
|
|
911
911
|
fs.writeFileSync(path.join(root, '.codex', 'runtime.json'), JSON.stringify({
|
|
912
912
|
spec: { completion_gate: false },
|
|
913
913
|
}));
|
|
@@ -929,40 +929,6 @@ test('empty or forged Codex hook authority fails closed', () => {
|
|
|
929
929
|
});
|
|
930
930
|
});
|
|
931
931
|
|
|
932
|
-
test('explicit Strict Codex completion ignores worker-writable proof strings and remains blocked', () => {
|
|
933
|
-
inHookFixture((root, hooks) => {
|
|
934
|
-
const feature = path.join(root, 'specs', 'auth');
|
|
935
|
-
const taskPath = 'tasks/task.md';
|
|
936
|
-
fs.mkdirSync(path.join(feature, 'tasks'), { recursive: true });
|
|
937
|
-
fs.writeFileSync(path.join(feature, 'spec.json'), JSON.stringify({
|
|
938
|
-
status: 'in_progress',
|
|
939
|
-
current_phase: 'closeout',
|
|
940
|
-
feature_name: 'auth',
|
|
941
|
-
workflow_policy: POLICY.workflowPolicySnapshot({ riskSignals: { auth: true }, assurance_level: 'Strict' }),
|
|
942
|
-
proofs: {
|
|
943
|
-
needsInspection: 'inspection completed',
|
|
944
|
-
needsIndependentAudit: 'PASS',
|
|
945
|
-
needsResearchGrounding: 'research completed',
|
|
946
|
-
},
|
|
947
|
-
task_registry: { [taskPath]: { status: 'done', completed_at: '2026-08-11T00:00:00.000Z' } },
|
|
948
|
-
}));
|
|
949
|
-
fs.writeFileSync(path.join(feature, taskPath), bindReceipt(root, [
|
|
950
|
-
'Status: done', '', '## Evidence', '', 'Verification: PASS',
|
|
951
|
-
'Command: node --test', 'Exit: 0', `Base: ${VALID_BASE}`, `Head: ${VALID_HEAD}`,
|
|
952
|
-
].join('\n'), 'auth'));
|
|
953
|
-
const result = runHook(path.join(hooks, 'spec-gate.cjs'), root, {
|
|
954
|
-
cwd: root,
|
|
955
|
-
session_id: 'session-a',
|
|
956
|
-
hook_event_name: 'Stop',
|
|
957
|
-
stop_hook_active: false,
|
|
958
|
-
});
|
|
959
|
-
assert.equal(result.status, 0, result.stderr);
|
|
960
|
-
const block = JSON.parse(result.stdout);
|
|
961
|
-
assert.equal(block.decision, 'block');
|
|
962
|
-
assert.match(block.reason, /needsInspection|needsIndependentAudit|needsResearchGrounding/);
|
|
963
|
-
});
|
|
964
|
-
});
|
|
965
|
-
|
|
966
932
|
test('Codex state hook persists direct native event fields at project root', () => {
|
|
967
933
|
inHookFixture((root, hooks) => {
|
|
968
934
|
const nested = path.join(root, 'packages', 'app');
|
|
@@ -1036,24 +1002,6 @@ test('Codex state hook persists direct native event fields at project root', ()
|
|
|
1036
1002
|
});
|
|
1037
1003
|
});
|
|
1038
1004
|
|
|
1039
|
-
test('semantic review authority hook is registered only for SubagentStop on Claude and Codex', () => {
|
|
1040
|
-
const configurations = [
|
|
1041
|
-
JSON.parse(fs.readFileSync(path.join(PACKAGE_ROOT, 'src/claude/settings/settings.json'), 'utf8')).hooks,
|
|
1042
|
-
JSON.parse(fs.readFileSync(path.join(PACKAGE_ROOT, 'src/codex/hooks.json'), 'utf8')).hooks,
|
|
1043
|
-
];
|
|
1044
|
-
for (const hooks of configurations) {
|
|
1045
|
-
const registrations = [];
|
|
1046
|
-
for (const [event, groups] of Object.entries(hooks)) {
|
|
1047
|
-
for (const group of groups) {
|
|
1048
|
-
for (const hook of group.hooks || []) {
|
|
1049
|
-
if (String(hook.command || '').includes('semantic-review-authority.cjs')) registrations.push(event);
|
|
1050
|
-
}
|
|
1051
|
-
}
|
|
1052
|
-
}
|
|
1053
|
-
assert.deepEqual(registrations, ['SubagentStop']);
|
|
1054
|
-
}
|
|
1055
|
-
});
|
|
1056
|
-
|
|
1057
1005
|
test('Codex canonical receipt provenance requires both Base and Head', () => {
|
|
1058
1006
|
const receipt = require(path.join(CODEX_HOOKS, 'lib', 'spec-receipt.cjs'));
|
|
1059
1007
|
const onlyBase = receipt.validateCanonicalReceipt('Verification: PASS\nCommand: pnpm test\nExit: 0\nBase: abc\n');
|
|
@@ -1089,61 +1037,55 @@ test('Codex canonical receipt provenance requires both Base and Head', () => {
|
|
|
1089
1037
|
// Integration via spec-gate hook: only Base blocks, both passes
|
|
1090
1038
|
inHookFixture((root, hooks) => {
|
|
1091
1039
|
const feature = path.join(root, 'specs', 'auth');
|
|
1092
|
-
const taskPath = 'tasks/task-R0-01-auth.md';
|
|
1093
|
-
fs.mkdirSync(path.join(feature, 'tasks'), { recursive: true });
|
|
1094
1040
|
const gate = path.join(hooks, 'spec-gate.cjs');
|
|
1095
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 });
|
|
1096
1044
|
|
|
1097
1045
|
// only Base -> block
|
|
1098
|
-
|
|
1099
|
-
fs.writeFileSync(path.join(feature, taskPath), 'Status: done\n\n## Evidence\n\nVerification: PASS\nCommand: pnpm test\nExit: 0\nBase: abc\n');
|
|
1100
|
-
fs.mkdirSync(payload.cwd, { recursive: true });
|
|
1046
|
+
writeProcessTask(feature, 'done', ['Verification: PASS', 'Command: pnpm test', 'Exit: 0', 'Base: abc', ...output]);
|
|
1101
1047
|
const blocked = runHook(gate, payload.cwd, payload);
|
|
1102
1048
|
assert.equal(JSON.parse(blocked.stdout).decision, 'block');
|
|
1103
1049
|
assert.match(JSON.parse(blocked.stdout).reason, /\bprovenance\b/);
|
|
1104
1050
|
|
|
1105
1051
|
// both -> no block
|
|
1106
|
-
|
|
1052
|
+
writeProcessTask(feature, 'done', bindReceipt(root, ['Verification: PASS', 'Command: pnpm test', 'Exit: 0', `Base: ${VALID_BASE}`, `Head: ${VALID_HEAD}`, ...output].join('\n')).split('\n'));
|
|
1107
1053
|
const passed = runHook(gate, payload.cwd, payload);
|
|
1108
1054
|
assert.equal(passed.stdout, '');
|
|
1109
1055
|
|
|
1110
1056
|
// empty Base: should block provenance — reset gate cache so re-checked as newly-done
|
|
1111
1057
|
try { fs.rmSync(path.join(root, '.codex', 'hooks', '.logs', 'spec-gate-last.json'), { force: true }); } catch {}
|
|
1112
|
-
|
|
1058
|
+
writeProcessTask(feature, 'done', ['Verification: PASS', 'Command: pnpm test', 'Exit: 0', 'Base:', 'Head: def', ...output]);
|
|
1113
1059
|
const blockedEmpty = runHook(gate, payload.cwd, payload);
|
|
1114
1060
|
assert.equal(JSON.parse(blockedEmpty.stdout).decision, 'block');
|
|
1115
1061
|
assert.match(JSON.parse(blockedEmpty.stdout).reason, /\bprovenance\b/);
|
|
1116
1062
|
|
|
1117
1063
|
// bare base_sha without colon should block — reset cache again
|
|
1118
1064
|
try { fs.rmSync(path.join(root, '.codex', 'hooks', '.logs', 'spec-gate-last.json'), { force: true }); } catch {}
|
|
1119
|
-
|
|
1065
|
+
writeProcessTask(feature, 'done', ['Verification: PASS', 'Command: pnpm test', 'Exit: 0', 'base_sha head_sha', ...output]);
|
|
1120
1066
|
const blockedBare = runHook(gate, payload.cwd, payload);
|
|
1121
1067
|
assert.equal(JSON.parse(blockedBare.stdout).decision, 'block');
|
|
1122
1068
|
assert.match(JSON.parse(blockedBare.stdout).reason, /\bprovenance\b/);
|
|
1123
1069
|
});
|
|
1124
1070
|
});
|
|
1125
1071
|
|
|
1126
|
-
test('Codex cache hardening: every done re-validated — mutation,
|
|
1072
|
+
test('Codex cache hardening: every done re-validated — mutation, malformed cache, unchanged valid', () => {
|
|
1127
1073
|
inHookFixture((root, hooks) => {
|
|
1128
1074
|
const feature = path.join(root, 'specs', 'auth');
|
|
1129
|
-
const taskPath = '
|
|
1130
|
-
fs.mkdirSync(path.join(feature, 'tasks'), { recursive: true });
|
|
1075
|
+
const taskPath = 'task-01-auth.md';
|
|
1131
1076
|
const gate = path.join(hooks, 'spec-gate.cjs');
|
|
1132
1077
|
const appCwd = path.join(root, 'packages', 'app');
|
|
1133
1078
|
fs.mkdirSync(appCwd, { recursive: true });
|
|
1134
1079
|
const payload = { cwd: appCwd, session_id: 'session-a', hook_event_name: 'Stop', stop_hook_active: false };
|
|
1135
1080
|
const cacheFile = path.join(root, '.codex', 'hooks', '.logs', 'spec-gate-last.json');
|
|
1136
|
-
const
|
|
1137
|
-
|
|
1138
|
-
'Command: pnpm test', 'Exit: 0', `Base: ${VALID_BASE}`, `Head: ${VALID_HEAD}`,
|
|
1139
|
-
|
|
1140
|
-
].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'));
|
|
1141
1085
|
|
|
1142
1086
|
// first-run (no cache) with valid receipt → no block, cache seeded
|
|
1143
1087
|
try { fs.rmSync(cacheFile, { force: true }); } catch {}
|
|
1144
|
-
|
|
1145
|
-
const validBody = bindReceipt(root, validBodyTemplate);
|
|
1146
|
-
fs.writeFileSync(path.join(feature, taskPath), validBody);
|
|
1088
|
+
writeValid();
|
|
1147
1089
|
let res = runHook(gate, appCwd, payload);
|
|
1148
1090
|
assert.equal(res.stdout, '', 'first-run valid should not block');
|
|
1149
1091
|
assert.ok(fs.existsSync(cacheFile), 'cache file must be created on first valid run');
|
|
@@ -1155,7 +1097,7 @@ test('Codex cache hardening: every done re-validated — mutation, deletion, mal
|
|
|
1155
1097
|
assert.equal(res.stdout, '', 'cache-hit unchanged valid must not block');
|
|
1156
1098
|
|
|
1157
1099
|
// mutation: remove Command/Verification → must block even though cache says done
|
|
1158
|
-
|
|
1100
|
+
writeProcessTask(feature, 'done', ['Exit: 0', 'Base: abc123', 'Head: def456', ...output]);
|
|
1159
1101
|
res = runHook(gate, appCwd, payload);
|
|
1160
1102
|
assert.equal(JSON.parse(res.stdout).decision, 'block', 'mutated receipt missing Verification/Command should block on cache-hit');
|
|
1161
1103
|
assert.match(JSON.parse(res.stdout).reason, /\bverification_state\b|\bcommand\b/);
|
|
@@ -1164,33 +1106,21 @@ test('Codex cache hardening: every done re-validated — mutation, deletion, mal
|
|
|
1164
1106
|
assert.equal(JSON.parse(res.stdout).decision, 'block', 'second hit after mutation still blocks');
|
|
1165
1107
|
|
|
1166
1108
|
// restore valid, then mutate provenance (remove Head) → block
|
|
1167
|
-
|
|
1109
|
+
writeValid();
|
|
1168
1110
|
assert.equal(runHook(gate, appCwd, payload).stdout, '', 'restored valid should pass again');
|
|
1169
|
-
|
|
1111
|
+
writeProcessTask(feature, 'done', ['Verification: PASS', 'Command: pnpm test', 'Exit: 0', 'Base: abc123', ...output]);
|
|
1170
1112
|
res = runHook(gate, appCwd, payload);
|
|
1171
1113
|
assert.equal(JSON.parse(res.stdout).decision, 'block');
|
|
1172
1114
|
assert.match(JSON.parse(res.stdout).reason, /\bprovenance\b/);
|
|
1173
1115
|
|
|
1174
|
-
// deletion → block with check a
|
|
1175
|
-
fs.writeFileSync(path.join(feature, taskPath), validBody);
|
|
1176
|
-
assert.equal(runHook(gate, appCwd, payload).stdout, '');
|
|
1177
|
-
fs.unlinkSync(path.join(feature, taskPath));
|
|
1178
|
-
res = runHook(gate, appCwd, payload);
|
|
1179
|
-
assert.equal(JSON.parse(res.stdout).decision, 'block');
|
|
1180
|
-
assert.match(JSON.parse(res.stdout).reason, /\ba\b/);
|
|
1181
|
-
// restore for next sub-test
|
|
1182
|
-
fs.writeFileSync(path.join(feature, taskPath), validBody);
|
|
1183
|
-
try { fs.rmSync(cacheFile, { force: true }); } catch {}
|
|
1184
|
-
assert.equal(runHook(gate, appCwd, payload).stdout, '');
|
|
1185
|
-
|
|
1186
1116
|
// malformed cache with valid receipt → must still pass (cache parse fail-open)
|
|
1117
|
+
writeValid();
|
|
1187
1118
|
fs.writeFileSync(cacheFile, '{ malformed');
|
|
1188
|
-
fs.writeFileSync(path.join(feature, taskPath), validBody);
|
|
1189
1119
|
res = runHook(gate, appCwd, payload);
|
|
1190
1120
|
assert.equal(res.stdout, '', 'malformed cache with valid receipt must not block');
|
|
1191
1121
|
// malformed cache with invalid receipt → must still block
|
|
1192
1122
|
fs.writeFileSync(cacheFile, '{ malformed again');
|
|
1193
|
-
|
|
1123
|
+
writeProcessTask(feature, 'done');
|
|
1194
1124
|
res = runHook(gate, appCwd, payload);
|
|
1195
1125
|
assert.equal(JSON.parse(res.stdout).decision, 'block', 'malformed cache with invalid receipt must block');
|
|
1196
1126
|
});
|
|
@@ -1200,18 +1130,11 @@ test('Codex cache identity separates same-feature receipts from different roots'
|
|
|
1200
1130
|
const contexts = [];
|
|
1201
1131
|
for (let index = 0; index < 2; index += 1) {
|
|
1202
1132
|
const context = inHookFixture((root, hooks) => {
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
feature_name: 'auth',
|
|
1209
|
-
task_registry: { [taskPath]: { status: 'done', completed_at: '2026-08-11T00:00:00.000Z' } },
|
|
1210
|
-
}));
|
|
1211
|
-
fs.writeFileSync(path.join(feature, taskPath), bindReceipt(root, [
|
|
1212
|
-
'Status: done', '', '## Evidence', '', 'Verification: PASS',
|
|
1213
|
-
'Command: node --test', 'Exit: 0', `Base: ${VALID_BASE}`, `Head: ${VALID_HEAD}`,
|
|
1214
|
-
].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');
|
|
1215
1138
|
const result = runHook(path.join(hooks, 'spec-gate.cjs'), root, {
|
|
1216
1139
|
cwd: root,
|
|
1217
1140
|
session_id: 'session-a',
|
|
@@ -1248,33 +1171,32 @@ test('Codex P0 regression: placeholder, explicit failure, artifact, symlink and
|
|
|
1248
1171
|
assert.ok(specReceipt.validateCanonicalReceipt('Verification: PASS\nCommand: pnpm test\nExit: 0\nBase: a\nHead: b\nArtifact: x\nsha256: TBD\n').includes('artifact_hash'));
|
|
1249
1172
|
assert.ok(specReceipt.validateCanonicalReceipt('Verification: PASS\nCommand: pnpm test\nExit: 0\nBase: a\nHead: b\nArtifact: x\nsha256: \n').includes('artifact_hash'));
|
|
1250
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`), []);
|
|
1251
|
-
// Probe D/E: symlink
|
|
1174
|
+
// Probe D/E: symlink packet and task
|
|
1252
1175
|
const tmp = fs2.mkdtempSync(path2.join(os2.tmpdir(), 'cafekit-codex-reg-'));
|
|
1253
1176
|
try {
|
|
1254
1177
|
const specsDir = path2.join(tmp, 'specs');
|
|
1255
|
-
|
|
1256
|
-
fs2.mkdirSync(path2.join(specsDir, 'valid'), { recursive: true });
|
|
1257
|
-
fs2.writeFileSync(path2.join(specsDir, 'valid', 'spec.json'), JSON.stringify({ status: 'in_progress' }));
|
|
1178
|
+
writeProcessTask(path2.join(specsDir, 'valid'), 'pending');
|
|
1258
1179
|
const outside = path2.join(tmp, 'outside');
|
|
1259
|
-
|
|
1260
|
-
fs2.
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
// 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
|
|
1266
1186
|
const featDir = path2.join(specsDir, 'valid');
|
|
1267
|
-
fs2.mkdirSync(path2.join(featDir, 'tasks'), { recursive: true });
|
|
1268
1187
|
const outsideTask = path2.join(tmp, 'outside-task.md');
|
|
1269
|
-
fs2.
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
assert.
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
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);
|
|
1278
1200
|
assert.equal(mal.error, 'invalid_specs');
|
|
1279
1201
|
} finally {
|
|
1280
1202
|
fs2.rmSync(tmp, { recursive: true, force: true });
|
|
@@ -1325,17 +1247,10 @@ test('Codex adapter parity - phantom vectors via shared validator (r3)', () => {
|
|
|
1325
1247
|
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'cafekit-codex-tap-heading-'));
|
|
1326
1248
|
try {
|
|
1327
1249
|
const featureDir = path.join(tmp, 'feature');
|
|
1328
|
-
const
|
|
1329
|
-
|
|
1330
|
-
const
|
|
1331
|
-
|
|
1332
|
-
const tapFailure = receipt.checkReceipt(featureDir, taskPath, { status: 'done', completed_at: '2026-08-11T00:00:00.000Z' });
|
|
1333
|
-
assert.ok(tapFailure.includes('c'), 'unindented TAP # fail 1 must reach the shared validator');
|
|
1334
|
-
|
|
1335
|
-
fs.writeFileSync(path.join(featureDir, taskPath), [...taskPrefix, '# Notes', 'FAILED tests/test_demo.py', ''].join('\n'));
|
|
1336
|
-
const markdownBody = receipt.evidenceBody(fs.readFileSync(path.join(featureDir, taskPath), 'utf8'));
|
|
1337
|
-
assert.doesNotMatch(markdownBody, /FAILED tests\/test_demo\.py/);
|
|
1338
|
-
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}`);
|
|
1339
1254
|
} finally {
|
|
1340
1255
|
fs.rmSync(tmp, { recursive: true, force: true });
|
|
1341
1256
|
}
|
|
@@ -1365,38 +1280,20 @@ test('Codex adapter enforces declared task artifact SHA-256 with missing, invali
|
|
|
1365
1280
|
const digest = crypto.createHash('sha256').update(artifact).digest('hex');
|
|
1366
1281
|
const cases = [
|
|
1367
1282
|
['', true],
|
|
1368
|
-
['sha256: abc123
|
|
1369
|
-
[`sha256: ${'0'.repeat(64)}
|
|
1370
|
-
[`sha256: ${digest}
|
|
1283
|
+
['sha256: abc123', true],
|
|
1284
|
+
[`sha256: ${'0'.repeat(64)}`, true],
|
|
1285
|
+
[`sha256: ${digest}`, false],
|
|
1371
1286
|
];
|
|
1372
1287
|
for (const [hashLine, shouldBlock] of cases) {
|
|
1373
1288
|
inHookFixture((root, hooks) => {
|
|
1374
1289
|
const feature = path.join(root, 'specs', 'artifact-demo');
|
|
1375
|
-
const taskPath = 'tasks/task.md';
|
|
1376
|
-
fs.mkdirSync(path.join(feature, 'tasks'), { recursive: true });
|
|
1377
1290
|
fs.mkdirSync(path.join(root, 'output'), { recursive: true });
|
|
1378
1291
|
fs.writeFileSync(path.join(root, 'output', 'bundle.js'), artifact);
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
current_phase: 'closeout',
|
|
1382
|
-
feature_name: 'artifact-demo',
|
|
1383
|
-
task_registry: {
|
|
1384
|
-
[taskPath]: {
|
|
1385
|
-
status: 'done',
|
|
1386
|
-
completed_at: '2026-08-11T00:00:00.000Z',
|
|
1387
|
-
artifacts: ['output/bundle.js'],
|
|
1388
|
-
},
|
|
1389
|
-
},
|
|
1390
|
-
}));
|
|
1391
|
-
fs.writeFileSync(path.join(feature, taskPath), [
|
|
1392
|
-
'# Task', '', '**Status:** done', '', '## Evidence', '',
|
|
1292
|
+
writeProcessTask(feature, 'done', null, 'node --test');
|
|
1293
|
+
writeProcessTask(feature, 'done', bindReceipt(root, [
|
|
1393
1294
|
'Verification: PASS', 'Command: node --test', 'Exit: 0', `Base: ${VALID_BASE}`, `Head: ${VALID_HEAD}`,
|
|
1394
|
-
'Artifact: output/bundle.js', hashLine,
|
|
1395
|
-
].join('\n'));
|
|
1396
|
-
fs.writeFileSync(
|
|
1397
|
-
path.join(feature, taskPath),
|
|
1398
|
-
bindReceipt(root, fs.readFileSync(path.join(feature, taskPath), 'utf8'), 'artifact-demo'),
|
|
1399
|
-
);
|
|
1295
|
+
'Artifact: output/bundle.js', hashLine, '```', 'node --test', 'ok 1', '```',
|
|
1296
|
+
].join('\n'), 'artifact-demo').split('\n'), 'node --test');
|
|
1400
1297
|
const result = runHook(path.join(hooks, 'spec-gate.cjs'), root, {
|
|
1401
1298
|
cwd: root,
|
|
1402
1299
|
session_id: 'session-a',
|