@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
|
@@ -20,7 +20,7 @@ const CACHE = path.join(hookStateDir(), 'spec-gate-last.json');
|
|
|
20
20
|
const PROVENANCE = require(path.join(__dirname, '..', '..', 'scripts', 'provenance.cjs'));
|
|
21
21
|
const POLICY = require(path.join(__dirname, '..', '..', 'scripts', 'workflow-policy.cjs'));
|
|
22
22
|
const FEATURE = 'demo';
|
|
23
|
-
const TASK_REL = '
|
|
23
|
+
const TASK_REL = 'task-01-x.md';
|
|
24
24
|
const VALID_BASE = '0123456789abcdef0123456789abcdef01234567';
|
|
25
25
|
const VALID_HEAD = '89abcdef0123456789abcdef0123456789abcdef';
|
|
26
26
|
const FIXTURE_ARTIFACT = Buffer.from('cafekit claude fixture artifact\n');
|
|
@@ -29,11 +29,6 @@ const FIXTURE_ARTIFACT_DIGEST = crypto.createHash('sha256').update(FIXTURE_ARTIF
|
|
|
29
29
|
function runHook(payload, cwd) {
|
|
30
30
|
const projectRoot = cwd || payload.cwd;
|
|
31
31
|
const specsRoot = path.join(projectRoot, 'specs');
|
|
32
|
-
if (fs.existsSync(specsRoot)) {
|
|
33
|
-
for (const featureName of fs.readdirSync(specsRoot)) {
|
|
34
|
-
installFeatureReceipt(projectRoot, featureName, payload.session_id || 'test');
|
|
35
|
-
}
|
|
36
|
-
}
|
|
37
32
|
// PROJECT_ROOT wins over cwd in the hook; pin it to the fixture so a host
|
|
38
33
|
// monorepo's real specs/ cannot leak into the test (e.g. active rtk-* specs).
|
|
39
34
|
const env = { ...process.env, PROJECT_ROOT: projectRoot };
|
|
@@ -69,7 +64,7 @@ function runtimeContext(dir) {
|
|
|
69
64
|
return PROVENANCE.deriveRuntimeContext({
|
|
70
65
|
projectRoot: dir,
|
|
71
66
|
specsRoot: path.join(dir, 'specs'),
|
|
72
|
-
specFile: path.join(dir, 'specs', FEATURE, '
|
|
67
|
+
specFile: path.join(dir, 'specs', FEATURE, 'plan.md'),
|
|
73
68
|
featureName: FEATURE,
|
|
74
69
|
runtimeSession: 'test',
|
|
75
70
|
});
|
|
@@ -118,38 +113,6 @@ function makeWorkflowFixture(receiptLines = [], plannedCommand = 'node --test')
|
|
|
118
113
|
return dir;
|
|
119
114
|
}
|
|
120
115
|
|
|
121
|
-
function installFeatureReceipt(dir, featureName = FEATURE, session = 'test') {
|
|
122
|
-
const featureDir = path.join(dir, 'specs', featureName);
|
|
123
|
-
const specFile = path.join(featureDir, 'spec.json');
|
|
124
|
-
if (!fs.existsSync(specFile)) return;
|
|
125
|
-
let spec;
|
|
126
|
-
try { spec = JSON.parse(fs.readFileSync(specFile, 'utf8')); } catch { return; }
|
|
127
|
-
const lifecyclePhase = spec.current_phase || spec.phase;
|
|
128
|
-
const explicitCloseout = ['completed', 'complete'].includes(spec.status)
|
|
129
|
-
|| ['closeout', 'completion', 'completed', 'complete'].includes(lifecyclePhase);
|
|
130
|
-
if (!explicitCloseout) return;
|
|
131
|
-
const tasks = Object.values(spec.task_registry || {});
|
|
132
|
-
if (tasks.length === 0 || tasks.some((task) => task.status !== 'done')) return;
|
|
133
|
-
const context = PROVENANCE.deriveRuntimeContext({
|
|
134
|
-
projectRoot: dir,
|
|
135
|
-
specsRoot: path.join(dir, 'specs'),
|
|
136
|
-
specFile,
|
|
137
|
-
featureName,
|
|
138
|
-
runtimeSession: session,
|
|
139
|
-
});
|
|
140
|
-
fs.writeFileSync(path.join(featureDir, 'feature-receipt.md'), [
|
|
141
|
-
`Feature: ${featureName}`,
|
|
142
|
-
'Expected: final integration verification passes',
|
|
143
|
-
'Observed: final integration verification passed',
|
|
144
|
-
'Verification: PASS',
|
|
145
|
-
'Command: node --test',
|
|
146
|
-
'Exit: 0',
|
|
147
|
-
`Base: ${context.base}`,
|
|
148
|
-
`Head: ${context.head}`,
|
|
149
|
-
'',
|
|
150
|
-
].join('\n'));
|
|
151
|
-
}
|
|
152
|
-
|
|
153
116
|
function clearCache() {
|
|
154
117
|
try { fs.unlinkSync(CACHE); } catch { /* absent */ }
|
|
155
118
|
}
|
|
@@ -164,52 +127,32 @@ function readCache() {
|
|
|
164
127
|
}
|
|
165
128
|
|
|
166
129
|
/**
|
|
167
|
-
* Build a minimal
|
|
130
|
+
* Build a minimal process-first packet under dir: plan.md plus one flat task
|
|
131
|
+
* whose Verification Plan runs `npm test` and whose inline Receipt varies.
|
|
168
132
|
* @param {object} opts
|
|
169
133
|
* @param {string} [opts.taskStatus='done']
|
|
170
|
-
* @param {
|
|
171
|
-
* @param {string} [opts.mdStatus='done']
|
|
172
|
-
* @param {'valid'|'legacy-valid'|'failed'|'fence-only'|'missing-evidence'|'placeholder'|'none'} [opts.evidence='valid']
|
|
134
|
+
* @param {'valid'|'failed'|'fence-only'|'missing-evidence'|'placeholder'|'none'} [opts.evidence='valid']
|
|
173
135
|
* @param {object|null} [opts.runtime] — if set, write .claude/runtime.json
|
|
174
|
-
* @param {object|null} [opts.workflowPolicy] — if set, persist completion obligations
|
|
175
|
-
* @param {string} [opts.phase='closeout'] — persisted lifecycle boundary
|
|
176
136
|
*/
|
|
177
137
|
function makeFixture(opts = {}) {
|
|
178
138
|
const {
|
|
179
139
|
taskStatus = 'done',
|
|
180
|
-
completed_at = '2026-07-01T00:00:00Z',
|
|
181
|
-
mdStatus = 'done',
|
|
182
140
|
evidence = 'valid',
|
|
183
141
|
runtime = null,
|
|
184
|
-
workflowPolicy = null,
|
|
185
|
-
phase = 'closeout',
|
|
186
142
|
} = opts;
|
|
187
143
|
const dir = tmpDir();
|
|
188
144
|
const featureDir = path.join(dir, 'specs', FEATURE);
|
|
189
|
-
|
|
190
|
-
fs.
|
|
191
|
-
|
|
192
|
-
const entry = { status: taskStatus };
|
|
193
|
-
if (completed_at !== null) entry.completed_at = completed_at;
|
|
194
|
-
|
|
195
|
-
const spec = {
|
|
196
|
-
status: 'in_progress',
|
|
197
|
-
feature_name: FEATURE,
|
|
198
|
-
current_phase: phase,
|
|
199
|
-
task_registry: { [TASK_REL]: entry },
|
|
200
|
-
};
|
|
201
|
-
if (workflowPolicy) spec.workflow_policy = workflowPolicy;
|
|
202
|
-
fs.writeFileSync(path.join(featureDir, 'spec.json'), JSON.stringify(spec));
|
|
145
|
+
fs.mkdirSync(featureDir, { recursive: true });
|
|
146
|
+
fs.writeFileSync(path.join(featureDir, 'plan.md'), '# Demo plan\nSpecs-Contract: process-first-ready-v1\n');
|
|
203
147
|
|
|
204
|
-
let
|
|
148
|
+
let receiptBlock = '';
|
|
205
149
|
if (evidence === 'valid') {
|
|
206
|
-
|
|
207
|
-
'##
|
|
150
|
+
receiptBlock = [
|
|
151
|
+
'## Receipt',
|
|
208
152
|
'',
|
|
209
153
|
'Verification: PASS',
|
|
210
154
|
'Command: npm test',
|
|
211
155
|
'Exit: 0',
|
|
212
|
-
'Result: PASS',
|
|
213
156
|
`Base: ${VALID_BASE}`,
|
|
214
157
|
`Head: ${VALID_HEAD}`,
|
|
215
158
|
`Artifact: output/bundle.js (sha256:${FIXTURE_ARTIFACT_DIGEST})`,
|
|
@@ -219,34 +162,38 @@ function makeFixture(opts = {}) {
|
|
|
219
162
|
'```',
|
|
220
163
|
'',
|
|
221
164
|
].join('\n');
|
|
222
|
-
} else if (evidence === 'legacy-valid') {
|
|
223
|
-
evidenceBlock = `## Evidence\n\nVerification: PASS\nCommand: npm test\nExit: 0\nBase: ${VALID_BASE}\nHead: ${VALID_HEAD}\nnpm test — passed, exit code 0\n`;
|
|
224
165
|
} else if (evidence === 'failed') {
|
|
225
|
-
|
|
166
|
+
receiptBlock = '## Receipt\n\nVerification: PASS\n\nFAIL: tests failed, exit code 1\n';
|
|
226
167
|
} else if (evidence === 'fence-only') {
|
|
227
|
-
|
|
168
|
+
receiptBlock = '## Receipt\n\n```\nnpm test\n```\n';
|
|
228
169
|
} else if (evidence === 'placeholder') {
|
|
229
|
-
|
|
230
|
-
'##
|
|
170
|
+
receiptBlock = [
|
|
171
|
+
'## Receipt',
|
|
231
172
|
'',
|
|
232
173
|
'Command: `{{TYPECHECK / TEST COMMAND}}`',
|
|
233
174
|
'Expected: {{What proves success}}',
|
|
234
175
|
'',
|
|
235
176
|
].join('\n');
|
|
236
177
|
} else if (evidence === 'missing-evidence') {
|
|
237
|
-
|
|
238
|
-
} else {
|
|
239
|
-
evidenceBlock = '';
|
|
178
|
+
receiptBlock = '## Risk Assessment\n\nNone.\n';
|
|
240
179
|
}
|
|
241
180
|
|
|
242
181
|
fs.writeFileSync(
|
|
243
|
-
path.join(
|
|
182
|
+
path.join(featureDir, TASK_REL),
|
|
244
183
|
[
|
|
245
|
-
|
|
184
|
+
'# Task 01: example',
|
|
185
|
+
'',
|
|
186
|
+
`Status: ${taskStatus}`,
|
|
187
|
+
'',
|
|
188
|
+
'## Dependencies',
|
|
189
|
+
'',
|
|
190
|
+
'- none',
|
|
246
191
|
'',
|
|
247
|
-
|
|
192
|
+
'## Verification Plan',
|
|
248
193
|
'',
|
|
249
|
-
|
|
194
|
+
'- Command: npm test',
|
|
195
|
+
'',
|
|
196
|
+
receiptBlock,
|
|
250
197
|
].join('\n'),
|
|
251
198
|
);
|
|
252
199
|
|
|
@@ -261,9 +208,7 @@ function makeFixture(opts = {}) {
|
|
|
261
208
|
if (evidence === 'valid') {
|
|
262
209
|
fs.mkdirSync(path.join(dir, 'output'), { recursive: true });
|
|
263
210
|
fs.writeFileSync(path.join(dir, 'output', 'bundle.js'), FIXTURE_ARTIFACT);
|
|
264
|
-
|
|
265
|
-
if (evidence === 'valid' || evidence === 'legacy-valid') {
|
|
266
|
-
const taskFile = path.join(dir, 'specs', FEATURE, TASK_REL);
|
|
211
|
+
const taskFile = path.join(featureDir, TASK_REL);
|
|
267
212
|
fs.writeFileSync(taskFile, bindFixtureReceipt(dir, fs.readFileSync(taskFile, 'utf8')));
|
|
268
213
|
}
|
|
269
214
|
|
|
@@ -295,10 +240,7 @@ function installClaudeGate(root, policyMode = 'valid') {
|
|
|
295
240
|
path.join(__dirname, '..', 'lib', 'hook-state-dir.cjs'),
|
|
296
241
|
path.join(hooks, 'lib', 'hook-state-dir.cjs'),
|
|
297
242
|
);
|
|
298
|
-
for (const name of [
|
|
299
|
-
'completion-authority-check.cjs', 'completion-authority-state.cjs', 'semantic-review-authority.cjs',
|
|
300
|
-
]) fs.copyFileSync(path.join(__dirname, '..', name), path.join(hooks, name));
|
|
301
|
-
for (const name of ['spec-resolver.cjs', 'spec-receipt.cjs', 'validate-spec-output.cjs', 'spec-ground.cjs', 'spec-semantic-model.cjs', 'spec-final-state.cjs']) {
|
|
243
|
+
for (const name of ['spec-resolver.cjs', 'spec-receipt.cjs']) {
|
|
302
244
|
fs.copyFileSync(path.join(__dirname, '..', '..', 'scripts', name), path.join(scripts, name));
|
|
303
245
|
}
|
|
304
246
|
if (policyMode === 'valid') {
|
|
@@ -404,31 +346,21 @@ test('Claude adapter requires canonical SHA-256 only for a declared task artifac
|
|
|
404
346
|
const dir = tmpDir();
|
|
405
347
|
const gate = installClaudeGate(dir);
|
|
406
348
|
const featureDir = path.join(dir, 'specs', FEATURE);
|
|
349
|
+
fs.mkdirSync(featureDir, { recursive: true });
|
|
350
|
+
fs.writeFileSync(path.join(featureDir, 'plan.md'), '# Demo plan\n');
|
|
407
351
|
fs.mkdirSync(path.join(featureDir, 'tasks'), { recursive: true });
|
|
408
352
|
fs.mkdirSync(path.join(dir, 'output'), { recursive: true });
|
|
409
353
|
fs.writeFileSync(path.join(dir, 'output', 'bundle.js'), FIXTURE_ARTIFACT);
|
|
410
|
-
fs.writeFileSync(path.join(featureDir, 'spec.json'), JSON.stringify({
|
|
411
|
-
status: 'in_progress',
|
|
412
|
-
current_phase: 'closeout',
|
|
413
|
-
feature_name: FEATURE,
|
|
414
|
-
task_registry: {
|
|
415
|
-
[TASK_REL]: {
|
|
416
|
-
status: 'done',
|
|
417
|
-
completed_at: '2026-08-11T00:00:00.000Z',
|
|
418
|
-
artifacts: ['output/bundle.js'],
|
|
419
|
-
},
|
|
420
|
-
},
|
|
421
|
-
}));
|
|
422
354
|
fs.writeFileSync(path.join(featureDir, TASK_REL), [
|
|
423
|
-
'# Task', '', '
|
|
355
|
+
'# Task', '', 'Status: done', '', '## Dependencies', '', '- none', '', '## Verification Plan', '', '- Command: node --test', '', '## Receipt', '',
|
|
424
356
|
'Verification: PASS', 'Command: node --test', 'Exit: 0', `Base: ${VALID_BASE}`, `Head: ${VALID_HEAD}`,
|
|
425
357
|
'Artifact: output/bundle.js', suffix,
|
|
358
|
+
'```', 'node --test', 'ok 1', '```',
|
|
426
359
|
].join('\n'));
|
|
427
360
|
fs.writeFileSync(
|
|
428
361
|
path.join(featureDir, TASK_REL),
|
|
429
362
|
bindFixtureReceipt(dir, fs.readFileSync(path.join(featureDir, TASK_REL), 'utf8')),
|
|
430
363
|
);
|
|
431
|
-
installFeatureReceipt(dir);
|
|
432
364
|
try {
|
|
433
365
|
const result = spawnSync(process.execPath, [gate], {
|
|
434
366
|
cwd: dir,
|
|
@@ -458,6 +390,8 @@ test('Claude artifact verification rejects traversal and symlink paths', () => {
|
|
|
458
390
|
for (const { artifactPath, symlink } of cases) {
|
|
459
391
|
const dir = tmpDir();
|
|
460
392
|
const featureDir = path.join(dir, 'specs', FEATURE);
|
|
393
|
+
fs.mkdirSync(featureDir, { recursive: true });
|
|
394
|
+
fs.writeFileSync(path.join(featureDir, 'plan.md'), '# Demo plan\n');
|
|
461
395
|
const artifactBytes = Buffer.from(`artifact-${artifactPath}\n`);
|
|
462
396
|
const digest = crypto.createHash('sha256').update(artifactBytes).digest('hex');
|
|
463
397
|
fs.mkdirSync(path.join(featureDir, 'tasks'), { recursive: true });
|
|
@@ -469,20 +403,8 @@ test('Claude artifact verification rejects traversal and symlink paths', () => {
|
|
|
469
403
|
} else {
|
|
470
404
|
fs.writeFileSync(path.join(dir, 'outside.js'), artifactBytes);
|
|
471
405
|
}
|
|
472
|
-
fs.writeFileSync(path.join(featureDir, 'spec.json'), JSON.stringify({
|
|
473
|
-
status: 'in_progress',
|
|
474
|
-
current_phase: 'closeout',
|
|
475
|
-
feature_name: FEATURE,
|
|
476
|
-
task_registry: {
|
|
477
|
-
[TASK_REL]: {
|
|
478
|
-
status: 'done',
|
|
479
|
-
completed_at: '2026-08-11T00:00:00.000Z',
|
|
480
|
-
artifacts: [artifactPath],
|
|
481
|
-
},
|
|
482
|
-
},
|
|
483
|
-
}));
|
|
484
406
|
fs.writeFileSync(path.join(featureDir, TASK_REL), bindFixtureReceipt(dir, [
|
|
485
|
-
'# Task', '', '
|
|
407
|
+
'# Task', '', 'Status: done', '', '## Dependencies', '', '- none', '', '## Verification Plan', '', '- Command: node --test', '', '## Receipt', '',
|
|
486
408
|
'Verification: PASS', 'Command: node --test', 'Exit: 0',
|
|
487
409
|
`Base: ${VALID_BASE}`, `Head: ${VALID_HEAD}`,
|
|
488
410
|
`Artifact: ${artifactPath} (sha256:${digest})`,
|
|
@@ -628,27 +550,6 @@ test('empty or malformed Claude hook payload fails closed', () => {
|
|
|
628
550
|
}
|
|
629
551
|
});
|
|
630
552
|
|
|
631
|
-
test('explicit Strict completion ignores worker-writable proof strings and remains blocked', () => {
|
|
632
|
-
const dir = makeFixture({
|
|
633
|
-
workflowPolicy: POLICY.workflowPolicySnapshot({ riskSignals: { auth: true }, assurance_level: 'Strict' }),
|
|
634
|
-
});
|
|
635
|
-
try {
|
|
636
|
-
const specPath = path.join(dir, 'specs', FEATURE, 'spec.json');
|
|
637
|
-
const spec = JSON.parse(fs.readFileSync(specPath, 'utf8'));
|
|
638
|
-
spec.proofs = {
|
|
639
|
-
needsInspection: 'inspection completed',
|
|
640
|
-
needsIndependentAudit: 'PASS',
|
|
641
|
-
needsResearchGrounding: 'research completed',
|
|
642
|
-
};
|
|
643
|
-
fs.writeFileSync(specPath, JSON.stringify(spec));
|
|
644
|
-
const body = parseBlock(runHook({}, dir).stdout);
|
|
645
|
-
assert.equal(body?.decision, 'block');
|
|
646
|
-
assert.match(body.reason, /needsInspection|needsIndependentAudit|needsResearchGrounding/);
|
|
647
|
-
} finally {
|
|
648
|
-
fs.rmSync(dir, { recursive: true, force: true });
|
|
649
|
-
}
|
|
650
|
-
});
|
|
651
|
-
|
|
652
553
|
test('7. Evidence with {{...}} placeholder → blocked (placeholder)', () => {
|
|
653
554
|
const dir = makeFixture({ evidence: 'placeholder' });
|
|
654
555
|
try {
|
|
@@ -689,70 +590,20 @@ test('9. a code fence alone is not verification proof', () => {
|
|
|
689
590
|
}
|
|
690
591
|
});
|
|
691
592
|
|
|
692
|
-
test('10. completed_at must be a valid ISO timestamp', () => {
|
|
693
|
-
const dir = makeFixture({ completed_at: 'yesterday' });
|
|
694
|
-
try {
|
|
695
|
-
seedCache({ [FEATURE]: { [TASK_REL]: 'pending' } });
|
|
696
|
-
const body = parseBlock(runHook({}, dir).stdout);
|
|
697
|
-
assert.ok(body);
|
|
698
|
-
assert.match(body.reason, /\bcompleted_at\b/);
|
|
699
|
-
} finally {
|
|
700
|
-
fs.rmSync(dir, { recursive: true, force: true });
|
|
701
|
-
}
|
|
702
|
-
});
|
|
703
|
-
|
|
704
|
-
test('11. done → pending cache transition is persisted and explicit closeout gates re-completion', () => {
|
|
705
|
-
const dir = makeFixture({ taskStatus: 'pending', mdStatus: 'pending', phase: 'implementation' });
|
|
706
|
-
try {
|
|
707
|
-
seedCache({ [FEATURE]: { [TASK_REL]: 'done' } });
|
|
708
|
-
assert.strictEqual(runHook({}, dir).stdout, '');
|
|
709
|
-
assert.strictEqual(readCache()[FEATURE][TASK_REL], 'pending');
|
|
710
|
-
|
|
711
|
-
const specFile = path.join(dir, 'specs', FEATURE, 'spec.json');
|
|
712
|
-
const spec = JSON.parse(fs.readFileSync(specFile, 'utf8'));
|
|
713
|
-
spec.task_registry[TASK_REL] = {
|
|
714
|
-
status: 'done',
|
|
715
|
-
completed_at: '2026-07-01T00:00:00Z',
|
|
716
|
-
};
|
|
717
|
-
spec.current_phase = 'closeout';
|
|
718
|
-
fs.writeFileSync(specFile, JSON.stringify(spec));
|
|
719
|
-
const taskFile = path.join(dir, 'specs', FEATURE, TASK_REL);
|
|
720
|
-
fs.writeFileSync(taskFile, '# Task\n\n**Status:** done\n\n## Evidence\n\nFAIL: tests failed\n');
|
|
721
|
-
|
|
722
|
-
assert.ok(parseBlock(runHook({}, dir).stdout), 're-completed failing task must be gated');
|
|
723
|
-
} finally {
|
|
724
|
-
fs.rmSync(dir, { recursive: true, force: true });
|
|
725
|
-
}
|
|
726
|
-
});
|
|
727
|
-
|
|
728
|
-
test('12. legacy successful receipt remains read-compatible', () => {
|
|
729
|
-
const dir = makeFixture({ evidence: 'legacy-valid' });
|
|
730
|
-
try {
|
|
731
|
-
seedCache({ [FEATURE]: { [TASK_REL]: 'pending' } });
|
|
732
|
-
assert.strictEqual(runHook({}, dir).stdout, '');
|
|
733
|
-
} finally {
|
|
734
|
-
fs.rmSync(dir, { recursive: true, force: true });
|
|
735
|
-
}
|
|
736
|
-
});
|
|
737
|
-
|
|
738
593
|
test('13. provenance requires both Base and Head - only Base fails', () => {
|
|
739
594
|
const dir = tmpDir();
|
|
740
595
|
const featureDir = path.join(dir, 'specs', FEATURE);
|
|
596
|
+
fs.mkdirSync(featureDir, { recursive: true });
|
|
597
|
+
fs.writeFileSync(path.join(featureDir, 'plan.md'), '# Demo plan\n');
|
|
741
598
|
const tasksDir = path.join(featureDir, 'tasks');
|
|
742
599
|
fs.mkdirSync(tasksDir, { recursive: true });
|
|
743
|
-
fs.writeFileSync(path.join(featureDir, 'spec.json'), JSON.stringify({
|
|
744
|
-
status: 'in_progress',
|
|
745
|
-
feature_name: FEATURE,
|
|
746
|
-
current_phase: 'closeout',
|
|
747
|
-
task_registry: { [TASK_REL]: { status: 'done', completed_at: '2026-07-01T00:00:00Z' } },
|
|
748
|
-
}));
|
|
749
600
|
// Only Base, missing Head
|
|
750
601
|
fs.writeFileSync(path.join(dir, 'specs', FEATURE, TASK_REL), bindFixtureReceipt(dir, [
|
|
751
602
|
'# Task',
|
|
752
603
|
'',
|
|
753
|
-
'
|
|
604
|
+
'Status: done',
|
|
754
605
|
'',
|
|
755
|
-
'##
|
|
606
|
+
'## Dependencies', '', '- none', '', '## Verification Plan', '', '- Command: npm test', '', '## Receipt',
|
|
756
607
|
'',
|
|
757
608
|
'Verification: PASS',
|
|
758
609
|
'Command: npm test',
|
|
@@ -776,21 +627,17 @@ test('13. provenance requires both Base and Head - only Base fails', () => {
|
|
|
776
627
|
test('14. provenance requires both Base and Head - only Head fails', () => {
|
|
777
628
|
const dir = tmpDir();
|
|
778
629
|
const featureDir = path.join(dir, 'specs', FEATURE);
|
|
630
|
+
fs.mkdirSync(featureDir, { recursive: true });
|
|
631
|
+
fs.writeFileSync(path.join(featureDir, 'plan.md'), '# Demo plan\n');
|
|
779
632
|
const tasksDir = path.join(featureDir, 'tasks');
|
|
780
633
|
fs.mkdirSync(tasksDir, { recursive: true });
|
|
781
|
-
fs.writeFileSync(path.join(featureDir, 'spec.json'), JSON.stringify({
|
|
782
|
-
status: 'in_progress',
|
|
783
|
-
feature_name: FEATURE,
|
|
784
|
-
current_phase: 'closeout',
|
|
785
|
-
task_registry: { [TASK_REL]: { status: 'done', completed_at: '2026-07-01T00:00:00Z' } },
|
|
786
|
-
}));
|
|
787
634
|
// Only Head, missing Base
|
|
788
635
|
fs.writeFileSync(path.join(dir, 'specs', FEATURE, TASK_REL), [
|
|
789
636
|
'# Task',
|
|
790
637
|
'',
|
|
791
|
-
'
|
|
638
|
+
'Status: done',
|
|
792
639
|
'',
|
|
793
|
-
'##
|
|
640
|
+
'## Dependencies', '', '- none', '', '## Verification Plan', '', '- Command: npm test', '', '## Receipt',
|
|
794
641
|
'',
|
|
795
642
|
'Verification: PASS',
|
|
796
643
|
'Command: npm test',
|
|
@@ -814,20 +661,16 @@ test('14. provenance requires both Base and Head - only Head fails', () => {
|
|
|
814
661
|
test('15. provenance with both Base and Head passes', () => {
|
|
815
662
|
const dir = tmpDir();
|
|
816
663
|
const featureDir = path.join(dir, 'specs', FEATURE);
|
|
664
|
+
fs.mkdirSync(featureDir, { recursive: true });
|
|
665
|
+
fs.writeFileSync(path.join(featureDir, 'plan.md'), '# Demo plan\n');
|
|
817
666
|
const tasksDir = path.join(featureDir, 'tasks');
|
|
818
667
|
fs.mkdirSync(tasksDir, { recursive: true });
|
|
819
|
-
fs.writeFileSync(path.join(featureDir, 'spec.json'), JSON.stringify({
|
|
820
|
-
status: 'in_progress',
|
|
821
|
-
feature_name: FEATURE,
|
|
822
|
-
current_phase: 'closeout',
|
|
823
|
-
task_registry: { [TASK_REL]: { status: 'done', completed_at: '2026-07-01T00:00:00Z' } },
|
|
824
|
-
}));
|
|
825
668
|
fs.writeFileSync(path.join(dir, 'specs', FEATURE, TASK_REL), bindFixtureReceipt(dir, [
|
|
826
669
|
'# Task',
|
|
827
670
|
'',
|
|
828
|
-
'
|
|
671
|
+
'Status: done',
|
|
829
672
|
'',
|
|
830
|
-
'##
|
|
673
|
+
'## Dependencies', '', '- none', '', '## Verification Plan', '', '- Command: npm test', '', '## Receipt',
|
|
831
674
|
'',
|
|
832
675
|
'Verification: PASS',
|
|
833
676
|
'Command: npm test',
|
|
@@ -847,57 +690,6 @@ test('15. provenance with both Base and Head passes', () => {
|
|
|
847
690
|
}
|
|
848
691
|
});
|
|
849
692
|
|
|
850
|
-
test('16. registry task path with ../ escape or sibling-prefix is rejected (check a)', () => {
|
|
851
|
-
const dir = tmpDir();
|
|
852
|
-
const featureDir = path.join(dir, 'specs', FEATURE);
|
|
853
|
-
const evilFeature = path.join(dir, 'specs', `${FEATURE}-evil`);
|
|
854
|
-
const maliciousRel = `../${FEATURE}-evil/tasks/task.md`;
|
|
855
|
-
const maliciousAbs = path.join(evilFeature, 'tasks/task.md');
|
|
856
|
-
// Prepare evil file with valid receipt outside feature (should be ignored due to path traversal)
|
|
857
|
-
fs.mkdirSync(path.join(featureDir, 'tasks'), { recursive: true });
|
|
858
|
-
fs.mkdirSync(path.join(evilFeature, 'tasks'), { recursive: true });
|
|
859
|
-
fs.writeFileSync(maliciousAbs, [
|
|
860
|
-
'# Task',
|
|
861
|
-
'',
|
|
862
|
-
'**Status:** done',
|
|
863
|
-
'',
|
|
864
|
-
'## Evidence',
|
|
865
|
-
'',
|
|
866
|
-
'Verification: PASS',
|
|
867
|
-
'Command: npm test',
|
|
868
|
-
'Exit: 0',
|
|
869
|
-
`Base: ${VALID_BASE}`,
|
|
870
|
-
`Head: ${VALID_HEAD}`,
|
|
871
|
-
'```',
|
|
872
|
-
'pass',
|
|
873
|
-
'```',
|
|
874
|
-
].join('\n'));
|
|
875
|
-
// Also create sibling-prefix style path: tasks/../../demo-evil/tasks/task.md
|
|
876
|
-
const siblingPrefixRel = `tasks/../../${FEATURE}-evil/tasks/task.md`;
|
|
877
|
-
fs.writeFileSync(path.join(featureDir, 'spec.json'), JSON.stringify({
|
|
878
|
-
status: 'in_progress',
|
|
879
|
-
feature_name: FEATURE,
|
|
880
|
-
current_phase: 'closeout',
|
|
881
|
-
task_registry: {
|
|
882
|
-
[maliciousRel]: { status: 'done', completed_at: '2026-07-01T00:00:00Z' },
|
|
883
|
-
[siblingPrefixRel]: { status: 'done', completed_at: '2026-07-01T00:00:00Z' },
|
|
884
|
-
[path.resolve(featureDir, maliciousRel)]: { status: 'done', completed_at: '2026-07-01T00:00:00Z' },
|
|
885
|
-
},
|
|
886
|
-
}));
|
|
887
|
-
try {
|
|
888
|
-
seedCache({ [FEATURE]: { [maliciousRel]: 'pending', [siblingPrefixRel]: 'pending', [path.resolve(featureDir, maliciousRel)]: 'pending' } });
|
|
889
|
-
const body = parseBlock(runHook({}, dir).stdout);
|
|
890
|
-
assert.ok(body, 'traversal path should block');
|
|
891
|
-
assert.strictEqual(body.decision, 'block');
|
|
892
|
-
// Must fail check a (file + Status)
|
|
893
|
-
assert.match(body.reason, /\ba\b/);
|
|
894
|
-
// Ensure malicious path is named in reason
|
|
895
|
-
assert.ok(body.reason.includes(maliciousRel) || body.reason.includes(siblingPrefixRel));
|
|
896
|
-
} finally {
|
|
897
|
-
fs.rmSync(dir, { recursive: true, force: true });
|
|
898
|
-
}
|
|
899
|
-
});
|
|
900
|
-
|
|
901
693
|
test('17. empty or bare provenance fails — Base:/Head: require non-empty same-line values', () => {
|
|
902
694
|
const cases = [
|
|
903
695
|
{ evidence: ['Verification: PASS','Command: npm test','Exit: 0','Base:','Head: def456'].join('\n'), desc: 'empty Base:' },
|
|
@@ -911,14 +703,10 @@ test('17. empty or bare provenance fails — Base:/Head: require non-empty same-
|
|
|
911
703
|
for (const { evidence, desc } of cases) {
|
|
912
704
|
const dir = tmpDir();
|
|
913
705
|
const featureDir = path.join(dir, 'specs', FEATURE);
|
|
706
|
+
fs.mkdirSync(featureDir, { recursive: true });
|
|
707
|
+
fs.writeFileSync(path.join(featureDir, 'plan.md'), '# Demo plan\n');
|
|
914
708
|
fs.mkdirSync(path.join(featureDir, 'tasks'), { recursive: true });
|
|
915
|
-
fs.writeFileSync(path.join(
|
|
916
|
-
status: 'in_progress',
|
|
917
|
-
feature_name: FEATURE,
|
|
918
|
-
current_phase: 'closeout',
|
|
919
|
-
task_registry: { [TASK_REL]: { status: 'done', completed_at: '2026-07-01T00:00:00Z' } },
|
|
920
|
-
}));
|
|
921
|
-
fs.writeFileSync(path.join(dir, 'specs', FEATURE, TASK_REL), ['# Task','','**Status:** done','','## Evidence','','' + evidence,'```','pass','```'].join('\n'));
|
|
709
|
+
fs.writeFileSync(path.join(dir, 'specs', FEATURE, TASK_REL), ['# Task','','Status: done','','## Dependencies', '', '- none', '', '## Verification Plan', '', '- Command: npm test', '', '## Receipt','','' + evidence,'```','pass','```'].join('\n'));
|
|
922
710
|
seedCache({ [FEATURE]: { [TASK_REL]: 'pending' } });
|
|
923
711
|
const body = parseBlock(runHook({}, dir).stdout);
|
|
924
712
|
assert.ok(body, `empty/bare provenance should block for ${desc}`);
|
|
@@ -937,15 +725,17 @@ test('18. valid non-empty Base: value and base_sha: value pass', () => {
|
|
|
937
725
|
for (const { evidence, shouldPass } of validCases) {
|
|
938
726
|
const dir = tmpDir();
|
|
939
727
|
const featureDir = path.join(dir, 'specs', FEATURE);
|
|
728
|
+
fs.mkdirSync(featureDir, { recursive: true });
|
|
729
|
+
fs.writeFileSync(path.join(featureDir, 'plan.md'), '# Demo plan\n');
|
|
940
730
|
fs.mkdirSync(path.join(featureDir, 'tasks'), { recursive: true });
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
fs.writeFileSync(
|
|
731
|
+
const taskFile = path.join(dir, 'specs', FEATURE, TASK_REL);
|
|
732
|
+
const taskText = (receipt) => ['# Task','','Status: done','','## Dependencies', '', '- none', '', '## Verification Plan', '', '- Command: ' + ((evidence.match(/^Command: (.*)$/m) || [])[1] || 'npm test'), '', '## Receipt','','' + receipt,'```','pass','```'].join('\n');
|
|
733
|
+
// Base and Head bind to the live tree only for a committed task file that has since
|
|
734
|
+
// changed, so every case commits the receipt and then edits it: a pass case rebinds
|
|
735
|
+
// to the runtime-derived pair, a fail case keeps its arbitrary values.
|
|
736
|
+
fs.writeFileSync(taskFile, taskText(evidence));
|
|
737
|
+
commitAll(dir, 'receipt');
|
|
738
|
+
fs.writeFileSync(taskFile, shouldPass ? taskText(bindFixtureReceipt(dir, evidence)) : `${taskText(evidence)}\n`);
|
|
949
739
|
seedCache({ [FEATURE]: { [TASK_REL]: 'pending' } });
|
|
950
740
|
const result = runHook({}, dir);
|
|
951
741
|
if (shouldPass) assert.strictEqual(result.stdout, '', `valid provenance should not block: ${evidence.slice(0,40)}`);
|
|
@@ -982,8 +772,8 @@ test('20. cache-hit: receipt mutation (removed Verification/Command) blocks even
|
|
|
982
772
|
// mutate: strip Verification: PASS and Command — keep file as done
|
|
983
773
|
const taskFile = path.join(dir, 'specs', FEATURE, TASK_REL);
|
|
984
774
|
fs.writeFileSync(taskFile, [
|
|
985
|
-
'# Task', '', '
|
|
986
|
-
'##
|
|
775
|
+
'# Task', '', 'Status: done', '',
|
|
776
|
+
'## Dependencies', '', '- none', '', '## Verification Plan', '', '- Command: npm test', '', '## Receipt', '',
|
|
987
777
|
'Exit: 0', `Base: ${VALID_BASE}`, `Head: ${VALID_HEAD}`,
|
|
988
778
|
'```', 'pass', '```',
|
|
989
779
|
].join('\n'));
|
|
@@ -1007,8 +797,8 @@ test('21. cache-hit: provenance mutation (removed Head / changed to stale) block
|
|
|
1007
797
|
// mutate: remove Head, leave only Base
|
|
1008
798
|
const taskFile = path.join(dir, 'specs', FEATURE, TASK_REL);
|
|
1009
799
|
fs.writeFileSync(taskFile, [
|
|
1010
|
-
'# Task', '', '
|
|
1011
|
-
'##
|
|
800
|
+
'# Task', '', 'Status: done', '',
|
|
801
|
+
'## Dependencies', '', '- none', '', '## Verification Plan', '', '- Command: npm test', '', '## Receipt', '', 'Verification: PASS', 'Command: npm test', 'Exit: 0', `Base: ${VALID_BASE}`,
|
|
1012
802
|
'```', 'pass', '```',
|
|
1013
803
|
].join('\n'));
|
|
1014
804
|
let body = parseBlock(runHook({}, dir).stdout);
|
|
@@ -1016,8 +806,8 @@ test('21. cache-hit: provenance mutation (removed Head / changed to stale) block
|
|
|
1016
806
|
assert.match(body.reason, /\bprovenance\b/);
|
|
1017
807
|
// mutate back to include both but with empty Head value (stale)
|
|
1018
808
|
fs.writeFileSync(taskFile, [
|
|
1019
|
-
'# Task', '', '
|
|
1020
|
-
'##
|
|
809
|
+
'# Task', '', 'Status: done', '',
|
|
810
|
+
'## Dependencies', '', '- none', '', '## Verification Plan', '', '- Command: npm test', '', '## Receipt', '', 'Verification: PASS', 'Command: npm test', 'Exit: 0', `Base: ${VALID_BASE}`, 'Head:',
|
|
1021
811
|
'```', 'pass', '```',
|
|
1022
812
|
].join('\n'));
|
|
1023
813
|
body = parseBlock(runHook({}, dir).stdout);
|
|
@@ -1028,22 +818,6 @@ test('21. cache-hit: provenance mutation (removed Head / changed to stale) block
|
|
|
1028
818
|
}
|
|
1029
819
|
});
|
|
1030
820
|
|
|
1031
|
-
test('22. cache-hit: deleted task file blocks even though spec still says done', () => {
|
|
1032
|
-
const dir = makeFixture({ evidence: 'valid' });
|
|
1033
|
-
try {
|
|
1034
|
-
seedCache({ [FEATURE]: { [TASK_REL]: 'pending' } });
|
|
1035
|
-
assert.strictEqual(runHook({}, dir).stdout, '');
|
|
1036
|
-
assert.strictEqual(readCache()[FEATURE][TASK_REL], 'done');
|
|
1037
|
-
fs.unlinkSync(path.join(dir, 'specs', FEATURE, TASK_REL));
|
|
1038
|
-
const body = parseBlock(runHook({}, dir).stdout);
|
|
1039
|
-
assert.ok(body, 'deleted receipt file should block on cache-hit');
|
|
1040
|
-
assert.strictEqual(body.decision, 'block');
|
|
1041
|
-
assert.match(body.reason, /\ba\b/);
|
|
1042
|
-
} finally {
|
|
1043
|
-
fs.rmSync(dir, { recursive: true, force: true });
|
|
1044
|
-
}
|
|
1045
|
-
});
|
|
1046
|
-
|
|
1047
821
|
test('23. malformed cache JSON does not bypass validation', () => {
|
|
1048
822
|
// valid receipt with malformed cache → must still PASS (fail-open on cache parse, not on receipt)
|
|
1049
823
|
const dirValid = makeFixture({ evidence: 'valid' });
|
|
@@ -1096,9 +870,10 @@ test('25. placeholder tokens Command: TODO / Base: TBD / artifact sha256 empty m
|
|
|
1096
870
|
for (const { evidence, check, desc } of cases) {
|
|
1097
871
|
const dir = tmpDir();
|
|
1098
872
|
const featureDir = path.join(dir, 'specs', FEATURE);
|
|
873
|
+
fs.mkdirSync(featureDir, { recursive: true });
|
|
874
|
+
fs.writeFileSync(path.join(featureDir, 'plan.md'), '# Demo plan\n');
|
|
1099
875
|
fs.mkdirSync(path.join(featureDir, 'tasks'), { recursive: true });
|
|
1100
|
-
fs.writeFileSync(path.join(
|
|
1101
|
-
fs.writeFileSync(path.join(dir, 'specs', FEATURE, TASK_REL), ['# Task','','**Status:** done','','## Evidence','','' + evidence,'```','pass','```'].join('\n'));
|
|
876
|
+
fs.writeFileSync(path.join(dir, 'specs', FEATURE, TASK_REL), ['# Task','','Status: done','','## Dependencies', '', '- none', '', '## Verification Plan', '', '- Command: ' + ((evidence.match(/^Command: (.*)$/m) || [])[1] || 'npm test'), '', '## Receipt','','' + evidence,'```','pass','```'].join('\n'));
|
|
1102
877
|
seedCache({ [FEATURE]: { [TASK_REL]: 'pending' } });
|
|
1103
878
|
const body = parseBlock(runHook({}, dir).stdout);
|
|
1104
879
|
assert.ok(body, `placeholder case ${desc} should block`);
|
|
@@ -1110,8 +885,8 @@ test('25. placeholder tokens Command: TODO / Base: TBD / artifact sha256 empty m
|
|
|
1110
885
|
const dir2 = tmpDir();
|
|
1111
886
|
const featureDir2 = path.join(dir2, 'specs', FEATURE);
|
|
1112
887
|
fs.mkdirSync(path.join(featureDir2, 'tasks'), { recursive: true });
|
|
1113
|
-
fs.writeFileSync(path.join(featureDir2, '
|
|
1114
|
-
fs.writeFileSync(path.join(dir2, 'specs', FEATURE, TASK_REL), bindFixtureReceipt(dir2, ['# Task','','
|
|
888
|
+
fs.writeFileSync(path.join(featureDir2, 'plan.md'), '# Demo plan\n');
|
|
889
|
+
fs.writeFileSync(path.join(dir2, 'specs', FEATURE, TASK_REL), bindFixtureReceipt(dir2, ['# Task','','Status: done','','## Dependencies', '', '- none', '', '## Verification Plan', '', '- Command: npm run todo:test', '', '## Receipt','','Verification: PASS','Command: npm run todo:test','Exit: 0',`Base: ${VALID_BASE}`,`Head: ${VALID_HEAD}`,'```','pass','```'].join('\n')));
|
|
1115
890
|
seedCache({ [FEATURE]: { [TASK_REL]: 'pending' } });
|
|
1116
891
|
assert.strictEqual(runHook({}, dir2).stdout, '', 'command containing todo substring should not block');
|
|
1117
892
|
fs.rmSync(dir2, { recursive: true, force: true });
|
|
@@ -1126,9 +901,10 @@ test('26. explicit failure outcomes Tests failed / Result FAIL and multiple Resu
|
|
|
1126
901
|
for (const { evidence, desc } of cases) {
|
|
1127
902
|
const dir = tmpDir();
|
|
1128
903
|
const featureDir = path.join(dir, 'specs', FEATURE);
|
|
904
|
+
fs.mkdirSync(featureDir, { recursive: true });
|
|
905
|
+
fs.writeFileSync(path.join(featureDir, 'plan.md'), '# Demo plan\n');
|
|
1129
906
|
fs.mkdirSync(path.join(featureDir, 'tasks'), { recursive: true });
|
|
1130
|
-
fs.writeFileSync(path.join(
|
|
1131
|
-
fs.writeFileSync(path.join(dir, 'specs', FEATURE, TASK_REL), ['# Task','','**Status:** done','','## Evidence','','' + evidence,'```','pass','```'].join('\n'));
|
|
907
|
+
fs.writeFileSync(path.join(dir, 'specs', FEATURE, TASK_REL), ['# Task','','Status: done','','## Dependencies', '', '- none', '', '## Verification Plan', '', '- Command: pnpm test', '', '## Receipt','','' + evidence,'```','pass','```'].join('\n'));
|
|
1132
908
|
seedCache({ [FEATURE]: { [TASK_REL]: 'pending' } });
|
|
1133
909
|
const body = parseBlock(runHook({}, dir).stdout);
|
|
1134
910
|
assert.ok(body, `explicit failure ${desc} should block`);
|
|
@@ -1140,8 +916,9 @@ test('26. explicit failure outcomes Tests failed / Result FAIL and multiple Resu
|
|
|
1140
916
|
test('27. task markdown symlink outside feature must be rejected (check a)', () => {
|
|
1141
917
|
const dir = tmpDir();
|
|
1142
918
|
const featureDir = path.join(dir, 'specs', FEATURE);
|
|
919
|
+
fs.mkdirSync(featureDir, { recursive: true });
|
|
920
|
+
fs.writeFileSync(path.join(featureDir, 'plan.md'), '# Demo plan\n');
|
|
1143
921
|
fs.mkdirSync(path.join(featureDir, 'tasks'), { recursive: true });
|
|
1144
|
-
fs.writeFileSync(path.join(featureDir, 'spec.json'), JSON.stringify({ status: 'in_progress', feature_name: FEATURE, current_phase: 'closeout', task_registry: { [TASK_REL]: { status: 'done', completed_at: '2026-07-01T00:00:00Z' } } }));
|
|
1145
922
|
const outside = path.join(dir, 'outside.md');
|
|
1146
923
|
fs.writeFileSync(outside, '# Task\n\n**Status:** done\n\n## Evidence\n\nVerification: PASS\nCommand: pnpm test\nExit: 0\nBase: a\nHead: b\n');
|
|
1147
924
|
const taskPath = path.join(dir, 'specs', FEATURE, TASK_REL);
|
|
@@ -1151,23 +928,7 @@ test('27. task markdown symlink outside feature must be rejected (check a)', ()
|
|
|
1151
928
|
const body = parseBlock(runHook({}, dir).stdout);
|
|
1152
929
|
assert.ok(body, 'symlink task outside should block');
|
|
1153
930
|
assert.strictEqual(body.decision, 'block');
|
|
1154
|
-
assert.match(body.reason,
|
|
1155
|
-
fs.rmSync(dir, { recursive: true, force: true });
|
|
1156
|
-
});
|
|
1157
|
-
|
|
1158
|
-
test('28. malformed spec.json must block with invalid_specs', () => {
|
|
1159
|
-
const dir = tmpDir();
|
|
1160
|
-
fs.mkdirSync(path.join(dir, 'specs', 'good'), { recursive: true });
|
|
1161
|
-
fs.mkdirSync(path.join(dir, 'specs', 'bad'), { recursive: true });
|
|
1162
|
-
fs.writeFileSync(path.join(dir, 'specs', 'good', 'spec.json'), JSON.stringify({ status: 'in_progress', feature_name: 'good', task_registry: {} }));
|
|
1163
|
-
fs.writeFileSync(path.join(dir, 'specs', 'bad', 'spec.json'), '{ malformed');
|
|
1164
|
-
seedCache({});
|
|
1165
|
-
const { stdout } = runHook({}, dir);
|
|
1166
|
-
const body = parseBlock(stdout);
|
|
1167
|
-
assert.ok(body, 'malformed spec should block');
|
|
1168
|
-
assert.strictEqual(body.decision, 'block');
|
|
1169
|
-
assert.match(body.reason, /invalid spec/i);
|
|
1170
|
-
assert.match(body.reason, /bad/);
|
|
931
|
+
assert.match(body.reason, /invalid workflow packet/);
|
|
1171
932
|
fs.rmSync(dir, { recursive: true, force: true });
|
|
1172
933
|
});
|
|
1173
934
|
|
|
@@ -1177,7 +938,6 @@ test('29. specs/linked symlink outside must be explicit_malformed and non-explic
|
|
|
1177
938
|
fs.mkdirSync(specsDir, { recursive: true });
|
|
1178
939
|
const outside = path.join(dir, 'outside-feat');
|
|
1179
940
|
fs.mkdirSync(outside, { recursive: true });
|
|
1180
|
-
fs.writeFileSync(path.join(outside, 'spec.json'), JSON.stringify({ status: 'in_progress' }));
|
|
1181
941
|
const link = path.join(specsDir, 'linked');
|
|
1182
942
|
fs.symlinkSync(outside, link);
|
|
1183
943
|
// non-explicit should block with invalid_specs (fail-closed, not phantom no-active)
|
|
@@ -1185,7 +945,7 @@ test('29. specs/linked symlink outside must be explicit_malformed and non-explic
|
|
|
1185
945
|
const nonExplicit = parseBlock(runHook({}, dir).stdout);
|
|
1186
946
|
assert.ok(nonExplicit, 'non-explicit symlink outside should block with invalid_specs');
|
|
1187
947
|
assert.strictEqual(nonExplicit.decision, 'block');
|
|
1188
|
-
assert.match(nonExplicit.reason, /invalid
|
|
948
|
+
assert.match(nonExplicit.reason, /invalid workflow packet/i);
|
|
1189
949
|
assert.match(nonExplicit.reason, /linked/);
|
|
1190
950
|
// explicit should block explicit_malformed
|
|
1191
951
|
const body = parseBlock(runHook({ featureName: 'linked' }, dir).stdout);
|
|
@@ -1227,15 +987,11 @@ test('30. phantom vectors via gate evidence must block with verification_state (
|
|
|
1227
987
|
for (const v of vectors) {
|
|
1228
988
|
const dir = tmpDir();
|
|
1229
989
|
const featureDir = path.join(dir, 'specs', FEATURE);
|
|
990
|
+
fs.mkdirSync(featureDir, { recursive: true });
|
|
991
|
+
fs.writeFileSync(path.join(featureDir, 'plan.md'), '# Demo plan\n');
|
|
1230
992
|
fs.mkdirSync(path.join(featureDir, 'tasks'), { recursive: true });
|
|
1231
|
-
fs.writeFileSync(path.join(featureDir, 'spec.json'), JSON.stringify({
|
|
1232
|
-
status: 'in_progress',
|
|
1233
|
-
feature_name: FEATURE,
|
|
1234
|
-
current_phase: 'closeout',
|
|
1235
|
-
task_registry: { [TASK_REL]: { status: 'done', completed_at: '2026-07-01T00:00:00Z' } },
|
|
1236
|
-
}));
|
|
1237
993
|
const evidence = bindFixtureReceipt(dir, ['Verification: PASS','Command: npm test','Exit: 0',`Base: ${VALID_BASE}`,`Head: ${VALID_HEAD}`, v].join('\n'));
|
|
1238
|
-
fs.writeFileSync(path.join(dir, 'specs', FEATURE, TASK_REL), ['# Task','','
|
|
994
|
+
fs.writeFileSync(path.join(dir, 'specs', FEATURE, TASK_REL), ['# Task','','Status: done','','## Dependencies', '', '- none', '', '## Verification Plan', '', '- Command: npm test', '', '## Receipt','','' + evidence,'```','pass','```'].join('\n'));
|
|
1239
995
|
seedCache({ [FEATURE]: { [TASK_REL]: 'pending' } });
|
|
1240
996
|
const body = parseBlock(runHook({}, dir).stdout);
|
|
1241
997
|
assert.ok(body, `vector should block: ${v}`);
|
|
@@ -1274,14 +1030,9 @@ test('30. phantom vectors via gate evidence must block with verification_state (
|
|
|
1274
1030
|
const dir = tmpDir();
|
|
1275
1031
|
const featureDir = path.join(dir, 'specs', FEATURE);
|
|
1276
1032
|
fs.mkdirSync(path.join(featureDir, 'tasks'), { recursive: true });
|
|
1277
|
-
fs.writeFileSync(path.join(featureDir, '
|
|
1278
|
-
status: 'in_progress',
|
|
1279
|
-
feature_name: FEATURE,
|
|
1280
|
-
current_phase: 'closeout',
|
|
1281
|
-
task_registry: { [TASK_REL]: { status: 'done', completed_at: '2026-07-01T00:00:00Z' } },
|
|
1282
|
-
}));
|
|
1033
|
+
fs.writeFileSync(path.join(featureDir, 'plan.md'), '# Demo plan\n');
|
|
1283
1034
|
const evidence = bindFixtureReceipt(dir, ['Verification: PASS','Command: npm test','Exit: 0',`Base: ${VALID_BASE}`,`Head: ${VALID_HEAD}`, v].join('\n'));
|
|
1284
|
-
fs.writeFileSync(path.join(dir, 'specs', FEATURE, TASK_REL), ['# Task','','
|
|
1035
|
+
fs.writeFileSync(path.join(dir, 'specs', FEATURE, TASK_REL), ['# Task','','Status: done','','## Dependencies', '', '- none', '', '## Verification Plan', '', '- Command: npm test', '', '## Receipt','','' + evidence,'```','pass','```'].join('\n'));
|
|
1285
1036
|
seedCache({ [FEATURE]: { [TASK_REL]: 'pending' } });
|
|
1286
1037
|
const { stdout } = runHook({}, dir);
|
|
1287
1038
|
assert.strictEqual(stdout, '', `positive control should not block: ${v}`);
|
|
@@ -1880,6 +1631,26 @@ test('52. the block names what to change, not just which check failed', () => {
|
|
|
1880
1631
|
}
|
|
1881
1632
|
});
|
|
1882
1633
|
|
|
1634
|
+
test('53. a hand-typed provenance pair is told to re-derive, not to add lines', () => {
|
|
1635
|
+
// Reported from a project whose receipt carried git short SHAs. Telling someone to
|
|
1636
|
+
// add `Base:` and `Head:` lines they had already written sends them looking in the
|
|
1637
|
+
// wrong place; the pair is there, it just is not the one the runtime derives.
|
|
1638
|
+
const dir = makeWorkflowFixture([
|
|
1639
|
+
'## Receipt', '', '- Verification: PASS', '- Command: node --test', '- Exit: 0',
|
|
1640
|
+
'- Base: d72bb57', '- Head: 5da1eca', '```text', '$ node --test', 'pass: 1', '```',
|
|
1641
|
+
]);
|
|
1642
|
+
try {
|
|
1643
|
+
clearCache();
|
|
1644
|
+
const body = parseBlock(runHook({}, dir).stdout);
|
|
1645
|
+
assert.ok(body && body.decision === 'block');
|
|
1646
|
+
assert.match(body.reason, /present but are not the pair this runtime derives/);
|
|
1647
|
+
assert.match(body.reason, /short SHAs/);
|
|
1648
|
+
assert.doesNotMatch(body.reason, /add runtime-derived/, 'the lines exist; do not ask for them again');
|
|
1649
|
+
} finally {
|
|
1650
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
1651
|
+
}
|
|
1652
|
+
});
|
|
1653
|
+
|
|
1883
1654
|
test('49. an unversioned specs root does not keep every receipt bound', () => {
|
|
1884
1655
|
// Committing specs/ is the user's choice, not the tool's requirement. Without a
|
|
1885
1656
|
// committed copy of the task file there is no baseline, so the selector used to keep
|
|
@@ -1955,3 +1726,143 @@ test('48. the gate\'s own runtime state does not defeat structure mode', () => {
|
|
|
1955
1726
|
fs.rmSync(dir, { recursive: true, force: true });
|
|
1956
1727
|
}
|
|
1957
1728
|
});
|
|
1729
|
+
|
|
1730
|
+
test('52. a plan with several commands binds the Receipt to the last one', () => {
|
|
1731
|
+
// A proof that takes several steps declares several commands. The Receipt must name the
|
|
1732
|
+
// last: naming an earlier, cheaper step would let the task close without its real proof.
|
|
1733
|
+
const planned = 'node -v\n- Command: node --test';
|
|
1734
|
+
const earlier = boundReceipt().map((line) => line
|
|
1735
|
+
.replace('Command: node --test', 'Command: node -v').replace('$ node --test', '$ node -v'));
|
|
1736
|
+
const absent = boundReceipt().map((line) => line
|
|
1737
|
+
.replace('Command: node --test', 'Command: npm test').replace('$ node --test', '$ npm test'));
|
|
1738
|
+
const cases = [
|
|
1739
|
+
[boundReceipt(), null, 'the last declared command closes the task'],
|
|
1740
|
+
[earlier, /command_identity/, 'an earlier declared command must not close the task'],
|
|
1741
|
+
[absent, /command_identity/, 'a command the plan never declared must not close the task'],
|
|
1742
|
+
];
|
|
1743
|
+
for (const [receipt, expected, message] of cases) {
|
|
1744
|
+
const dir = makeWorkflowFixture(receipt, planned);
|
|
1745
|
+
try {
|
|
1746
|
+
clearCache();
|
|
1747
|
+
const out = runHook({}, dir).stdout;
|
|
1748
|
+
if (expected === null) {
|
|
1749
|
+
assert.strictEqual(out, '', message);
|
|
1750
|
+
} else {
|
|
1751
|
+
const body = parseBlock(out);
|
|
1752
|
+
assert.ok(body && body.decision === 'block', message);
|
|
1753
|
+
assert.match(body.reason, expected, message);
|
|
1754
|
+
}
|
|
1755
|
+
} finally {
|
|
1756
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
1757
|
+
}
|
|
1758
|
+
}
|
|
1759
|
+
});
|
|
1760
|
+
|
|
1761
|
+
test('53. an indented or sub-heading Command never becomes the canonical one', () => {
|
|
1762
|
+
// A cheap command nested under a Counterexample, or placed under a ### step after the main
|
|
1763
|
+
// block, must not become the command a Receipt can close on.
|
|
1764
|
+
const cheap = boundReceipt().map((line) => line
|
|
1765
|
+
.replace('Command: node --test', 'Command: true').replace('$ node --test', '$ true'));
|
|
1766
|
+
for (const planned of [
|
|
1767
|
+
'node --test\n- Counterexample: none\n - Command: true',
|
|
1768
|
+
'node --test\n\n### Cleanup\n\n- Command: true',
|
|
1769
|
+
]) {
|
|
1770
|
+
for (const [receipt, expected] of [[cheap, /command_identity/], [boundReceipt(), null]]) {
|
|
1771
|
+
const dir = makeWorkflowFixture(receipt, planned);
|
|
1772
|
+
try {
|
|
1773
|
+
clearCache();
|
|
1774
|
+
const out = runHook({}, dir).stdout;
|
|
1775
|
+
if (expected === null) assert.strictEqual(out, '', `the main command must still close: ${planned}`);
|
|
1776
|
+
else assert.match(parseBlock(out).reason, expected, `a nested command must not close: ${planned}`);
|
|
1777
|
+
} finally {
|
|
1778
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
1779
|
+
}
|
|
1780
|
+
}
|
|
1781
|
+
}
|
|
1782
|
+
});
|
|
1783
|
+
|
|
1784
|
+
function plantLegacyPacket(dir, name = 'old') {
|
|
1785
|
+
const legacyDir = path.join(dir, 'specs', name);
|
|
1786
|
+
fs.mkdirSync(path.join(legacyDir, 'tasks'), { recursive: true });
|
|
1787
|
+
fs.writeFileSync(path.join(legacyDir, 'spec.json'), JSON.stringify({
|
|
1788
|
+
feature_name: name,
|
|
1789
|
+
status: 'in_progress',
|
|
1790
|
+
task_registry: { 'tasks/task-R1-01-x.md': { status: 'done' } },
|
|
1791
|
+
}));
|
|
1792
|
+
fs.writeFileSync(path.join(legacyDir, 'tasks', 'task-R1-01-x.md'), '# Task\n\n**Status:** done\n');
|
|
1793
|
+
}
|
|
1794
|
+
|
|
1795
|
+
function writeActiveFeature(dir, featureName) {
|
|
1796
|
+
fs.mkdirSync(path.join(dir, 'specs', '_shared'), { recursive: true });
|
|
1797
|
+
fs.writeFileSync(path.join(dir, 'specs', '_shared', 'active-feature.json'), JSON.stringify({ featureName }));
|
|
1798
|
+
}
|
|
1799
|
+
|
|
1800
|
+
test('a leftover legacy packet alone never blocks Stop', () => {
|
|
1801
|
+
const dir = tmpDir();
|
|
1802
|
+
try {
|
|
1803
|
+
plantLegacyPacket(dir);
|
|
1804
|
+
clearCache();
|
|
1805
|
+
assert.strictEqual(runHook({}, dir).stdout, '');
|
|
1806
|
+
} finally {
|
|
1807
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
1808
|
+
}
|
|
1809
|
+
});
|
|
1810
|
+
|
|
1811
|
+
test('a recorded active feature naming a legacy packet does not block Stop', () => {
|
|
1812
|
+
const dir = makeFixture({ evidence: 'valid' });
|
|
1813
|
+
try {
|
|
1814
|
+
plantLegacyPacket(dir);
|
|
1815
|
+
writeActiveFeature(dir, 'old');
|
|
1816
|
+
clearCache();
|
|
1817
|
+
assert.strictEqual(runHook({}, dir).stdout, '');
|
|
1818
|
+
} finally {
|
|
1819
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
1820
|
+
}
|
|
1821
|
+
});
|
|
1822
|
+
|
|
1823
|
+
test('a recorded active feature naming a missing packet still blocks as explicit_not_found', () => {
|
|
1824
|
+
const dir = makeFixture({ evidence: 'valid' });
|
|
1825
|
+
try {
|
|
1826
|
+
writeActiveFeature(dir, 'nope');
|
|
1827
|
+
clearCache();
|
|
1828
|
+
const body = parseBlock(runHook({}, dir).stdout);
|
|
1829
|
+
assert.strictEqual(body?.decision, 'block');
|
|
1830
|
+
assert.match(body.reason, /explicit_not_found/);
|
|
1831
|
+
} finally {
|
|
1832
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
1833
|
+
}
|
|
1834
|
+
});
|
|
1835
|
+
|
|
1836
|
+
test('a process-first packet with a dependency cycle and a done task blocks Stop', () => {
|
|
1837
|
+
const dir = tmpDir();
|
|
1838
|
+
try {
|
|
1839
|
+
const featureDir = path.join(dir, 'specs', FEATURE);
|
|
1840
|
+
fs.mkdirSync(featureDir, { recursive: true });
|
|
1841
|
+
fs.writeFileSync(path.join(featureDir, 'plan.md'), '# Demo plan\nSpecs-Contract: process-first-ready-v1\n');
|
|
1842
|
+
const task = (status, dependency) => [
|
|
1843
|
+
'# Task', '', `Status: ${status}`, '', '## Dependencies', '', `- ${dependency}`, '',
|
|
1844
|
+
'## Verification Plan', '', '- Command: npm test', '',
|
|
1845
|
+
].join('\n');
|
|
1846
|
+
fs.writeFileSync(path.join(featureDir, 'task-01-a.md'), task('done', 'task-02-b.md'));
|
|
1847
|
+
fs.writeFileSync(path.join(featureDir, 'task-02-b.md'), task('pending', 'task-01-a.md'));
|
|
1848
|
+
clearCache();
|
|
1849
|
+
const body = parseBlock(runHook({}, dir).stdout);
|
|
1850
|
+
assert.strictEqual(body?.decision, 'block');
|
|
1851
|
+
assert.match(body.reason, /invalid workflow packet/);
|
|
1852
|
+
} finally {
|
|
1853
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
1854
|
+
}
|
|
1855
|
+
});
|
|
1856
|
+
|
|
1857
|
+
test('a packet holding both plan.md and spec.json is read as process-first and its unreceipted done task blocks', () => {
|
|
1858
|
+
const dir = makeFixture({ evidence: 'missing-evidence' });
|
|
1859
|
+
try {
|
|
1860
|
+
fs.writeFileSync(path.join(dir, 'specs', FEATURE, 'spec.json'), JSON.stringify({ feature_name: FEATURE }));
|
|
1861
|
+
clearCache();
|
|
1862
|
+
const body = parseBlock(runHook({}, dir).stdout);
|
|
1863
|
+
assert.strictEqual(body?.decision, 'block');
|
|
1864
|
+
assert.match(body.reason, new RegExp(TASK_REL));
|
|
1865
|
+
} finally {
|
|
1866
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
1867
|
+
}
|
|
1868
|
+
});
|