@haposoft/cafekit 0.16.0-rc.3 → 0.16.0-rc.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/__tests__/change-firewall.semantic-firewall.test.js +2134 -0
- package/bin/__tests__/codex-hooks.test.js +531 -29
- package/bin/__tests__/codex-native.test.js +228 -7
- package/bin/__tests__/develop-contract.test.js +653 -185
- package/bin/__tests__/finalizer-pass-gate.semantic-firewall.test.js +342 -0
- package/bin/__tests__/installer-safety.test.js +39 -0
- package/bin/__tests__/opencode-spec-gate.test.js +24 -16
- package/bin/__tests__/package-inventory.test.js +705 -13
- package/bin/__tests__/release-preflight.semantic-firewall.test.js +578 -0
- package/bin/__tests__/review-receipt-c2.semantic-firewall.test.js +378 -0
- package/bin/__tests__/specs-v2-execution-closeout.test.js +468 -0
- package/bin/__tests__/specs-v2-policy-and-scaffold.test.js +665 -0
- package/bin/__tests__/specs-v2-validator-grounder-contract.test.js +1132 -0
- package/bin/__tests__/test-runtime-dependency-closure.cjs +182 -0
- package/bin/__tests__/validator-grounding.test.js +2013 -221
- package/bin/lib/managed-writer.js +12 -3
- package/bin/phases/codex-runtime.js +17 -1
- package/package.json +5 -2
- package/src/claude/CLAUDE.md +6 -0
- package/src/claude/agents/code-auditor.md +20 -0
- package/src/claude/agents/spec-maker.md +143 -99
- package/src/claude/hooks/__tests__/completion-authority.test.js +321 -58
- package/src/claude/hooks/__tests__/privacy-block.test.js +254 -0
- package/src/claude/hooks/__tests__/semantic-review-authority.test.js +257 -0
- package/src/claude/hooks/__tests__/spec-gate.test.js +115 -40
- package/src/claude/hooks/completion-authority-check.cjs +37 -288
- package/src/claude/hooks/completion-authority-state.cjs +10 -8
- package/src/claude/hooks/completion-authority.cjs +13 -7
- package/src/claude/hooks/docs-sync.cjs +5 -2
- package/src/claude/hooks/lib/privacy-command-analysis.cjs +393 -0
- package/src/claude/hooks/lib/runtime-path-safety.cjs +119 -0
- package/src/claude/hooks/privacy-block.cjs +99 -28
- package/src/claude/hooks/semantic-review-authority.cjs +152 -0
- package/src/claude/hooks/spec-gate.cjs +47 -117
- package/src/claude/hooks/spec-state.cjs +8 -2
- package/src/claude/migration-manifest.json +9 -1
- package/src/claude/rules/state-sync.md +52 -21
- package/src/claude/rules/workflow.md +17 -9
- package/src/claude/scripts/change-firewall.cjs +1748 -0
- package/src/claude/scripts/spec-authoring-digest.cjs +151 -0
- package/src/claude/scripts/spec-authoring-validation.cjs +126 -0
- package/src/claude/scripts/spec-final-state.cjs +290 -0
- package/src/claude/scripts/spec-ground.cjs +571 -173
- package/src/claude/scripts/spec-readiness.cjs +425 -0
- package/src/claude/scripts/spec-receipt.cjs +162 -0
- package/src/claude/scripts/spec-resolver.cjs +67 -16
- package/src/claude/scripts/spec-scaffold.cjs +1594 -139
- package/src/claude/scripts/spec-semantic-model.cjs +396 -0
- package/src/claude/scripts/validate-spec-output.cjs +3110 -218
- package/src/claude/scripts/workflow-policy.cjs +501 -183
- package/src/claude/settings/settings.json +4 -0
- package/src/claude/skills/code-review/SKILL.md +12 -3
- package/src/claude/skills/code-review/references/spec-compliance-review.md +10 -2
- package/src/claude/skills/code-review/references/verification-gate.md +13 -0
- package/src/claude/skills/develop/SKILL.md +47 -60
- package/src/claude/skills/develop/references/parallel-waves.md +9 -6
- package/src/claude/skills/develop/references/quality-gate.md +19 -12
- package/src/claude/skills/specs/SKILL.md +183 -177
- package/src/claude/skills/specs/references/ask-user-question-gates.md +27 -18
- package/src/claude/skills/specs/references/cross-spec-dependency.md +8 -4
- package/src/claude/skills/specs/references/research-strategy.md +4 -1
- package/src/claude/skills/specs/references/review.md +214 -262
- package/src/claude/skills/specs/references/scope-inquiry.md +18 -5
- package/src/claude/skills/specs/rules/design-discovery-full.md +33 -81
- package/src/claude/skills/specs/rules/design-discovery-light.md +21 -42
- package/src/claude/skills/specs/rules/design-principles.md +136 -180
- package/src/claude/skills/specs/rules/design-review.md +42 -97
- package/src/claude/skills/specs/rules/ears-format.md +42 -50
- package/src/claude/skills/specs/rules/phase-decision-matrix.md +42 -41
- package/src/claude/skills/specs/rules/task-scoring-rubric.md +2 -1
- package/src/claude/skills/specs/rules/tasks-generation.md +189 -300
- package/src/claude/skills/specs/templates/design.md +49 -263
- package/src/claude/skills/specs/templates/requirements.md +26 -36
- package/src/claude/skills/specs/templates/research.md +9 -98
- package/src/claude/skills/specs/templates/spec-state.json +35 -85
- package/src/claude/skills/specs/templates/task.md +30 -102
- package/src/claude/skills/sync/SKILL.md +33 -10
- package/src/claude/skills/sync/references/sync-protocols.md +31 -13
- package/src/claude/skills/test/SKILL.md +17 -7
- package/src/codex/AGENTS.md +6 -0
- package/src/codex/hooks/completion-authority-check.cjs +43 -240
- package/src/codex/hooks/completion-authority-state.cjs +10 -8
- package/src/codex/hooks/completion-authority.cjs +13 -7
- package/src/codex/hooks/lib/hook-context.cjs +16 -2
- package/src/codex/hooks/lib/privacy-command-analysis.cjs +3 -0
- package/src/codex/hooks/lib/privacy-state.cjs +61 -38
- package/src/codex/hooks/lib/runtime-path-safety.cjs +3 -0
- package/src/codex/hooks/lib/spec-receipt.cjs +72 -134
- package/src/codex/hooks/lib/spec-utils.cjs +56 -5
- package/src/codex/hooks/privacy-approval.cjs +6 -2
- package/src/codex/hooks/privacy-block.cjs +94 -15
- package/src/codex/hooks/semantic-review-authority.cjs +130 -0
- package/src/codex/hooks/spec-gate.cjs +49 -34
- package/src/codex/hooks/spec-state.cjs +10 -3
- package/src/codex/hooks/state.cjs +2 -0
- package/src/codex/hooks.json +6 -1
- package/src/codex/rules/state-sync.md +47 -27
- package/src/common/AGENTS.md +1 -1
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
const fs = require('fs');
|
|
5
|
+
const path = require('path');
|
|
6
|
+
const { spawnSync } = require('child_process');
|
|
7
|
+
const STATE = require('./completion-authority-state.cjs');
|
|
8
|
+
const PATH_SAFETY = require('./lib/runtime-path-safety.cjs');
|
|
9
|
+
|
|
10
|
+
const SCHEMA_VERSION = 1;
|
|
11
|
+
const MARKER = 'CAFEKIT_SEMANTIC_REVIEW_ATTESTATION ';
|
|
12
|
+
const SHA256_RE = /^sha256:[a-f0-9]{64}$/;
|
|
13
|
+
const REVIEWER_ROLE_ALLOWLIST = new Set(['code-auditor', 'code_auditor']);
|
|
14
|
+
const RECORD_FIELDS = ['schema_version', 'kind', 'identity', 'semantic_digest', 'event', 'verdict', 'host_session_id', 'reviewer_agent_id', 'reviewer_agent_type', 'issued_at', 'mac'];
|
|
15
|
+
|
|
16
|
+
function plain(value) {
|
|
17
|
+
return Boolean(value && typeof value === 'object' && !Array.isArray(value));
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function exactKeys(value, fields) {
|
|
21
|
+
return plain(value) && JSON.stringify(Object.keys(value).sort()) === JSON.stringify([...fields].sort());
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function inside(root, target) {
|
|
25
|
+
const relative = path.relative(root, target);
|
|
26
|
+
return relative !== '' && relative !== '..' && !relative.startsWith(`..${path.sep}`) && !path.isAbsolute(relative);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function projectRoot(payload = {}) {
|
|
30
|
+
const installedRoot = path.resolve(__dirname, '..', '..');
|
|
31
|
+
if (fs.existsSync(path.join(installedRoot, '.claude', 'hooks', path.basename(__filename)))) {
|
|
32
|
+
try { return STATE.canonicalProjectRoot(installedRoot); } catch {}
|
|
33
|
+
}
|
|
34
|
+
for (const candidate of [process.env.CLAUDE_PROJECT_DIR, process.env.PROJECT_ROOT]) {
|
|
35
|
+
if (typeof candidate === 'string' && candidate.trim()) {
|
|
36
|
+
try { return STATE.canonicalProjectRoot(candidate); } catch { /* continue */ }
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
const cwd = typeof payload.cwd === 'string' && payload.cwd.trim() ? payload.cwd : process.cwd();
|
|
40
|
+
return STATE.canonicalProjectRoot(cwd);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function identityFor(root, specFile, featureName) {
|
|
44
|
+
if (typeof featureName !== 'string' || !featureName.trim() || featureName.includes('/') || featureName.includes('\\') || featureName === '.' || featureName === '..' || featureName.includes('..')) throw new Error('feature identity is malformed');
|
|
45
|
+
if (!/^[A-Za-z0-9][A-Za-z0-9._-]*$/.test(featureName)) throw new Error('feature identity is malformed');
|
|
46
|
+
const canonicalRoot = STATE.canonicalProjectRoot(root);
|
|
47
|
+
const canonicalSpec = fs.realpathSync(path.resolve(specFile));
|
|
48
|
+
if (!inside(canonicalRoot, canonicalSpec) || path.basename(canonicalSpec) !== 'spec.json') throw new Error('spec identity escapes the project');
|
|
49
|
+
const relative = path.relative(canonicalRoot, canonicalSpec);
|
|
50
|
+
const expected = path.join('specs', featureName, 'spec.json');
|
|
51
|
+
if (relative !== expected) throw new Error(`spec identity must be ${expected}, not ${relative}`);
|
|
52
|
+
const spec = JSON.parse(fs.readFileSync(canonicalSpec, 'utf8'));
|
|
53
|
+
if (spec.feature_name !== featureName) throw new Error('feature identity does not match spec.json');
|
|
54
|
+
return { project_root: canonicalRoot, spec_file: canonicalSpec, feature_name: featureName };
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function recordPath(root, identity) {
|
|
58
|
+
const name = STATE.digest({ namespace: 'cafekit-semantic-review-observation-v1', identity });
|
|
59
|
+
return path.join(STATE.stateDir(root), 'semantic-reviews', `observation-${name}.json`);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function parseClaim(message) {
|
|
63
|
+
const lines = String(message || '').split(/\r?\n/).filter((line) => line.startsWith(MARKER));
|
|
64
|
+
if (lines.length === 0) return null;
|
|
65
|
+
if (lines.length !== 1) throw new Error('exactly one semantic review attestation marker is required');
|
|
66
|
+
const claim = JSON.parse(lines[0].slice(MARKER.length));
|
|
67
|
+
if (!exactKeys(claim, ['feature_name', 'spec_file', 'semantic_digest', 'verdict'])) throw new Error('semantic review claim fields are malformed');
|
|
68
|
+
if (claim.verdict !== 'PASS' || !SHA256_RE.test(claim.semantic_digest || '')) throw new Error('semantic review claim must contain literal PASS and a sha256 digest');
|
|
69
|
+
return claim;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function validatorFor(root) {
|
|
73
|
+
const installedRoot = path.join(root, '.claude');
|
|
74
|
+
const installedHook = path.join(installedRoot, 'hooks', path.basename(__filename));
|
|
75
|
+
if (fs.existsSync(installedHook)) {
|
|
76
|
+
const canonicalHook = PATH_SAFETY.canonicalRegularFile(root, installedHook, 'installed semantic hook');
|
|
77
|
+
if (canonicalHook !== fs.realpathSync(__filename)) throw new Error('installed semantic hook identity is invalid');
|
|
78
|
+
const validator = path.join(installedRoot, 'scripts', 'validate-spec-output.cjs');
|
|
79
|
+
PATH_SAFETY.verifyCommonJsClosure(installedRoot, validator);
|
|
80
|
+
return PATH_SAFETY.canonicalRegularFile(installedRoot, validator, 'semantic validator');
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const sourceRuntime = path.resolve(__dirname, '..');
|
|
84
|
+
const sourceRoot = path.resolve(sourceRuntime, '..');
|
|
85
|
+
if (path.basename(sourceRuntime) !== 'claude' || path.basename(sourceRoot) !== 'src') {
|
|
86
|
+
throw new Error('semantic validator runtime root is not recognized');
|
|
87
|
+
}
|
|
88
|
+
const validator = path.join(sourceRuntime, 'scripts', 'validate-spec-output.cjs');
|
|
89
|
+
PATH_SAFETY.verifyCommonJsClosure(sourceRoot, validator);
|
|
90
|
+
return PATH_SAFETY.canonicalRegularFile(sourceRoot, validator, 'semantic validator');
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function currentDigest(root, specFile) {
|
|
94
|
+
const validator = validatorFor(root);
|
|
95
|
+
const result = spawnSync(process.execPath, [validator, path.dirname(specFile), '--semantic-digest'], { cwd: root, encoding: 'utf8' });
|
|
96
|
+
const digest = result.stdout.trim();
|
|
97
|
+
if (result.status !== 0 || !SHA256_RE.test(digest)) throw new Error(`semantic digest could not be recomputed (${result.stderr.trim() || `exit ${result.status}`})`);
|
|
98
|
+
return digest;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function recordFromSubagentStop(payload) {
|
|
102
|
+
// Honest-agent guardrail: the host hook observes this SubagentStop payload.
|
|
103
|
+
// The worker-authored claim is not authenticated input or a security boundary.
|
|
104
|
+
if (!plain(payload) || payload.hook_event_name !== 'SubagentStop') return { recorded: false, reason: 'event' };
|
|
105
|
+
for (const field of ['session_id', 'agent_id', 'agent_type', 'last_assistant_message']) {
|
|
106
|
+
if (typeof payload[field] !== 'string' || !payload[field].trim()) return { recorded: false, reason: `missing-${field}` };
|
|
107
|
+
}
|
|
108
|
+
if (!REVIEWER_ROLE_ALLOWLIST.has(payload.agent_type)) return { recorded: false, reason: 'reviewer-role' };
|
|
109
|
+
const root = projectRoot(payload);
|
|
110
|
+
const claim = parseClaim(payload.last_assistant_message);
|
|
111
|
+
if (claim === null) return { recorded: false, reason: 'no-claim' };
|
|
112
|
+
const requestedSpec = path.resolve(root, claim.spec_file);
|
|
113
|
+
const identity = identityFor(root, requestedSpec, claim.feature_name);
|
|
114
|
+
const digest = currentDigest(root, identity.spec_file);
|
|
115
|
+
if (digest !== claim.semantic_digest) throw new Error('claimed semantic digest does not match current artifacts');
|
|
116
|
+
const key = STATE.ensureKey();
|
|
117
|
+
const record = {
|
|
118
|
+
schema_version: SCHEMA_VERSION, kind: 'semantic-review-observation', identity,
|
|
119
|
+
semantic_digest: digest, event: 'SubagentStop', verdict: 'PASS',
|
|
120
|
+
host_session_id: payload.session_id, reviewer_agent_id: payload.agent_id,
|
|
121
|
+
reviewer_agent_type: payload.agent_type, issued_at: new Date().toISOString(),
|
|
122
|
+
};
|
|
123
|
+
record.mac = STATE.recordMac(record, key);
|
|
124
|
+
STATE.atomicWrite(recordPath(root, identity), record);
|
|
125
|
+
return { recorded: true, record };
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function verifyAttestation(root, specFile, featureName, semanticDigest) {
|
|
129
|
+
let identity;
|
|
130
|
+
try { identity = identityFor(root, specFile, featureName); } catch (error) { return { ok: false, reason: error.message }; }
|
|
131
|
+
const file = recordPath(root, identity);
|
|
132
|
+
let record;
|
|
133
|
+
try { record = JSON.parse(fs.readFileSync(file, 'utf8')); } catch (error) { return { ok: false, reason: error.code === 'ENOENT' ? 'matching host-hook observation is missing' : 'host-hook observation is unreadable' }; }
|
|
134
|
+
const key = STATE.readKey();
|
|
135
|
+
if (key.missing) return { ok: false, reason: 'completion authority key is missing' };
|
|
136
|
+
if (!exactKeys(record, RECORD_FIELDS) || !STATE.macMatches(record, key.key)) return { ok: false, reason: 'host-hook observation is malformed or its MAC is invalid' };
|
|
137
|
+
if (record.schema_version !== SCHEMA_VERSION || record.kind !== 'semantic-review-observation' || record.event !== 'SubagentStop' || record.verdict !== 'PASS') return { ok: false, reason: 'host-hook observation event or verdict is invalid' };
|
|
138
|
+
if (STATE.stableStringify(record.identity) !== STATE.stableStringify(identity)) return { ok: false, reason: 'host-hook observation project, feature, or spec identity is invalid' };
|
|
139
|
+
if (record.semantic_digest !== semanticDigest) return { ok: false, reason: 'host-hook observation semantic digest is stale' };
|
|
140
|
+
return { ok: true, record };
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
if (require.main === module) {
|
|
144
|
+
try {
|
|
145
|
+
const raw = fs.readFileSync(0, 'utf8').trim();
|
|
146
|
+
if (raw) recordFromSubagentStop(JSON.parse(raw));
|
|
147
|
+
} catch (error) {
|
|
148
|
+
process.stderr.write(`CafeKit semantic review SubagentStop claim rejected: ${error.message}\n`);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
module.exports = { MARKER, REVIEWER_ROLE_ALLOWLIST, recordFromSubagentStop, verifyAttestation, identityFor, recordPath };
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* Stop Hook — spec-gate.cjs
|
|
6
6
|
* Implements: https://docs.anthropic.com/en/docs/claude-code/hooks
|
|
7
7
|
*
|
|
8
|
-
* Blocks turn end when
|
|
8
|
+
* Blocks turn end when any done task lacks a verification receipt.
|
|
9
9
|
* Block contract (exit 0 + stdout): {"decision":"block","reason":"..."}.
|
|
10
10
|
*
|
|
11
11
|
* Safety: stop_hook_active loop guard; no worker-writable completion-gate
|
|
@@ -83,11 +83,17 @@ try {
|
|
|
83
83
|
|
|
84
84
|
let POLICY;
|
|
85
85
|
let RESOLVER;
|
|
86
|
+
let RECEIPT;
|
|
87
|
+
let FINAL_STATE;
|
|
86
88
|
try {
|
|
87
89
|
POLICY = require(policyPath);
|
|
88
90
|
RESOLVER = require(path.join(__dirname, '..', 'scripts', 'spec-resolver.cjs'));
|
|
91
|
+
RECEIPT = require(path.join(__dirname, '..', 'scripts', 'spec-receipt.cjs'));
|
|
92
|
+
FINAL_STATE = require('./completion-authority-check.cjs');
|
|
89
93
|
if (typeof POLICY.validateCanonicalReceipt !== 'function'
|
|
90
|
-
|| typeof POLICY.completionDecisionForSpec !== 'function'
|
|
94
|
+
|| typeof POLICY.completionDecisionForSpec !== 'function'
|
|
95
|
+
|| typeof RECEIPT.checkTaskReceipt !== 'function'
|
|
96
|
+
|| typeof FINAL_STATE.evaluateCloseout !== 'function') {
|
|
91
97
|
throw new Error('shared workflow policy lacks completion authority functions');
|
|
92
98
|
}
|
|
93
99
|
} catch (error) {
|
|
@@ -106,11 +112,9 @@ try {
|
|
|
106
112
|
} catch { /* gate stays on */ }
|
|
107
113
|
// Active-spec discovery via shared resolver — explicit target if present, else fail on ambiguity.
|
|
108
114
|
const baseDir = cwd;
|
|
109
|
-
const
|
|
110
|
-
const explicitPath = payload.specPath || payload.spec_path || payload.featurePath || null;
|
|
111
|
-
const resolved = RESOLVER.resolveActiveSpec({ projectRoot: baseDir, runtime, explicitFeature, explicitPath });
|
|
115
|
+
const resolved = FINAL_STATE.resolveCandidate({ resolver: RESOLVER, projectRoot: baseDir, runtime, payload });
|
|
112
116
|
if (!resolved) process.exit(0);
|
|
113
|
-
if (resolved.error === 'multiple_active') {
|
|
117
|
+
if (resolved.error === 'multiple_active' || resolved.error === 'multiple_persisted') {
|
|
114
118
|
process.stdout.write(JSON.stringify({
|
|
115
119
|
decision: 'block',
|
|
116
120
|
reason: `Completion gate: multiple active specs detected (${resolved.candidates.join(', ')}). Provide explicit feature target or resolve ambiguity before completing tasks. Candidates: ${resolved.candidates.join(', ')}`
|
|
@@ -136,6 +140,23 @@ try {
|
|
|
136
140
|
const activeSpec = resolved.spec;
|
|
137
141
|
const featureName = resolved.featureName;
|
|
138
142
|
const specsPath = resolved.specsDir;
|
|
143
|
+
const lifecyclePhase = activeSpec.current_phase || activeSpec.phase;
|
|
144
|
+
const explicitCloseout = ['done', 'completed', 'complete'].includes(activeSpec.status)
|
|
145
|
+
|| ['closeout', 'completion', 'completed', 'complete'].includes(lifecyclePhase);
|
|
146
|
+
if (activeSpec.schema_version === '2.1') {
|
|
147
|
+
const finalState = FINAL_STATE.evaluateCloseout({
|
|
148
|
+
resolver: RESOLVER,
|
|
149
|
+
policy: POLICY,
|
|
150
|
+
projectRoot: baseDir,
|
|
151
|
+
runtime,
|
|
152
|
+
payload: { ...payload, session_id: sessionIdentity(payload) },
|
|
153
|
+
});
|
|
154
|
+
if (!finalState.ok) {
|
|
155
|
+
emitBlock(`Completion gate: ${finalState.reason}`);
|
|
156
|
+
process.exit(0);
|
|
157
|
+
}
|
|
158
|
+
if (finalState.active) process.exit(0);
|
|
159
|
+
}
|
|
139
160
|
const runtimeContext = POLICY.deriveRuntimeContext({
|
|
140
161
|
projectRoot: baseDir,
|
|
141
162
|
specsRoot: specsPath,
|
|
@@ -181,117 +202,27 @@ try {
|
|
|
181
202
|
const allDoneTasks = Object.keys(taskRegistry).filter((tp) =>
|
|
182
203
|
(taskRegistry[tp]?.status || 'pending') === 'done'
|
|
183
204
|
);
|
|
184
|
-
const
|
|
185
|
-
// /m so ^ matches line starts (Evidence is never at byte 0 of the file).
|
|
186
|
-
// legacy heading aliases: read-compat only, no longer advertised
|
|
187
|
-
const EVID_RE = /^#{2,3}\s+(Evidence|Task Test Plan & Verification Evidence|Verification & Evidence)\b/m;
|
|
188
|
-
const PASS_MARKER_RE = /^\s*Verification:\s*PASS\s*$/m;
|
|
189
|
-
const LEGACY_SUCCESS_RE = /^\s*(?:PASS(?:ED)?|✓)(?:\s*:|$)|exit\s+code\s*[:=]?\s*0\b/im;
|
|
190
|
-
|
|
191
|
-
/** Body of first Evidence heading until next same-or-higher heading. */
|
|
192
|
-
function evidenceBody(text) {
|
|
193
|
-
const lines = text.split('\n');
|
|
194
|
-
let start = -1, level = 0;
|
|
195
|
-
for (let i = 0; i < lines.length; i++) {
|
|
196
|
-
const m = lines[i].match(/^(#{2,3})\s+(Evidence|Task Test Plan & Verification Evidence|Verification & Evidence)\b/);
|
|
197
|
-
if (m) { start = i + 1; level = m[1].length; break; }
|
|
198
|
-
}
|
|
199
|
-
if (start < 0) return null;
|
|
200
|
-
let end = lines.length;
|
|
201
|
-
for (let i = start; i < lines.length; i++) {
|
|
202
|
-
const hm = lines[i].match(/^(#{1,6})\s+/);
|
|
203
|
-
const isTapMetadata = typeof POLICY.isTapMetadataHeading === 'function'
|
|
204
|
-
&& POLICY.isTapMetadataHeading(lines[i]);
|
|
205
|
-
if (hm && hm[1].length <= level && !isTapMetadata) { end = i; break; }
|
|
206
|
-
}
|
|
207
|
-
return lines.slice(start, end).join('\n');
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
function validateCanonicalReceipt(body, options) {
|
|
211
|
-
return POLICY.validateCanonicalReceipt(body, options);
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
// Receipt checks a–h → failed letter list.
|
|
215
|
-
// a: file + Status header has "done"; b: Evidence heading; c: no {{...}} + unambiguous PASS; d: completed_at; e: command; f: exit/result; g: provenance; h: artifact hash
|
|
216
|
-
function safeTaskFile(featureDir, taskPath) {
|
|
217
|
-
const resolvedFeature = path.resolve(featureDir);
|
|
218
|
-
const target = path.resolve(featureDir, taskPath);
|
|
219
|
-
const relative = path.relative(resolvedFeature, target);
|
|
220
|
-
if (relative === '..' || relative.startsWith(`..${path.sep}`) || path.isAbsolute(relative)) return null;
|
|
221
|
-
if (!fs.existsSync(target)) return null;
|
|
222
|
-
try {
|
|
223
|
-
const canonicalFeature = fs.realpathSync(resolvedFeature);
|
|
224
|
-
const canonicalTarget = fs.realpathSync(target);
|
|
225
|
-
const rel = path.relative(canonicalFeature, canonicalTarget);
|
|
226
|
-
if (rel === '..' || rel.startsWith(`..${path.sep}`) || path.isAbsolute(rel)) return null;
|
|
227
|
-
} catch {
|
|
228
|
-
return null;
|
|
229
|
-
}
|
|
230
|
-
return target;
|
|
231
|
-
}
|
|
232
|
-
|
|
233
|
-
function checkReceipt(taskPath) {
|
|
234
|
-
const fails = [];
|
|
235
|
-
const featureDir = path.join(specsPath, featureName);
|
|
236
|
-
const abs = safeTaskFile(featureDir, taskPath);
|
|
237
|
-
if (!abs || !fs.existsSync(abs)) return ['a'];
|
|
238
|
-
let text = '';
|
|
239
|
-
try { text = fs.readFileSync(abs, 'utf8'); } catch { return ['a']; }
|
|
240
|
-
|
|
241
|
-
const statusLine = text.split('\n').find((l) => /^\s*(\*\*)?Status(\*\*)?\s*:/i.test(l));
|
|
242
|
-
if (!statusLine || !/\bdone\b/i.test(statusLine)) fails.push('a');
|
|
243
|
-
|
|
244
|
-
const body = evidenceBody(text);
|
|
245
|
-
if (body !== null) receiptBodies.set(taskPath, body);
|
|
246
|
-
if (body === null || !EVID_RE.test(text)) {
|
|
247
|
-
fails.push('b');
|
|
248
|
-
} else if (
|
|
249
|
-
/\{\{[^}]+\}\}/.test(body) ||
|
|
250
|
-
!(PASS_MARKER_RE.test(body) || LEGACY_SUCCESS_RE.test(body))
|
|
251
|
-
) {
|
|
252
|
-
fails.push('c');
|
|
253
|
-
} else {
|
|
254
|
-
// Canonical receipt checks for done tasks — delegated to shared validator (single authority)
|
|
255
|
-
const canonicalFails = validateCanonicalReceipt(body, POLICY.receiptValidatorOptions(taskRegistry[taskPath], {
|
|
256
|
-
runtimeContext,
|
|
257
|
-
requireProvenanceBinding: true,
|
|
258
|
-
}));
|
|
259
|
-
const map = { verification_state: 'c', command: 'e', exit_result: 'f', provenance: 'g', artifact_hash: 'h', artifact_declaration: 'h', placeholder: 'c', shared_validator: 'c', validator_unavailable: 'c' };
|
|
260
|
-
for (const cf of canonicalFails) {
|
|
261
|
-
if (cf === 'verification_state' || cf === 'shared_validator' || cf === 'validator_unavailable') {
|
|
262
|
-
if (!fails.includes('c')) fails.push('c');
|
|
263
|
-
continue;
|
|
264
|
-
}
|
|
265
|
-
if (cf === 'placeholder') {
|
|
266
|
-
if (!fails.includes('c')) fails.push('c');
|
|
267
|
-
continue;
|
|
268
|
-
}
|
|
269
|
-
const letter = map[cf];
|
|
270
|
-
if (letter && !fails.includes(letter)) fails.push(letter);
|
|
271
|
-
}
|
|
272
|
-
// Enforce strict Verification: PASS, not just legacy success
|
|
273
|
-
if (!PASS_MARKER_RE.test(body) && !fails.includes('c')) fails.push('c');
|
|
274
|
-
}
|
|
275
|
-
|
|
276
|
-
const at = taskRegistry[taskPath]?.completed_at;
|
|
277
|
-
if (
|
|
278
|
-
typeof at !== 'string' ||
|
|
279
|
-
!/^\d{4}-\d{2}-\d{2}T/.test(at) ||
|
|
280
|
-
Number.isNaN(Date.parse(at))
|
|
281
|
-
) fails.push('d');
|
|
282
|
-
return fails;
|
|
283
|
-
}
|
|
284
|
-
|
|
205
|
+
const featureDir = path.join(specsPath, featureName);
|
|
285
206
|
const failures = allDoneTasks
|
|
286
|
-
.map((
|
|
207
|
+
.map((taskPath) => ({
|
|
208
|
+
taskPath,
|
|
209
|
+
fails: RECEIPT.checkTaskReceipt(featureDir, taskPath, taskRegistry[taskPath], runtimeContext, POLICY).failures,
|
|
210
|
+
}))
|
|
287
211
|
.filter((f) => f.fails.length > 0);
|
|
288
212
|
|
|
289
|
-
const
|
|
290
|
-
const
|
|
213
|
+
const featureCloseoutRequired = explicitCloseout && allDoneTasks.length > 0;
|
|
214
|
+
const featureReceipt = featureCloseoutRequired
|
|
215
|
+
? RECEIPT.checkFeatureReceipt(featureDir, runtimeContext, POLICY)
|
|
216
|
+
: null;
|
|
217
|
+
if (featureReceipt && featureReceipt.failures.length) {
|
|
218
|
+
failures.push({ taskPath: 'feature-receipt.md', fails: featureReceipt.failures });
|
|
219
|
+
}
|
|
220
|
+
const completion = featureCloseoutRequired && featureReceipt?.failures.length === 0
|
|
221
|
+
&& Object.prototype.hasOwnProperty.call(activeSpec, 'workflow_policy')
|
|
291
222
|
? POLICY.completionDecisionForSpec(activeSpec, {
|
|
292
223
|
runtimeContext,
|
|
293
|
-
executionReceipt:
|
|
294
|
-
taskContext:
|
|
224
|
+
executionReceipt: featureReceipt.body,
|
|
225
|
+
taskContext: {},
|
|
295
226
|
})
|
|
296
227
|
: null;
|
|
297
228
|
|
|
@@ -310,20 +241,19 @@ try {
|
|
|
310
241
|
fs.writeFileSync(cacheFile, JSON.stringify(cache));
|
|
311
242
|
} catch { /* fail-open */ }
|
|
312
243
|
|
|
313
|
-
if (allDoneTasks.length === 0) process.exit(0);
|
|
314
244
|
const completionBlocked = completion && completion.completion !== 'complete' && completion.completion !== 'not_applicable';
|
|
315
245
|
if (failures.length === 0 && !completionBlocked) process.exit(0);
|
|
316
246
|
|
|
317
247
|
const lines = [
|
|
318
248
|
failures.length > 0
|
|
319
|
-
? `⚠️ Completion gate: ${failures.length}
|
|
249
|
+
? `⚠️ Completion gate: ${failures.length} done task(s) lack a verification receipt.`
|
|
320
250
|
: '⚠️ Completion gate: workflow completion proof is incomplete.',
|
|
321
251
|
];
|
|
322
252
|
for (const { taskPath, fails } of failures) {
|
|
323
253
|
lines.push(`- \`${taskPath}\`: failed check(s) ${fails.join(', ')}`);
|
|
324
|
-
lines.push(
|
|
325
|
-
` Fix:
|
|
326
|
-
|
|
254
|
+
lines.push(taskPath === 'feature-receipt.md'
|
|
255
|
+
? ` Fix: run final integration proof, then write \`specs/${featureName}/feature-receipt.md\`.`
|
|
256
|
+
: ` Fix: write canonical proof to \`specs/${featureName}/receipts/${path.posix.basename(taskPath)}\`; legacy \`## Evidence\` remains read-compatible.`);
|
|
327
257
|
}
|
|
328
258
|
if (completionBlocked) {
|
|
329
259
|
lines.push(`- Completion decision unfinished: ${completion.blocker || 'required workflow proof is missing.'}`);
|
|
@@ -54,9 +54,11 @@ try {
|
|
|
54
54
|
|
|
55
55
|
let POLICY;
|
|
56
56
|
let RESOLVER;
|
|
57
|
+
let RECEIPT;
|
|
57
58
|
try {
|
|
58
59
|
POLICY = require(path.join(__dirname, '..', 'scripts', 'workflow-policy.cjs'));
|
|
59
60
|
RESOLVER = require(path.join(__dirname, '..', 'scripts', 'spec-resolver.cjs'));
|
|
61
|
+
RECEIPT = require(path.join(__dirname, '..', 'scripts', 'spec-receipt.cjs'));
|
|
60
62
|
if (typeof POLICY.flashState !== 'function') throw new Error('shared workflow policy has no flashState function');
|
|
61
63
|
} catch (error) {
|
|
62
64
|
logCrash(error);
|
|
@@ -132,6 +134,8 @@ try {
|
|
|
132
134
|
const deps = Array.isArray(task?.dependencies) ? task.dependencies : [];
|
|
133
135
|
return status === 'pending' && deps.every((dep) => taskStatusByPath.get(dep) === 'done');
|
|
134
136
|
});
|
|
137
|
+
const featureDir = path.join(specsPath, featureName);
|
|
138
|
+
const featureReceiptPresent = RECEIPT.safeRead(featureDir, 'feature-receipt.md').status === 'ok';
|
|
135
139
|
|
|
136
140
|
// ── State-change gate: only emit the full tollgate when spec state changed ──
|
|
137
141
|
// The cache is shared by installed hook copies, so its key must carry the
|
|
@@ -150,6 +154,7 @@ try {
|
|
|
150
154
|
phase,
|
|
151
155
|
done: taskCounts.done || 0,
|
|
152
156
|
total: taskEntries.length,
|
|
157
|
+
featureReceiptPresent,
|
|
153
158
|
});
|
|
154
159
|
const cacheFile = path.join(__dirname, '.logs', 'tollgate-last.txt');
|
|
155
160
|
|
|
@@ -182,8 +187,9 @@ try {
|
|
|
182
187
|
if (flashTasks.length > 0) {
|
|
183
188
|
lines.push(`- Flash verification pending: ${flashTasks.map((taskPath) => `\`${taskPath}\``).join(', ')}. A PASS proof keeps the persisted task in_progress until explicit sync-finalize.`);
|
|
184
189
|
}
|
|
185
|
-
lines.push(`- Sync \`spec.json\` + task
|
|
186
|
-
lines.push(`-
|
|
190
|
+
lines.push(`- Sync \`spec.json\` + task Markdown status after verified work; task proof belongs in \`receipts/<task-basename>.md\`.`);
|
|
191
|
+
lines.push(`- Create \`feature-receipt.md\` once after final integration proof${featureReceiptPresent ? ' (present)' : ' (not required before closeout)'}.`);
|
|
192
|
+
lines.push(`- Validate with \`node .claude/scripts/validate-spec-output.cjs specs/${featureName}\`; hooks revalidate receipt bytes but never grant approval.`);
|
|
187
193
|
lines.push('');
|
|
188
194
|
|
|
189
195
|
console.log(lines.join('\n'));
|
|
@@ -63,6 +63,11 @@
|
|
|
63
63
|
"scan-staged-secrets.cjs",
|
|
64
64
|
"workflow-policy.cjs",
|
|
65
65
|
"spec-resolver.cjs",
|
|
66
|
+
"spec-receipt.cjs",
|
|
67
|
+
"spec-ground.cjs",
|
|
68
|
+
"spec-semantic-model.cjs",
|
|
69
|
+
"spec-final-state.cjs",
|
|
70
|
+
"spec-readiness.cjs",
|
|
66
71
|
"provenance.cjs"
|
|
67
72
|
]
|
|
68
73
|
},
|
|
@@ -107,6 +112,7 @@
|
|
|
107
112
|
"hooks/completion-authority.cjs",
|
|
108
113
|
"hooks/completion-authority-check.cjs",
|
|
109
114
|
"hooks/completion-authority-state.cjs",
|
|
115
|
+
"hooks/semantic-review-authority.cjs",
|
|
110
116
|
"hooks/state.cjs",
|
|
111
117
|
"hooks/lib/color.cjs",
|
|
112
118
|
"hooks/lib/parser.cjs",
|
|
@@ -114,7 +120,9 @@
|
|
|
114
120
|
"hooks/lib/git.cjs",
|
|
115
121
|
"hooks/lib/config.cjs",
|
|
116
122
|
"hooks/lib/context.cjs",
|
|
117
|
-
"hooks/lib/detect.cjs"
|
|
123
|
+
"hooks/lib/detect.cjs",
|
|
124
|
+
"hooks/lib/privacy-command-analysis.cjs",
|
|
125
|
+
"hooks/lib/runtime-path-safety.cjs"
|
|
118
126
|
]
|
|
119
127
|
},
|
|
120
128
|
"settings": {
|
|
@@ -1,31 +1,62 @@
|
|
|
1
|
-
#
|
|
1
|
+
# Specs v2.1 state synchronization
|
|
2
2
|
|
|
3
|
-
##
|
|
3
|
+
## Authority layers
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
5
|
+
`spec.json` is machine semantic authority. Requirements, design, conditional
|
|
6
|
+
research, and conditional task Markdown are human projections. Host state may
|
|
7
|
+
record authority or integrity, but it never supplies product semantics.
|
|
8
8
|
|
|
9
|
-
|
|
9
|
+
Planning depth and assurance level remain independent. The artifact router runs
|
|
10
|
+
before discovery: None persists no spec; Compact and Full persist the bounded
|
|
11
|
+
core; research and tasks remain absent unless their semantic triggers exist.
|
|
10
12
|
|
|
11
|
-
|
|
12
|
-
Before returning control to the user or orchestrator, the agent **MUST**:
|
|
13
|
+
## Canonical lifecycle
|
|
13
14
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
3. Call `TaskUpdate` if Claude Tasks are active, setting the status to "completed" only after the physical files were updated.
|
|
15
|
+
The feature lifecycle is exactly `in_progress`, `paused`, `blocked`, or `done`.
|
|
16
|
+
Authoring state is independently `draft`, `validated`, or `absent`. Do not emit
|
|
17
|
+
legacy lifecycle aliases in a new 2.1 artifact.
|
|
18
18
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
19
|
+
Technical readiness is not lifecycle completion. `ready_for_implementation`
|
|
20
|
+
may change only through the installed machine finalization path after every
|
|
21
|
+
routed artifact, projection, semantic review, grounding check, and deterministic
|
|
22
|
+
gate passes. An author must not directly declare readiness, authority, approval,
|
|
23
|
+
review capability, execution proof, or closeout.
|
|
24
24
|
|
|
25
|
-
|
|
25
|
+
## Projection sync
|
|
26
26
|
|
|
27
|
-
|
|
27
|
+
After any semantic Markdown edit:
|
|
28
28
|
|
|
29
|
-
|
|
29
|
+
1. invoke the explicit installed machine semantic-sync step to promote `semantic_model`;
|
|
30
|
+
2. regenerate or reconcile every affected Markdown projection;
|
|
31
|
+
3. verify requirements, decisions, anchors, task registry, typed boundaries,
|
|
32
|
+
and verification refs round-trip without loss; and
|
|
33
|
+
4. rerun the applicable deterministic validation and grounding checks.
|
|
30
34
|
|
|
31
|
-
|
|
35
|
+
For Claude Code, the isolated promotion is
|
|
36
|
+
`node .claude/scripts/spec-scaffold.cjs <feature> --sync-semantic-model`.
|
|
37
|
+
|
|
38
|
+
Task Markdown exists only when typed ownership, dependency, transition, proof,
|
|
39
|
+
or parallel topology requires it. Its status and dependencies project the
|
|
40
|
+
matching `task_registry` entry. Its Verification Plan is prospective and never
|
|
41
|
+
contains an execution verdict or proof claim. Topology comes only from typed
|
|
42
|
+
`coordination.boundaries`, never from legacy trigger fields, priority markers,
|
|
43
|
+
related-file lists, or prose.
|
|
44
|
+
|
|
45
|
+
## Lifecycle updates
|
|
46
|
+
|
|
47
|
+
- Starting or continuing authored work keeps the feature `in_progress`.
|
|
48
|
+
- A deliberate resumable stop uses `paused` with the exact remaining decision.
|
|
49
|
+
- An external or semantic blocker uses `blocked` with a concrete cause.
|
|
50
|
+
- Task state changes update `task_registry` and the matching task projection
|
|
51
|
+
together, but only from observed work and verification.
|
|
52
|
+
- Feature `done` is reserved for final-state authority after implementation
|
|
53
|
+
closeout; authoring or readiness cannot produce it.
|
|
54
|
+
|
|
55
|
+
State synchronization never fabricates commands, PASS results, timestamps,
|
|
56
|
+
receipts, completed work, reviewer identity, or user approval. When execution
|
|
57
|
+
evidence is absent, preserve the unfinished state and report the missing proof.
|
|
58
|
+
Never hand-author the `semantic_model` shape or treat an unsynchronized Markdown
|
|
59
|
+
edit as machine authority.
|
|
60
|
+
|
|
61
|
+
Claude-native plan/task state may mirror physical state when available, but it
|
|
62
|
+
is never a substitute for `spec.json` or authority to advance the lifecycle.
|
|
@@ -13,14 +13,16 @@ Use the CafeKit loop: **Understand -> Plan -> Execute -> Verify -> Sync**.
|
|
|
13
13
|
## 2. Plan
|
|
14
14
|
|
|
15
15
|
- For non-trivial features, use `/specs` to create or validate the spec.
|
|
16
|
-
- For
|
|
16
|
+
- For technically ready specs, start implementation only after a new explicit Develop invocation and work one task file at a time.
|
|
17
17
|
- Extract from the active task:
|
|
18
|
-
- `
|
|
19
|
-
- `
|
|
20
|
-
- `
|
|
21
|
-
- `
|
|
22
|
-
- `
|
|
23
|
-
- `
|
|
18
|
+
- `Status`
|
|
19
|
+
- `Outcome`
|
|
20
|
+
- `Scope`
|
|
21
|
+
- `Anchors and Ownership`
|
|
22
|
+
- `Changes`
|
|
23
|
+
- `Acceptance`
|
|
24
|
+
- `Dependencies`
|
|
25
|
+
- `Verification Plan`
|
|
24
26
|
- If these are missing or too vague to verify, route back to spec correction.
|
|
25
27
|
|
|
26
28
|
## 3. Execute
|
|
@@ -32,7 +34,7 @@ Use the CafeKit loop: **Understand -> Plan -> Execute -> Verify -> Sync**.
|
|
|
32
34
|
|
|
33
35
|
## 4. Verify
|
|
34
36
|
|
|
35
|
-
- Run exact commands from `
|
|
37
|
+
- Run exact commands from `Verification Plan` first.
|
|
36
38
|
- Then run repo-level lint/test/build as needed for confidence.
|
|
37
39
|
- Use only fresh verification from the current run when claiming completion.
|
|
38
40
|
- `PRECHECK_FAIL` outranks `NO_TESTS`.
|
|
@@ -42,7 +44,13 @@ Use the CafeKit loop: **Understand -> Plan -> Execute -> Verify -> Sync**.
|
|
|
42
44
|
## 5. Sync
|
|
43
45
|
|
|
44
46
|
- Mark task state only after implementation, tests/evidence, and review pass.
|
|
45
|
-
- Write
|
|
47
|
+
- Write task proof to `receipts/<task-basename>.md`; use legacy task-local
|
|
48
|
+
`## Evidence` only when consuming an old task format.
|
|
49
|
+
- Create `feature-receipt.md` only for final integration closeout.
|
|
50
|
+
- Treat `spec.json` as machine authority; task Markdown is a human projection.
|
|
51
|
+
- Derive task ordering, ownership, proof, and parallel eligibility only from
|
|
52
|
+
typed `coordination.boundaries`, never `(P)`, `task_triggers`, `Related Files`,
|
|
53
|
+
or prose markers.
|
|
46
54
|
- Keep `spec.json.task_registry` and markdown task files aligned.
|
|
47
55
|
- Run docs checkpoint when a completed task affects public docs or architecture docs.
|
|
48
56
|
|