@haposoft/cafekit 0.18.1 → 0.18.3
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__/adv-skill.test.js +371 -0
- package/bin/__tests__/agent-authority.test.js +38 -0
- package/bin/__tests__/agent-instructions.test.js +54 -0
- package/bin/__tests__/claude-plugin-build.test.js +151 -0
- 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 +812 -12
- package/bin/__tests__/codex-implicit-policy.test.js +160 -0
- package/bin/__tests__/codex-native-hook-boundaries.test.js +136 -0
- package/bin/__tests__/codex-native.test.js +198 -58
- package/bin/__tests__/codex-plugin-build.test.js +230 -0
- package/bin/__tests__/codex-plugin-context.test.js +135 -0
- package/bin/__tests__/codex-plugin-hooks.test.js +220 -0
- 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-runtime-layout.test.js +81 -0
- package/bin/__tests__/codex-setup-agents.test.js +216 -0
- package/bin/__tests__/codex-specs-projection.test.js +76 -0
- package/bin/__tests__/develop-contract.test.js +1 -1
- 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-hooks-retired.test.js +81 -21
- package/bin/__tests__/migrate-apply.test.js +373 -0
- package/bin/__tests__/migrate-cli.test.js +152 -0
- package/bin/__tests__/migrate-merged.test.js +269 -0
- package/bin/__tests__/migrate-plan.test.js +271 -0
- package/bin/__tests__/omp-bridge.test.js +147 -17
- package/bin/__tests__/omp-hooks.test.js +35 -5
- package/bin/__tests__/orca-session.test.js +7 -0
- package/bin/__tests__/plugin-safety-keys.test.js +121 -0
- package/bin/__tests__/privacy-paths.test.js +501 -0
- package/bin/__tests__/usage-hook-retired.test.js +29 -9
- package/bin/__tests__/vibe-run.test.js +215 -0
- package/bin/__tests__/vibe-skill.test.js +549 -0
- package/bin/install.js +124 -1
- package/bin/lib/backup.js +43 -24
- package/bin/lib/codex-install.js +141 -12
- package/bin/lib/context.js +6 -1
- package/bin/lib/lock.js +98 -18
- package/bin/lib/managed-writer.js +16 -10
- package/bin/lib/migrate-apply.js +202 -0
- package/bin/lib/migrate-merged.js +342 -0
- package/bin/lib/migrate-plan.js +419 -0
- package/bin/lib/plugin-inventory.js +33 -0
- package/bin/phases/claude-runtime.js +93 -25
- package/bin/phases/claude-settings.js +22 -1
- package/bin/phases/codex-hooks.js +134 -33
- package/bin/phases/codex-runtime.js +110 -85
- package/bin/phases/copy-payload.js +50 -4
- package/bin/phases/omp-runtime.js +4 -26
- package/bin/phases/root-config.js +5 -2
- package/package.json +1 -1
- package/src/claude/CLAUDE.md +5 -1
- package/src/claude/agents/advisor.md +55 -0
- package/src/claude/agents/debugger.md +2 -3
- package/src/claude/agents/deployer.md +2 -1
- package/src/claude/agents/docs-keeper.md +12 -0
- package/src/claude/agents/inspector.md +1 -1
- package/src/claude/agents/project-manager.md +2 -2
- package/src/claude/agents/researcher.md +0 -1
- package/src/claude/agents/spec-maker.md +4 -1
- package/src/claude/gitignore +3 -0
- package/src/claude/hooks/__tests__/installed-root.test.js +111 -0
- package/src/claude/hooks/__tests__/plugin-context.test.js +96 -0
- package/src/claude/hooks/__tests__/plugin-options.test.js +141 -0
- package/src/claude/hooks/__tests__/plugin-state-isolation.test.js +125 -0
- package/src/claude/hooks/__tests__/precompact.test.js +213 -11
- package/src/claude/hooks/__tests__/privacy-block.test.js +60 -9
- package/src/claude/hooks/__tests__/session-writeenv.test.js +25 -0
- package/src/claude/hooks/__tests__/setup-statusline.test.js +179 -0
- package/src/claude/hooks/__tests__/spec-gate.test.js +227 -11
- package/src/claude/hooks/__tests__/spec-state-touched.test.js +204 -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 +11 -7
- package/src/claude/hooks/inspect-block.cjs +20 -66
- package/src/claude/hooks/lib/hook-state-dir.cjs +19 -1
- package/src/claude/hooks/lib/inspect-paths.cjs +304 -0
- package/src/claude/hooks/lib/parser.cjs +1 -1
- package/src/claude/hooks/lib/plugin-options.cjs +47 -0
- package/src/claude/hooks/lib/privacy-command-analysis.cjs +317 -24
- package/src/claude/hooks/lib/privacy-paths.cjs +673 -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 +80 -0
- package/src/claude/hooks/lib/update-check.cjs +77 -0
- package/src/claude/hooks/plugin-context.cjs +80 -0
- package/src/claude/hooks/precompact.cjs +8 -15
- package/src/claude/hooks/privacy-block.cjs +25 -206
- package/src/claude/hooks/rules.cjs +10 -29
- package/src/claude/hooks/session.cjs +37 -117
- package/src/claude/hooks/spec-gate.cjs +9 -26
- package/src/claude/hooks/spec-state.cjs +44 -59
- package/src/claude/hooks/state.cjs +101 -52
- package/src/claude/migration-manifest.json +14 -2
- package/src/claude/plugin/commands/setup-statusline.md +12 -0
- package/src/claude/plugin/core.md +71 -0
- package/src/claude/rules/hook-protocols.md +1 -1
- package/src/claude/rules/manage-docs.md +1 -2
- package/src/claude/rules/review-audit-self-decision.md +1 -1
- package/src/claude/rules/skill-workflow-routing.md +1 -1
- package/src/claude/rules/state-sync.md +14 -0
- package/src/claude/rules/workflow.md +1 -1
- package/src/claude/runtime.json +0 -3
- package/src/claude/scripts/generate-skill-catalog.cjs +7 -2
- package/src/claude/scripts/provenance.cjs +54 -16
- package/src/claude/scripts/setup-statusline.cjs +88 -0
- package/src/claude/settings/settings.json +1 -1
- package/src/claude/skills/adv/SKILL.md +139 -0
- package/src/claude/skills/adv/references/optional-modes.md +41 -0
- package/src/claude/skills/adv/references/ultra-mode.md +29 -0
- 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/brainstorm/SKILL.md +1 -1
- package/src/claude/skills/code-review/SKILL.md +4 -1
- package/src/claude/skills/code-review/references/pr-fix-reply.md +42 -0
- package/src/claude/skills/develop/SKILL.md +5 -3
- package/src/claude/skills/develop/references/quality-gate.md +1 -1
- package/src/claude/skills/fix/SKILL.md +2 -2
- package/src/claude/skills/git/SKILL.md +5 -3
- package/src/claude/skills/git/references/merge-pr.md +125 -0
- package/src/claude/skills/git/references/worktree-blueprint.md +1 -1
- package/src/claude/skills/specs/SKILL.md +2 -2
- package/src/claude/skills/specs/references/review.md +1 -1
- package/src/claude/skills/specs/references/templates.md +2 -0
- package/src/claude/skills/vibe/SKILL.md +69 -0
- package/src/claude/skills/vibe/references/pipeline-skill-integration.md +34 -0
- package/src/claude/skills/vibe/references/report-templates.md +84 -0
- package/src/claude/skills/vibe/references/subagent-orchestration.md +23 -0
- package/src/codex/AGENTS.md +9 -1
- package/src/codex/hooks/ask-guard.cjs +54 -0
- package/src/codex/hooks/inspect-block.cjs +15 -8
- package/src/codex/hooks/lib/hook-context.cjs +118 -4
- package/src/codex/hooks/lib/inspect-paths.cjs +1 -199
- package/src/codex/hooks/lib/plugin-options.cjs +2 -0
- package/src/codex/hooks/lib/privacy-paths.cjs +1 -0
- package/src/codex/hooks/lib/privacy-state.cjs +116 -17
- 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-session-touch.cjs +100 -0
- package/src/codex/hooks/lib/spec-touch.cjs +3 -0
- package/src/codex/hooks/lib/state-store.cjs +5 -4
- package/src/codex/hooks/lib/update-check.cjs +1 -0
- package/src/codex/hooks/precompact.cjs +40 -0
- package/src/codex/hooks/privacy-approval.cjs +28 -17
- package/src/codex/hooks/privacy-block.cjs +11 -216
- package/src/codex/hooks/rules.cjs +14 -34
- package/src/codex/hooks/secret-output-guardrail.cjs +1 -1
- package/src/codex/hooks/session.cjs +59 -11
- package/src/codex/hooks/spec-gate.cjs +21 -6
- package/src/codex/hooks/spec-state.cjs +32 -6
- package/src/codex/hooks/state.cjs +25 -4
- package/src/codex/hooks.json +39 -7
- package/src/codex/plugin/core.md +76 -0
- package/src/codex/plugin/hooks/plugin-context.cjs +63 -0
- package/src/codex/plugin/skills/setup-agents/SKILL.md +17 -0
- package/src/codex/rules/hook-protocols.md +13 -6
- package/src/codex/scripts/setup-agents.cjs +124 -0
- package/src/common/AGENTS.md +1 -1
- package/src/omp/extensions/{cafekit-bridge.mjs → cafekit-bridge.js} +85 -9
- package/src/omp/hooks/privacy-block.cjs +26 -207
- 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,16 @@ 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
|
-
|
|
118
|
+
// The plugin is updated by its marketplace, so an npx notice would be wrong there.
|
|
119
|
+
const updateCheckPromise = process.env.CLAUDE_PLUGIN_ROOT
|
|
120
|
+
? Promise.resolve('')
|
|
121
|
+
: checkCafeKitUpdate(runtimeDir(cwd)).catch(() => '');
|
|
208
122
|
|
|
209
123
|
// Project detection
|
|
210
124
|
const projectType = runtime.project?.type !== 'auto'
|
|
@@ -221,7 +135,6 @@ try {
|
|
|
221
135
|
|
|
222
136
|
// Static environment
|
|
223
137
|
const gitBranch = run('git branch --show-current', '', cwd);
|
|
224
|
-
const gitUrl = run('git remote get-url origin', '', cwd);
|
|
225
138
|
const gitRoot = run('git rev-parse --show-toplevel', '', cwd);
|
|
226
139
|
const nodeVer = process.version;
|
|
227
140
|
const pythonVer = run('python3 --version', '', cwd) || run('python --version', '', cwd);
|
|
@@ -235,7 +148,6 @@ try {
|
|
|
235
148
|
writeEnv(envFile, 'PACKAGE_MANAGER', packageManager);
|
|
236
149
|
writeEnv(envFile, 'FRAMEWORK', framework);
|
|
237
150
|
writeEnv(envFile, 'GIT_BRANCH', gitBranch);
|
|
238
|
-
writeEnv(envFile, 'GIT_URL', gitUrl);
|
|
239
151
|
writeEnv(envFile, 'GIT_ROOT', gitRoot);
|
|
240
152
|
writeEnv(envFile, 'NODE_VERSION', nodeVer);
|
|
241
153
|
writeEnv(envFile, 'PYTHON_VERSION', pythonVer);
|
|
@@ -262,6 +174,14 @@ try {
|
|
|
262
174
|
|
|
263
175
|
console.log(`Session ${source}. ${parts.length ? parts.join(' | ') : 'No project info detected.'}${updateNotice}`);
|
|
264
176
|
|
|
177
|
+
if (typeof payload.cwd === 'string' && payload.cwd) {
|
|
178
|
+
let sessionCwd = path.resolve(payload.cwd);
|
|
179
|
+
try { sessionCwd = fs.realpathSync(sessionCwd); } catch { /* lexical fallback */ }
|
|
180
|
+
if (sessionCwd !== cwd) {
|
|
181
|
+
console.log(`CafeKit project root: ${cwd} (session working directory: ${sessionCwd}).`);
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
265
185
|
// Compact warning — context compaction can lose pending approval state
|
|
266
186
|
if (source === 'compact') {
|
|
267
187
|
console.log('\n🚨 SESSION COMPRESSED — VERIFY PENDING AUTHORIZATIONS:');
|
|
@@ -274,8 +194,11 @@ try {
|
|
|
274
194
|
// bearings, and nothing more.
|
|
275
195
|
try {
|
|
276
196
|
const fs2 = require('fs'), p2 = require('path');
|
|
277
|
-
|
|
197
|
+
if (typeof payload.session_id !== 'string' || !payload.session_id.trim()) process.exit(0);
|
|
198
|
+
const key = require('crypto').createHash('sha256').update(payload.session_id).digest('hex');
|
|
199
|
+
const file = p2.join(require('./lib/hook-state-dir.cjs').hookStateDir(), `compact-recovery-${key}.json`);
|
|
278
200
|
const record = JSON.parse(fs2.readFileSync(file, 'utf8'));
|
|
201
|
+
if (record.session_id !== payload.session_id) process.exit(0);
|
|
279
202
|
const parts = [];
|
|
280
203
|
if (record.branch) parts.push(`branch ${record.branch}`);
|
|
281
204
|
if (record.head) parts.push(`HEAD ${record.head}`);
|
|
@@ -297,15 +220,12 @@ try {
|
|
|
297
220
|
}
|
|
298
221
|
|
|
299
222
|
process.exit(0);
|
|
300
|
-
})()
|
|
223
|
+
})().catch((error) => {
|
|
224
|
+
logCrash(error);
|
|
225
|
+
process.exit(0);
|
|
226
|
+
});
|
|
301
227
|
|
|
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 (_) {}
|
|
228
|
+
} catch (error) {
|
|
229
|
+
logCrash(error);
|
|
310
230
|
process.exit(0);
|
|
311
231
|
}
|
|
@@ -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
|
|
|
@@ -132,6 +108,8 @@ try {
|
|
|
132
108
|
}
|
|
133
109
|
if (!resolved) process.exit(0);
|
|
134
110
|
if (resolved.layoutKind === 'process-v3-completed-set') {
|
|
111
|
+
hasDoneTasks = resolved.candidates.some(candidate =>
|
|
112
|
+
Object.values(candidate.taskRegistry || {}).some(task => task?.status === 'done'));
|
|
135
113
|
const configuredGate = runtime.spec?.completion_gate;
|
|
136
114
|
if (configuredGate !== undefined && configuredGate !== true) {
|
|
137
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.');
|
|
@@ -178,6 +156,7 @@ try {
|
|
|
178
156
|
|
|
179
157
|
const featureName = resolved.featureName;
|
|
180
158
|
const specsPath = resolved.specsDir;
|
|
159
|
+
hasDoneTasks = Object.values(resolved.taskRegistry || {}).some(task => task?.status === 'done');
|
|
181
160
|
const runtimeContext = POLICY.deriveRuntimeContext({
|
|
182
161
|
projectRoot: baseDir,
|
|
183
162
|
specsRoot: specsPath,
|
|
@@ -250,6 +229,10 @@ try {
|
|
|
250
229
|
process.exit(0);
|
|
251
230
|
|
|
252
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
|
+
}
|
|
253
236
|
logCrash(e);
|
|
254
237
|
emitBlock(`Completion gate controlled failure: ${e.message}. Completion is blocked until the hook is repaired.`);
|
|
255
238
|
process.exit(0);
|
|
@@ -13,6 +13,8 @@
|
|
|
13
13
|
|
|
14
14
|
const fs = require('fs');
|
|
15
15
|
const path = require('path');
|
|
16
|
+
const { projectRoot } = require('./lib/project-root.cjs');
|
|
17
|
+
const { packetAt, namedPackets, explicitFailure, touchedPaths } = require('./lib/spec-touch.cjs');
|
|
16
18
|
const { runtimeDirName, runtimeDir, runtimePath } = require('./lib/runtime-dir.cjs');
|
|
17
19
|
|
|
18
20
|
function logCrash(error) {
|
|
@@ -40,44 +42,6 @@ function sessionIdentity(payload) {
|
|
|
40
42
|
return candidates.find((value) => typeof value === 'string' && value.length > 0) || null;
|
|
41
43
|
}
|
|
42
44
|
|
|
43
|
-
function canonicalPath(value) {
|
|
44
|
-
try { return fs.realpathSync(value); } catch { return null; }
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
/** Real path of `value`, resolving its deepest existing ancestor (the file may not exist yet). */
|
|
48
|
-
function canonicalTarget(value) {
|
|
49
|
-
if (typeof value !== 'string' || !value) return null;
|
|
50
|
-
let current = path.resolve(value);
|
|
51
|
-
const rest = [];
|
|
52
|
-
while (!fs.existsSync(current)) {
|
|
53
|
-
const parent = path.dirname(current);
|
|
54
|
-
if (parent === current) return null;
|
|
55
|
-
rest.unshift(path.basename(current));
|
|
56
|
-
current = parent;
|
|
57
|
-
}
|
|
58
|
-
const real = canonicalPath(current);
|
|
59
|
-
return real ? path.join(real, ...rest) : null;
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
function escapeRegExp(text) {
|
|
63
|
-
return text.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
/**
|
|
67
|
-
* Packets the prompt names: `specs/<name>`, a `/cf:<command> <name>` token, or the bare
|
|
68
|
-
* name when it carries `-`, `_` or a digit. A plain word such as `docs` must use `specs/`,
|
|
69
|
-
* so an ordinary sentence does not touch a packet.
|
|
70
|
-
*/
|
|
71
|
-
function packetsNamedIn(text, names) {
|
|
72
|
-
if (typeof text !== 'string' || !text) return [];
|
|
73
|
-
return names.filter((name) => {
|
|
74
|
-
const n = escapeRegExp(name);
|
|
75
|
-
if (new RegExp(`(^|[^\\w-])specs/${n}(?![\\w-])`).test(text)) return true;
|
|
76
|
-
if (new RegExp(`/cf:[\\w-]+\\s+(?:specs/)?${n}(?![\\w-])`).test(text)) return true;
|
|
77
|
-
return /[-_0-9]/.test(name) && new RegExp(`(^|[^\\w/-])${n}(?![\\w-])`).test(text);
|
|
78
|
-
});
|
|
79
|
-
}
|
|
80
|
-
|
|
81
45
|
try {
|
|
82
46
|
// ── Main ──────────────────────────────────────────────────────────────────
|
|
83
47
|
|
|
@@ -94,6 +58,7 @@ try {
|
|
|
94
58
|
let RECEIPT;
|
|
95
59
|
try {
|
|
96
60
|
POLICY = require(path.join(__dirname, '..', 'scripts', 'workflow-policy.cjs'));
|
|
61
|
+
require(path.join(__dirname, '..', 'scripts', 'provenance.cjs')).enableSnapshotMemo();
|
|
97
62
|
RESOLVER = require(path.join(__dirname, '..', 'scripts', 'spec-resolver.cjs'));
|
|
98
63
|
RECEIPT = require(path.join(__dirname, '..', 'scripts', 'spec-receipt.cjs'));
|
|
99
64
|
if (typeof RESOLVER.resolveWorkflowCandidate !== 'function') throw new Error('shared spec resolver has no resolveWorkflowCandidate function');
|
|
@@ -103,10 +68,12 @@ try {
|
|
|
103
68
|
process.exit(0);
|
|
104
69
|
}
|
|
105
70
|
|
|
71
|
+
const baseDir = projectRoot(payload);
|
|
72
|
+
|
|
106
73
|
// Read runtime configuration if exists
|
|
107
74
|
let runtime = {};
|
|
108
75
|
try {
|
|
109
|
-
const p = runtimePath(
|
|
76
|
+
const p = runtimePath(baseDir, 'runtime.json');
|
|
110
77
|
if (fs.existsSync(p)) runtime = JSON.parse(fs.readFileSync(p, 'utf8'));
|
|
111
78
|
} catch { /* ignore */ }
|
|
112
79
|
|
|
@@ -114,20 +81,17 @@ try {
|
|
|
114
81
|
// (The Stop completion gate has its own toggle: spec.completion_gate.)
|
|
115
82
|
if (runtime.spec && runtime.spec.tollgate === false) process.exit(0);
|
|
116
83
|
|
|
117
|
-
// Same rule as spec-gate: an installed hook trusts its own location over the environment.
|
|
118
|
-
const installedRoot = path.basename(path.resolve(__dirname, '..')).startsWith('.') ? path.resolve(__dirname, '..', '..') : null;
|
|
119
|
-
const baseDir = installedRoot || process.env.PROJECT_ROOT || cwd;
|
|
120
84
|
const explicitFeature = payload.featureName || payload.feature || payload.explicitFeature || null;
|
|
121
|
-
const explicitPath = payload.specPath || payload.spec_path || payload.featurePath || null;
|
|
85
|
+
const explicitPath = payload.explicitPath || payload.specPath || payload.spec_path || payload.featurePath || null;
|
|
122
86
|
|
|
123
|
-
// ── Per-session touch set (Claude
|
|
124
|
-
// Several sessions can share one checkout. On Claude the tollgate speaks only
|
|
125
|
-
// packet this session touched.
|
|
126
|
-
// `sessionId` before normalization, so
|
|
87
|
+
// ── Per-session touch set (Claude and omp) ────────────────────────────────
|
|
88
|
+
// Several sessions can share one checkout. On Claude and omp the tollgate speaks only
|
|
89
|
+
// about a packet this session touched. Codex runs its own copy, and grok sends
|
|
90
|
+
// `sessionId` before normalization, so both keep the old behaviour.
|
|
127
91
|
const claudeSession = rawPayload && typeof rawPayload.session_id === 'string' && rawPayload.session_id
|
|
128
92
|
? rawPayload.session_id
|
|
129
93
|
: null;
|
|
130
|
-
const touchFilter =
|
|
94
|
+
const touchFilter = ['.claude', '.omp'].includes(runtimeDirName()) && claudeSession !== null;
|
|
131
95
|
const touchFile = touchFilter
|
|
132
96
|
? path.join(
|
|
133
97
|
require('./lib/hook-state-dir.cjs').hookStateDir(),
|
|
@@ -155,14 +119,10 @@ try {
|
|
|
155
119
|
|
|
156
120
|
if (payload.hook_event_name === 'PostToolUse') {
|
|
157
121
|
// Record only; no resolve, no git, no tollgate cache.
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
const
|
|
161
|
-
if (
|
|
162
|
-
const rel = path.relative(realRoot, realTarget);
|
|
163
|
-
const parts = rel.split(path.sep);
|
|
164
|
-
if (rel && parts[0] !== '..' && !path.isAbsolute(rel) && parts.length > 1) addTouched([parts[0]]);
|
|
165
|
-
}
|
|
122
|
+
// omp's ast_edit keeps its own name: aliasing it to Edit would lift the inspect scope check.
|
|
123
|
+
if (touchFilter && specsRoot && !explicitFailure(payload) && /^(Edit|Write|MultiEdit|ast_edit)$/.test(payload.tool_name || '')) {
|
|
124
|
+
const names = touchedPaths(payload).map((value) => packetAt(value, specsRoot, cwd)).filter(Boolean);
|
|
125
|
+
if (names.length) addTouched(names);
|
|
166
126
|
}
|
|
167
127
|
process.exit(0);
|
|
168
128
|
}
|
|
@@ -204,12 +164,29 @@ try {
|
|
|
204
164
|
try {
|
|
205
165
|
packets = fs.readdirSync(specsRoot, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => e.name);
|
|
206
166
|
} catch { /* no specs root */ }
|
|
207
|
-
const named =
|
|
167
|
+
const named = specsRoot ? namedPackets(payload.prompt || payload.user_prompt, packets, specsRoot, baseDir) : [];
|
|
208
168
|
if (typeof explicitFeature === 'string' && explicitFeature) named.push(explicitFeature);
|
|
169
|
+
const fromPath = specsRoot ? packetAt(explicitPath, specsRoot, baseDir) : null;
|
|
170
|
+
if (fromPath) named.push(fromPath);
|
|
209
171
|
if (named.length) addTouched(named);
|
|
210
172
|
}
|
|
211
173
|
|
|
212
|
-
|
|
174
|
+
let resolved = RESOLVER.resolveWorkflowCandidate({ projectRoot: baseDir, runtime, explicitFeature, explicitPath });
|
|
175
|
+
if (touchFilter && !explicitFeature && !explicitPath
|
|
176
|
+
&& ['multiple_active', 'invalid_specs'].includes(resolved?.error)) {
|
|
177
|
+
const touched = readTouched();
|
|
178
|
+
const candidates = resolved.candidates || [];
|
|
179
|
+
if (!candidates.includes('<specs>') && !candidates.some(name => touched.has(name))) {
|
|
180
|
+
const selected = [...touched].map(name => RESOLVER.resolveWorkflowCandidate({
|
|
181
|
+
projectRoot: baseDir, runtime, explicitFeature: name,
|
|
182
|
+
}));
|
|
183
|
+
resolved = selected.find(candidate => candidate?.error);
|
|
184
|
+
const active = selected.filter(candidate => candidate && !candidate.error && !candidate.allTasksDone);
|
|
185
|
+
if (!resolved) resolved = active.length > 1
|
|
186
|
+
? { error: 'multiple_active', candidates: active.map(candidate => candidate.featureName) }
|
|
187
|
+
: active[0] || null;
|
|
188
|
+
}
|
|
189
|
+
}
|
|
213
190
|
|
|
214
191
|
if (!resolved) {
|
|
215
192
|
process.exit(0); // No active spec, do nothing
|
|
@@ -299,7 +276,10 @@ try {
|
|
|
299
276
|
done: taskCounts.done || 0,
|
|
300
277
|
total: taskEntries.length,
|
|
301
278
|
});
|
|
302
|
-
const
|
|
279
|
+
const cacheName = touchFilter
|
|
280
|
+
? `tollgate-last-${require('crypto').createHash('sha256').update(claudeSession).digest('hex').slice(0, 16)}.txt`
|
|
281
|
+
: 'tollgate-last.txt';
|
|
282
|
+
const cacheFile = path.join(require('./lib/hook-state-dir.cjs').hookStateDir(), cacheName);
|
|
303
283
|
|
|
304
284
|
let lastKey = '';
|
|
305
285
|
if (stateKey) {
|
|
@@ -335,12 +315,17 @@ try {
|
|
|
335
315
|
}
|
|
336
316
|
lines.push('- Sync each flat task `Status:` only after verified work; task proof belongs in that task\'s inline `## Receipt`.');
|
|
337
317
|
lines.push(`- Workflow source: \`specs/${featureName}/plan.md\`; Stop re-checks every done receipt, binding Base/Head to the runtime only while a committed task file no longer matches its committed bytes.`);
|
|
318
|
+
lines.push('- Hooks revalidate receipt bytes but never grant approval.');
|
|
338
319
|
lines.push('');
|
|
339
320
|
|
|
340
321
|
console.log(lines.join('\n'));
|
|
341
322
|
process.exit(0);
|
|
342
323
|
|
|
343
324
|
} catch (e) {
|
|
325
|
+
if (e.code === 'no_git_history') {
|
|
326
|
+
console.log('Repo has no commits yet; create the first commit, then record Base/Head after it.');
|
|
327
|
+
process.exit(0);
|
|
328
|
+
}
|
|
344
329
|
logCrash(e);
|
|
345
330
|
emitControlledFailure(e.message);
|
|
346
331
|
process.exit(0);
|