@haposoft/cafekit 0.18.0 → 0.18.2
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 +12 -1
- package/bin/__tests__/claude-settings.test.js +123 -0
- package/bin/__tests__/codex-ask-guard.test.js +123 -0
- package/bin/__tests__/codex-ask-projection.test.js +51 -0
- package/bin/__tests__/codex-hooks-ownership.test.js +80 -1
- package/bin/__tests__/codex-hooks.test.js +938 -237
- package/bin/__tests__/codex-native-hook-boundaries.test.js +136 -0
- package/bin/__tests__/codex-native.test.js +222 -149
- package/bin/__tests__/codex-projection-parity.test.js +314 -0
- package/bin/__tests__/codex-quiet-hooks.test.js +170 -0
- package/bin/__tests__/codex-research-projection.test.js +70 -0
- package/bin/__tests__/codex-specs-projection.test.js +76 -0
- package/bin/__tests__/develop-contract.test.js +102 -1362
- package/bin/__tests__/docs-sync-on-demand.test.js +25 -20
- package/bin/__tests__/inspect-paths.test.js +183 -0
- package/bin/__tests__/installer-safety.test.js +335 -0
- package/bin/__tests__/legacy-authority-decoupled.test.js +14 -19
- package/bin/__tests__/legacy-hooks-retired.test.js +18 -8
- package/bin/__tests__/omp-bridge.test.js +66 -12
- package/bin/__tests__/omp-hooks.test.js +11 -5
- package/bin/__tests__/orca-session.test.js +7 -0
- package/bin/__tests__/package-inventory.test.js +1 -395
- package/bin/__tests__/privacy-paths.test.js +317 -0
- package/bin/__tests__/spec-narrowing.test.js +30 -134
- package/bin/__tests__/specs-v2-execution-closeout.test.js +0 -372
- package/bin/__tests__/test-runtime-dependency-closure.cjs +1 -2
- package/bin/__tests__/usage-hook-retired.test.js +5 -3
- package/bin/__tests__/validator-grounding.test.js +17 -2929
- package/bin/install.js +25 -0
- package/bin/lib/codex-install.js +134 -12
- package/bin/lib/context.js +1 -1
- package/bin/lib/managed-writer.js +2 -3
- package/bin/phases/claude-runtime.js +91 -25
- package/bin/phases/claude-settings.js +22 -1
- package/bin/phases/codex-hooks.js +96 -33
- package/bin/phases/codex-runtime.js +45 -49
- package/bin/phases/copy-payload.js +11 -28
- package/bin/phases/omp-runtime.js +6 -3
- package/package.json +2 -3
- package/src/claude/CLAUDE.md +4 -0
- package/src/claude/agents/code-auditor.md +9 -6
- package/src/claude/agents/debugger.md +3 -9
- package/src/claude/agents/docs-keeper.md +4 -7
- package/src/claude/agents/git-ops.md +2 -4
- package/src/claude/agents/implementer.md +6 -7
- package/src/claude/agents/inspector.md +2 -0
- package/src/claude/agents/project-manager.md +2 -2
- package/src/claude/agents/spec-maker.md +4 -4
- package/src/claude/agents/test-runner.md +8 -11
- package/src/claude/agents/ui-ux-designer.md +0 -1
- package/src/claude/gitignore +3 -0
- package/src/claude/hooks/__tests__/installed-root.test.js +111 -0
- package/src/claude/hooks/__tests__/precompact.test.js +206 -10
- package/src/claude/hooks/__tests__/privacy-block.test.js +27 -0
- package/src/claude/hooks/__tests__/session-writeenv.test.js +25 -0
- package/src/claude/hooks/__tests__/spec-gate.test.js +311 -340
- package/src/claude/hooks/__tests__/spec-state-touched.test.js +230 -8
- package/src/claude/hooks/__tests__/state.test.js +207 -7
- package/src/claude/hooks/__tests__/update-check.test.js +88 -0
- package/src/claude/hooks/agent.cjs +7 -6
- package/src/claude/hooks/inspect-block.cjs +15 -63
- package/src/claude/hooks/lib/inspect-paths.cjs +304 -0
- package/src/claude/hooks/lib/parser.cjs +1 -1
- package/src/claude/hooks/lib/privacy-command-analysis.cjs +191 -17
- package/src/claude/hooks/lib/privacy-paths.cjs +313 -0
- package/src/claude/hooks/lib/project-root.cjs +15 -0
- package/src/claude/hooks/lib/rules-reservation.cjs +36 -0
- package/src/claude/hooks/lib/spec-touch.cjs +67 -0
- package/src/claude/hooks/lib/update-check.cjs +77 -0
- package/src/claude/hooks/precompact.cjs +8 -15
- package/src/claude/hooks/privacy-block.cjs +23 -205
- package/src/claude/hooks/rules.cjs +6 -28
- package/src/claude/hooks/session.cjs +34 -117
- package/src/claude/hooks/spec-gate.cjs +17 -89
- package/src/claude/hooks/spec-state.cjs +89 -96
- package/src/claude/hooks/state.cjs +96 -51
- package/src/claude/migration-manifest.json +20 -8
- package/src/claude/output-styles/coding-level-0-eli5.md +9 -9
- package/src/claude/output-styles/coding-level-1-junior.md +3 -3
- package/src/claude/output-styles/coding-level-2-mid.md +3 -3
- package/src/claude/output-styles/coding-level-3-senior.md +2 -2
- package/src/claude/output-styles/coding-level-4-lead.md +5 -5
- package/src/claude/output-styles/coding-level-5-god.md +6 -6
- package/src/claude/rules/ai-dev-rules.md +1 -2
- package/src/claude/rules/hook-protocols.md +1 -1
- package/src/claude/rules/workflow.md +1 -1
- package/src/claude/runtime.json +0 -3
- package/src/claude/scripts/generate-skill-catalog.cjs +2 -2
- package/src/claude/scripts/provenance.cjs +14 -0
- package/src/claude/scripts/spec-receipt.cjs +0 -114
- package/src/claude/scripts/spec-resolver.cjs +41 -457
- package/src/claude/scripts/workflow-policy.cjs +3 -1801
- package/src/claude/settings/settings.json +1 -1
- package/src/claude/skills/agent-browser/SKILL.md +1 -1
- package/src/claude/skills/ai-multimodal/scripts/check_setup.py +8 -68
- package/src/claude/skills/ai-multimodal/scripts/gemini_batch_process.py +6 -28
- package/src/claude/skills/chrome-devtools/SKILL.md +14 -31
- package/src/claude/skills/code-review/SKILL.md +25 -108
- package/src/claude/skills/code-review/references/spec-compliance-review.md +4 -7
- package/src/claude/skills/develop/SKILL.md +22 -70
- package/src/claude/skills/develop/references/subagent-patterns.md +0 -6
- package/src/claude/skills/git/SKILL.md +1 -1
- package/src/claude/skills/git/references/worktree-blueprint.md +1 -1
- package/src/claude/skills/research/SKILL.md +24 -55
- package/src/claude/skills/sync/SKILL.md +12 -34
- package/src/claude/skills/sync/references/rebind-and-audit.md +2 -2
- package/src/claude/skills/test/SKILL.md +39 -123
- package/src/claude/skills/test/references/execution-strategy.md +11 -22
- package/src/claude/skills/ui-ux/SKILL.md +1 -1
- package/src/claude/skills/ui-ux-pro-max/SKILL.md +24 -79
- package/src/claude/skills/web-testing/SKILL.md +2 -20
- package/src/codex/AGENTS.md +8 -0
- package/src/codex/hooks/ask-guard.cjs +54 -0
- package/src/codex/hooks/inspect-block.cjs +11 -7
- package/src/codex/hooks/lib/inspect-paths.cjs +1 -199
- package/src/codex/hooks/lib/privacy-paths.cjs +1 -0
- package/src/codex/hooks/lib/privacy-state.cjs +98 -16
- package/src/codex/hooks/lib/rules-reservation.cjs +38 -0
- package/src/codex/hooks/lib/secret-keywords.cjs +1 -0
- package/src/codex/hooks/lib/spec-receipt.cjs +2 -30
- package/src/codex/hooks/lib/spec-session-touch.cjs +100 -0
- package/src/codex/hooks/lib/spec-touch.cjs +3 -0
- package/src/codex/hooks/lib/spec-utils.cjs +6 -45
- package/src/codex/hooks/lib/update-check.cjs +1 -0
- package/src/codex/hooks/precompact.cjs +39 -0
- package/src/codex/hooks/privacy-approval.cjs +28 -17
- package/src/codex/hooks/privacy-block.cjs +9 -215
- package/src/codex/hooks/rules.cjs +4 -30
- package/src/codex/hooks/secret-output-guardrail.cjs +1 -1
- package/src/codex/hooks/session.cjs +47 -11
- package/src/codex/hooks/spec-gate.cjs +32 -74
- package/src/codex/hooks/spec-state.cjs +68 -46
- package/src/codex/hooks/state.cjs +25 -4
- package/src/codex/hooks.json +39 -7
- package/src/codex/rules/hook-protocols.md +13 -6
- package/src/common/AGENTS.md +4 -10
- package/src/omp/extensions/cafekit-bridge.mjs +12 -4
- package/src/omp/hooks/privacy-block.cjs +24 -206
- package/bin/__tests__/change-firewall.semantic-firewall.test.js +0 -2133
- package/bin/__tests__/finalizer-pass-gate.semantic-firewall.test.js +0 -342
- package/bin/__tests__/release-preflight.semantic-firewall.test.js +0 -586
- package/bin/__tests__/review-receipt-c2.semantic-firewall.test.js +0 -378
- package/bin/__tests__/specs-v2-policy-and-scaffold.test.js +0 -665
- package/bin/__tests__/specs-v2-validator-grounder-contract.test.js +0 -1132
- package/src/claude/hooks/lib/context.cjs +0 -527
- package/src/claude/hooks/lib/detect.cjs +0 -474
- package/src/claude/scripts/change-firewall.cjs +0 -1748
- package/src/claude/scripts/spec-authoring-digest.cjs +0 -151
- package/src/claude/scripts/spec-authoring-validation.cjs +0 -126
- package/src/claude/scripts/spec-final-state.cjs +0 -290
- package/src/claude/scripts/spec-ground.cjs +0 -650
- package/src/claude/scripts/spec-readiness.cjs +0 -425
- package/src/claude/scripts/spec-scaffold.cjs +0 -2317
- package/src/claude/scripts/spec-semantic-model.cjs +0 -396
- package/src/claude/scripts/validate-spec-output.cjs +0 -3914
- package/src/claude/skills/code-review/references/verification-gate.md +0 -41
- package/src/claude/skills/specs/templates/design.md +0 -82
- package/src/claude/skills/specs/templates/requirements-init.md +0 -8
- package/src/claude/skills/specs/templates/requirements.md +0 -42
- package/src/claude/skills/specs/templates/spec-state.json +0 -56
- package/src/claude/skills/specs/templates/task.md +0 -47
- package/src/codex/rules/state-sync.md +0 -60
|
@@ -11,11 +11,24 @@
|
|
|
11
11
|
* Exit: 0 always (fail-open)
|
|
12
12
|
*/
|
|
13
13
|
|
|
14
|
+
function logCrash(error) {
|
|
15
|
+
try {
|
|
16
|
+
const fs = require('fs'), path = require('path');
|
|
17
|
+
const dir = require('./lib/hook-state-dir.cjs').hookStateDir();
|
|
18
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
19
|
+
fs.appendFileSync(path.join(dir, 'hook-log.jsonl'),
|
|
20
|
+
JSON.stringify({ ts: new Date().toISOString(), hook: 'session', status: 'crash', error: error.message }) + '\n');
|
|
21
|
+
} catch (_) {}
|
|
22
|
+
}
|
|
23
|
+
|
|
14
24
|
try {
|
|
15
25
|
const fs = require('fs');
|
|
16
26
|
const path = require('path');
|
|
17
27
|
const { runtimeDirName, runtimeDir, runtimePath } = require('./lib/runtime-dir.cjs');
|
|
18
28
|
const os = require('os');
|
|
29
|
+
const { projectRoot } = require('./lib/project-root.cjs');
|
|
30
|
+
const { checkCafeKitUpdate } = require('./lib/update-check.cjs');
|
|
31
|
+
const { clearSessionReservation } = require('./lib/rules-reservation.cjs');
|
|
19
32
|
const { execSync } = require('child_process');
|
|
20
33
|
|
|
21
34
|
// ── Utilities ─────────────────────────────────────────────────────────────
|
|
@@ -41,21 +54,6 @@ try {
|
|
|
41
54
|
} catch { /* fail-open */ }
|
|
42
55
|
}
|
|
43
56
|
|
|
44
|
-
function projectRoot() {
|
|
45
|
-
const configured = typeof process.env.CLAUDE_PROJECT_DIR === 'string'
|
|
46
|
-
? process.env.CLAUDE_PROJECT_DIR.trim()
|
|
47
|
-
: '';
|
|
48
|
-
if (configured) {
|
|
49
|
-
try { return fs.realpathSync(path.resolve(configured)); } catch { /* continue */ }
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
const installedRoot = path.resolve(__dirname, '..', '..');
|
|
53
|
-
const installedHook = runtimePath(installedRoot, 'hooks', path.basename(__filename));
|
|
54
|
-
if (fs.existsSync(installedHook)) return installedRoot;
|
|
55
|
-
|
|
56
|
-
try { return fs.realpathSync(process.cwd()); } catch { return path.resolve(process.cwd()); }
|
|
57
|
-
}
|
|
58
|
-
|
|
59
57
|
/** Read <runtime>/runtime.json config */
|
|
60
58
|
function readRuntime(cwd) {
|
|
61
59
|
try {
|
|
@@ -104,94 +102,6 @@ try {
|
|
|
104
102
|
return '';
|
|
105
103
|
}
|
|
106
104
|
|
|
107
|
-
// ── CafeKit Update Check ──────────────────────────────────────────────────
|
|
108
|
-
|
|
109
|
-
/**
|
|
110
|
-
* Fetch the latest published version of @haposoft/cafekit from the npm registry.
|
|
111
|
-
* Returns a string (e.g. "0.11.2") or null on any failure. Timeout: 3 s.
|
|
112
|
-
*/
|
|
113
|
-
function fetchLatestVersion() {
|
|
114
|
-
try {
|
|
115
|
-
const https = require('https');
|
|
116
|
-
return new Promise((resolve) => {
|
|
117
|
-
const req = https.get(
|
|
118
|
-
'https://registry.npmjs.org/@haposoft/cafekit/latest',
|
|
119
|
-
{ timeout: 3000, headers: { Accept: 'application/json' } },
|
|
120
|
-
(res) => {
|
|
121
|
-
let body = '';
|
|
122
|
-
res.on('data', (chunk) => { body += chunk; });
|
|
123
|
-
res.on('end', () => {
|
|
124
|
-
try { resolve(JSON.parse(body).version || null); }
|
|
125
|
-
catch { resolve(null); }
|
|
126
|
-
});
|
|
127
|
-
}
|
|
128
|
-
);
|
|
129
|
-
req.on('error', () => resolve(null));
|
|
130
|
-
req.on('timeout', () => { req.destroy(); resolve(null); });
|
|
131
|
-
});
|
|
132
|
-
} catch { return Promise.resolve(null); }
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
/** Simple semver compare: returns true when b > a. */
|
|
136
|
-
function isNewer(a, b) {
|
|
137
|
-
const n = (v) => String(v).split('.').map((x) => parseInt(x, 10) || 0);
|
|
138
|
-
const [aN, bN] = [n(a), n(b)];
|
|
139
|
-
for (let i = 0; i < 3; i++) {
|
|
140
|
-
if (bN[i] > aN[i]) return true;
|
|
141
|
-
if (bN[i] < aN[i]) return false;
|
|
142
|
-
}
|
|
143
|
-
return false;
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
/**
|
|
147
|
-
* Read the installed CafeKit version from <runtime>/cafekit.json.
|
|
148
|
-
* Returns null if not found.
|
|
149
|
-
*/
|
|
150
|
-
function getInstalledVersion(cwd) {
|
|
151
|
-
try {
|
|
152
|
-
const p = runtimePath(cwd, 'cafekit.json');
|
|
153
|
-
if (!fs.existsSync(p)) return null;
|
|
154
|
-
const meta = JSON.parse(fs.readFileSync(p, 'utf8'));
|
|
155
|
-
return typeof meta.version === 'string' ? meta.version : null;
|
|
156
|
-
} catch { return null; }
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
/**
|
|
160
|
-
* Check for a newer CafeKit version and return an update notice string,
|
|
161
|
-
* or an empty string if already up to date or check fails.
|
|
162
|
-
* Result is cached in <runtime>/.cafekit-update-cache.json for 12 hours.
|
|
163
|
-
*/
|
|
164
|
-
async function checkCafeKitUpdate(cwd) {
|
|
165
|
-
const installed = getInstalledVersion(cwd);
|
|
166
|
-
if (!installed) return '';
|
|
167
|
-
|
|
168
|
-
const CACHE_TTL = 12 * 60 * 60 * 1000; // 12 hours
|
|
169
|
-
const cachePath = runtimePath(cwd, '.cafekit-update-cache.json');
|
|
170
|
-
|
|
171
|
-
// Return cached result if still fresh.
|
|
172
|
-
try {
|
|
173
|
-
if (fs.existsSync(cachePath)) {
|
|
174
|
-
const cache = JSON.parse(fs.readFileSync(cachePath, 'utf8'));
|
|
175
|
-
if (Date.now() - (cache.ts || 0) < CACHE_TTL) {
|
|
176
|
-
return cache.notice || '';
|
|
177
|
-
}
|
|
178
|
-
}
|
|
179
|
-
} catch { /* stale or corrupt — refetch */ }
|
|
180
|
-
|
|
181
|
-
// Fetch from npm registry.
|
|
182
|
-
const latest = await fetchLatestVersion();
|
|
183
|
-
const notice = (latest && isNewer(installed, latest))
|
|
184
|
-
? `\n⚡ CafeKit update available: ${installed} → ${latest}\n Run: npx @haposoft/cafekit\n`
|
|
185
|
-
: '';
|
|
186
|
-
|
|
187
|
-
// Persist cache (fail-open if write fails).
|
|
188
|
-
try {
|
|
189
|
-
fs.writeFileSync(cachePath, JSON.stringify({ ts: Date.now(), notice }), 'utf8');
|
|
190
|
-
} catch { /* fail-open */ }
|
|
191
|
-
|
|
192
|
-
return notice;
|
|
193
|
-
}
|
|
194
|
-
|
|
195
105
|
// ── Main ──────────────────────────────────────────────────────────────────
|
|
196
106
|
|
|
197
107
|
(async () => {
|
|
@@ -199,12 +109,13 @@ try {
|
|
|
199
109
|
const { normalizeHookPayload } = require('./lib/hook-payload.cjs');
|
|
200
110
|
const payload = stdin ? normalizeHookPayload(JSON.parse(stdin)) : {};
|
|
201
111
|
const source = payload.source || 'unknown';
|
|
112
|
+
if (source === 'compact') clearSessionReservation(payload.session_id);
|
|
202
113
|
const envFile = process.env.CLAUDE_ENV_FILE;
|
|
203
|
-
const cwd = projectRoot();
|
|
114
|
+
const cwd = projectRoot(payload);
|
|
204
115
|
const runtime = readRuntime(cwd);
|
|
205
116
|
|
|
206
117
|
// Check CafeKit update in parallel with project detection (async, fail-open).
|
|
207
|
-
const updateCheckPromise = checkCafeKitUpdate(cwd);
|
|
118
|
+
const updateCheckPromise = checkCafeKitUpdate(runtimeDir(cwd)).catch(() => '');
|
|
208
119
|
|
|
209
120
|
// Project detection
|
|
210
121
|
const projectType = runtime.project?.type !== 'auto'
|
|
@@ -221,7 +132,6 @@ try {
|
|
|
221
132
|
|
|
222
133
|
// Static environment
|
|
223
134
|
const gitBranch = run('git branch --show-current', '', cwd);
|
|
224
|
-
const gitUrl = run('git remote get-url origin', '', cwd);
|
|
225
135
|
const gitRoot = run('git rev-parse --show-toplevel', '', cwd);
|
|
226
136
|
const nodeVer = process.version;
|
|
227
137
|
const pythonVer = run('python3 --version', '', cwd) || run('python --version', '', cwd);
|
|
@@ -235,7 +145,6 @@ try {
|
|
|
235
145
|
writeEnv(envFile, 'PACKAGE_MANAGER', packageManager);
|
|
236
146
|
writeEnv(envFile, 'FRAMEWORK', framework);
|
|
237
147
|
writeEnv(envFile, 'GIT_BRANCH', gitBranch);
|
|
238
|
-
writeEnv(envFile, 'GIT_URL', gitUrl);
|
|
239
148
|
writeEnv(envFile, 'GIT_ROOT', gitRoot);
|
|
240
149
|
writeEnv(envFile, 'NODE_VERSION', nodeVer);
|
|
241
150
|
writeEnv(envFile, 'PYTHON_VERSION', pythonVer);
|
|
@@ -262,6 +171,14 @@ try {
|
|
|
262
171
|
|
|
263
172
|
console.log(`Session ${source}. ${parts.length ? parts.join(' | ') : 'No project info detected.'}${updateNotice}`);
|
|
264
173
|
|
|
174
|
+
if (typeof payload.cwd === 'string' && payload.cwd) {
|
|
175
|
+
let sessionCwd = path.resolve(payload.cwd);
|
|
176
|
+
try { sessionCwd = fs.realpathSync(sessionCwd); } catch { /* lexical fallback */ }
|
|
177
|
+
if (sessionCwd !== cwd) {
|
|
178
|
+
console.log(`CafeKit project root: ${cwd} (session working directory: ${sessionCwd}).`);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
265
182
|
// Compact warning — context compaction can lose pending approval state
|
|
266
183
|
if (source === 'compact') {
|
|
267
184
|
console.log('\n🚨 SESSION COMPRESSED — VERIFY PENDING AUTHORIZATIONS:');
|
|
@@ -274,8 +191,11 @@ try {
|
|
|
274
191
|
// bearings, and nothing more.
|
|
275
192
|
try {
|
|
276
193
|
const fs2 = require('fs'), p2 = require('path');
|
|
277
|
-
|
|
194
|
+
if (typeof payload.session_id !== 'string' || !payload.session_id.trim()) process.exit(0);
|
|
195
|
+
const key = require('crypto').createHash('sha256').update(payload.session_id).digest('hex');
|
|
196
|
+
const file = p2.join(require('./lib/hook-state-dir.cjs').hookStateDir(), `compact-recovery-${key}.json`);
|
|
278
197
|
const record = JSON.parse(fs2.readFileSync(file, 'utf8'));
|
|
198
|
+
if (record.session_id !== payload.session_id) process.exit(0);
|
|
279
199
|
const parts = [];
|
|
280
200
|
if (record.branch) parts.push(`branch ${record.branch}`);
|
|
281
201
|
if (record.head) parts.push(`HEAD ${record.head}`);
|
|
@@ -297,15 +217,12 @@ try {
|
|
|
297
217
|
}
|
|
298
218
|
|
|
299
219
|
process.exit(0);
|
|
300
|
-
})()
|
|
220
|
+
})().catch((error) => {
|
|
221
|
+
logCrash(error);
|
|
222
|
+
process.exit(0);
|
|
223
|
+
});
|
|
301
224
|
|
|
302
|
-
} catch (
|
|
303
|
-
|
|
304
|
-
const fs = require('fs'), p = require('path');
|
|
305
|
-
const d = require('./lib/hook-state-dir.cjs').hookStateDir();
|
|
306
|
-
if (!fs.existsSync(d)) fs.mkdirSync(d, { recursive: true });
|
|
307
|
-
fs.appendFileSync(p.join(d, 'hook-log.jsonl'),
|
|
308
|
-
JSON.stringify({ ts: new Date().toISOString(), hook: 'session', status: 'crash', error: e.message }) + '\n');
|
|
309
|
-
} catch (_) {}
|
|
225
|
+
} catch (error) {
|
|
226
|
+
logCrash(error);
|
|
310
227
|
process.exit(0);
|
|
311
228
|
}
|
|
@@ -16,32 +16,7 @@
|
|
|
16
16
|
const fs = require('fs');
|
|
17
17
|
const path = require('path');
|
|
18
18
|
const { runtimeDirName, runtimeDir, runtimePath } = require('./lib/runtime-dir.cjs');
|
|
19
|
-
|
|
20
|
-
function projectRoot(payload = {}) {
|
|
21
|
-
const configured = typeof process.env.CLAUDE_PROJECT_DIR === 'string'
|
|
22
|
-
? process.env.CLAUDE_PROJECT_DIR.trim()
|
|
23
|
-
: '';
|
|
24
|
-
if (configured) {
|
|
25
|
-
try { return fs.realpathSync(path.resolve(configured)); } catch { /* continue */ }
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
const installedRoot = path.resolve(__dirname, '..', '..');
|
|
29
|
-
const installedHook = runtimePath(installedRoot, 'hooks', path.basename(__filename));
|
|
30
|
-
if (fs.existsSync(installedHook)) return installedRoot;
|
|
31
|
-
|
|
32
|
-
const sourceFixture = typeof process.env.PROJECT_ROOT === 'string'
|
|
33
|
-
? process.env.PROJECT_ROOT.trim()
|
|
34
|
-
: '';
|
|
35
|
-
if (sourceFixture) {
|
|
36
|
-
try { return fs.realpathSync(path.resolve(sourceFixture)); } catch { /* continue */ }
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
const legacy = typeof payload.cwd === 'string' ? payload.cwd.trim() : '';
|
|
40
|
-
if (legacy) {
|
|
41
|
-
try { return fs.realpathSync(path.resolve(legacy)); } catch { /* continue */ }
|
|
42
|
-
}
|
|
43
|
-
try { return fs.realpathSync(process.cwd()); } catch { return path.resolve(process.cwd()); }
|
|
44
|
-
}
|
|
19
|
+
const { projectRoot } = require('./lib/project-root.cjs');
|
|
45
20
|
|
|
46
21
|
function logCrash(error) {
|
|
47
22
|
try {
|
|
@@ -68,6 +43,7 @@ function sessionIdentity(payload) {
|
|
|
68
43
|
return candidates.find((value) => typeof value === 'string' && value.trim() !== '') || null;
|
|
69
44
|
}
|
|
70
45
|
|
|
46
|
+
let hasDoneTasks = false;
|
|
71
47
|
try {
|
|
72
48
|
const policyPath = path.join(__dirname, '..', 'scripts', 'workflow-policy.cjs');
|
|
73
49
|
|
|
@@ -97,8 +73,7 @@ try {
|
|
|
97
73
|
RESOLVER = require(path.join(__dirname, '..', 'scripts', 'spec-resolver.cjs'));
|
|
98
74
|
RECEIPT = require(path.join(__dirname, '..', 'scripts', 'spec-receipt.cjs'));
|
|
99
75
|
if (typeof POLICY.validateCanonicalReceipt !== 'function'
|
|
100
|
-
|| typeof
|
|
101
|
-
|| typeof RECEIPT.checkTaskReceipt !== 'function'
|
|
76
|
+
|| typeof RECEIPT.checkWorkflowTaskReceipt !== 'function'
|
|
102
77
|
|| typeof RECEIPT.checkWorkflowReceiptSet !== 'function'
|
|
103
78
|
|| typeof RESOLVER.resolveWorkflowCandidate !== 'function') {
|
|
104
79
|
throw new Error('shared workflow policy lacks completion authority functions');
|
|
@@ -133,6 +108,8 @@ try {
|
|
|
133
108
|
}
|
|
134
109
|
if (!resolved) process.exit(0);
|
|
135
110
|
if (resolved.layoutKind === 'process-v3-completed-set') {
|
|
111
|
+
hasDoneTasks = resolved.candidates.some(candidate =>
|
|
112
|
+
Object.values(candidate.taskRegistry || {}).some(task => task?.status === 'done'));
|
|
136
113
|
const configuredGate = runtime.spec?.completion_gate;
|
|
137
114
|
if (configuredGate !== undefined && configuredGate !== true) {
|
|
138
115
|
emitBlock('Completion gate: runtime.spec.completion_gate is a worker-writable flag, not an authorization; no completion-gate bypass is supported. Remove the flag and satisfy the gate.');
|
|
@@ -164,7 +141,7 @@ try {
|
|
|
164
141
|
if (resolved.error === 'invalid_specs') {
|
|
165
142
|
process.stdout.write(JSON.stringify({
|
|
166
143
|
decision: 'block',
|
|
167
|
-
reason: `Completion gate: invalid
|
|
144
|
+
reason: `Completion gate: invalid workflow packet detected (${resolved.candidates.join(', ')}): ${resolved.reason}. Fix the packet's plan.md or task files before completing tasks.`,
|
|
168
145
|
}) + '\n');
|
|
169
146
|
process.exit(0);
|
|
170
147
|
}
|
|
@@ -177,22 +154,13 @@ try {
|
|
|
177
154
|
}
|
|
178
155
|
if (resolved.error) process.exit(0);
|
|
179
156
|
|
|
180
|
-
const activeSpec = resolved.spec || {};
|
|
181
|
-
const processWorkflow = resolved.layoutKind === 'process-v3';
|
|
182
157
|
const featureName = resolved.featureName;
|
|
183
158
|
const specsPath = resolved.specsDir;
|
|
184
|
-
|
|
185
|
-
const explicitCloseout = ['done', 'completed', 'complete'].includes(activeSpec.status)
|
|
186
|
-
|| ['closeout', 'completion', 'completed', 'complete'].includes(lifecyclePhase);
|
|
187
|
-
// Legacy 2.1 closeout is retired; a 2.1 packet mid-execution keeps the ordinary receipt checks.
|
|
188
|
-
if (!processWorkflow && activeSpec.schema_version === '2.1' && explicitCloseout) {
|
|
189
|
-
emitBlock('Completion gate: Legacy spec.json closeout is no longer supported; move the packet to plan.md with flat task files');
|
|
190
|
-
process.exit(0);
|
|
191
|
-
}
|
|
159
|
+
hasDoneTasks = Object.values(resolved.taskRegistry || {}).some(task => task?.status === 'done');
|
|
192
160
|
const runtimeContext = POLICY.deriveRuntimeContext({
|
|
193
161
|
projectRoot: baseDir,
|
|
194
162
|
specsRoot: specsPath,
|
|
195
|
-
specFile: resolved.stateFile
|
|
163
|
+
specFile: resolved.stateFile,
|
|
196
164
|
featureName,
|
|
197
165
|
runtimeSession: sessionIdentity(payload),
|
|
198
166
|
});
|
|
@@ -202,7 +170,7 @@ try {
|
|
|
202
170
|
emitBlock('Completion gate: runtime.spec.completion_gate is a worker-writable flag, not an authorization; no completion-gate bypass is supported. Remove the flag and satisfy the gate.');
|
|
203
171
|
process.exit(0);
|
|
204
172
|
}
|
|
205
|
-
const taskRegistry = resolved.taskRegistry ||
|
|
173
|
+
const taskRegistry = resolved.taskRegistry || {};
|
|
206
174
|
const cacheFile = path.join(require('./lib/hook-state-dir.cjs').hookStateDir(), 'spec-gate-last.json');
|
|
207
175
|
const cacheExists = fs.existsSync(cacheFile);
|
|
208
176
|
let cache = {};
|
|
@@ -215,17 +183,6 @@ try {
|
|
|
215
183
|
currentStatuses[tp] = task?.status || 'pending';
|
|
216
184
|
}
|
|
217
185
|
|
|
218
|
-
const staleFlashTasks = processWorkflow ? [] : Object.entries(taskRegistry)
|
|
219
|
-
.filter(([, task]) => POLICY.isStaleFlashDone(task))
|
|
220
|
-
.map(([taskPath]) => taskPath);
|
|
221
|
-
if (staleFlashTasks.length > 0) {
|
|
222
|
-
process.stdout.write(JSON.stringify({
|
|
223
|
-
decision: 'block',
|
|
224
|
-
reason: `Completion gate: ${staleFlashTasks.length} task(s) marked done with FLASH_UNVERIFIED (${staleFlashTasks.join(', ')}). Run /cf:test for exact proof, then use explicit sync-finalize.`
|
|
225
|
-
}) + '\n');
|
|
226
|
-
process.exit(0);
|
|
227
|
-
}
|
|
228
|
-
|
|
229
186
|
// Cache hardening: every Stop re-reads and re-checks the receipt of every
|
|
230
187
|
// task currently marked done, so a cached PASS cannot hide later mutations
|
|
231
188
|
// (deletion, placeholder, changed Verification/Command/Exit, missing
|
|
@@ -241,29 +198,11 @@ try {
|
|
|
241
198
|
const featureDir = path.join(specsPath, featureName);
|
|
242
199
|
const failures = allDoneTasks
|
|
243
200
|
.map((taskPath) => {
|
|
244
|
-
const proof =
|
|
245
|
-
? RECEIPT.checkWorkflowTaskReceipt(featureDir, taskPath, runtimeContext, POLICY)
|
|
246
|
-
: RECEIPT.checkTaskReceipt(featureDir, taskPath, taskRegistry[taskPath], runtimeContext, POLICY);
|
|
201
|
+
const proof = RECEIPT.checkWorkflowTaskReceipt(featureDir, taskPath, runtimeContext, POLICY);
|
|
247
202
|
return { taskPath, fails: proof.failures, body: proof.body };
|
|
248
203
|
})
|
|
249
204
|
.filter((f) => f.fails.length > 0);
|
|
250
205
|
|
|
251
|
-
const featureCloseoutRequired = !processWorkflow && explicitCloseout && allDoneTasks.length > 0;
|
|
252
|
-
const featureReceipt = featureCloseoutRequired
|
|
253
|
-
? RECEIPT.checkFeatureReceipt(featureDir, runtimeContext, POLICY)
|
|
254
|
-
: null;
|
|
255
|
-
if (featureReceipt && featureReceipt.failures.length) {
|
|
256
|
-
failures.push({ taskPath: 'feature-receipt.md', fails: featureReceipt.failures });
|
|
257
|
-
}
|
|
258
|
-
const completion = featureCloseoutRequired && featureReceipt?.failures.length === 0
|
|
259
|
-
&& Object.prototype.hasOwnProperty.call(activeSpec, 'workflow_policy')
|
|
260
|
-
? POLICY.completionDecisionForSpec(activeSpec, {
|
|
261
|
-
runtimeContext,
|
|
262
|
-
executionReceipt: featureReceipt.body,
|
|
263
|
-
taskContext: {},
|
|
264
|
-
})
|
|
265
|
-
: null;
|
|
266
|
-
|
|
267
206
|
// Always persist status transitions, including done → pending. Keep
|
|
268
207
|
// failing done tasks at their previous cached status so a stale valid
|
|
269
208
|
// entry cannot mask a mutated receipt on the next Stop; with
|
|
@@ -279,32 +218,21 @@ try {
|
|
|
279
218
|
fs.writeFileSync(cacheFile, JSON.stringify(cache));
|
|
280
219
|
} catch { /* fail-open */ }
|
|
281
220
|
|
|
282
|
-
|
|
283
|
-
if (failures.length === 0 && !completionBlocked) process.exit(0);
|
|
221
|
+
if (failures.length === 0) process.exit(0);
|
|
284
222
|
|
|
285
|
-
const lines = [
|
|
286
|
-
failures.length > 0
|
|
287
|
-
? `⚠️ Completion gate: ${failures.length} done task(s) lack a verification receipt.`
|
|
288
|
-
: '⚠️ Completion gate: workflow completion proof is incomplete.',
|
|
289
|
-
];
|
|
223
|
+
const lines = [`⚠️ Completion gate: ${failures.length} done task(s) lack a verification receipt.`];
|
|
290
224
|
for (const { taskPath, fails, body } of failures) {
|
|
291
225
|
lines.push(`- \`${taskPath}\`: failed check(s) ${fails.join(', ')}`);
|
|
292
|
-
lines.push(taskPath === '
|
|
293
|
-
? ` Fix: run final integration proof, then write \`specs/${featureName}/feature-receipt.md\`.`
|
|
294
|
-
: processWorkflow
|
|
295
|
-
? ` Fix in \`specs/${featureName}/${path.posix.basename(taskPath)}\`: ${(typeof RECEIPT.receiptFixHint === 'function' && RECEIPT.receiptFixHint(fails, body)) || 'write a runtime-bound `## Receipt` with command output'}.`
|
|
296
|
-
: ` Fix: write canonical proof to \`specs/${featureName}/receipts/${path.posix.basename(taskPath)}\`; legacy \`## Evidence\` remains read-compatible.`);
|
|
297
|
-
}
|
|
298
|
-
if (completionBlocked) {
|
|
299
|
-
lines.push(`- Completion decision unfinished: ${completion.blocker || 'required workflow proof is missing.'}`);
|
|
300
|
-
if (Array.isArray(completion.missingProof) && completion.missingProof.length > 0) {
|
|
301
|
-
lines.push(` Missing proof: ${completion.missingProof.join(', ')}`);
|
|
302
|
-
}
|
|
226
|
+
lines.push(` Fix in \`specs/${featureName}/${path.posix.basename(taskPath)}\`: ${(typeof RECEIPT.receiptFixHint === 'function' && RECEIPT.receiptFixHint(fails, body)) || 'write a runtime-bound `## Receipt` with command output'}.`);
|
|
303
227
|
}
|
|
304
228
|
process.stdout.write(JSON.stringify({ decision: 'block', reason: lines.slice(0, 8).join('\n') }) + '\n');
|
|
305
229
|
process.exit(0);
|
|
306
230
|
|
|
307
231
|
} catch (e) {
|
|
232
|
+
if (e.code === 'no_git_history') {
|
|
233
|
+
if (hasDoneTasks) emitBlock('Completion gate: repo has no commits yet; create the first commit, then record Base/Head after it.');
|
|
234
|
+
process.exit(0);
|
|
235
|
+
}
|
|
308
236
|
logCrash(e);
|
|
309
237
|
emitBlock(`Completion gate controlled failure: ${e.message}. Completion is blocked until the hook is repaired.`);
|
|
310
238
|
process.exit(0);
|