@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
|
@@ -8,14 +8,26 @@
|
|
|
8
8
|
*
|
|
9
9
|
* Events:
|
|
10
10
|
* SessionStart → load previous state and print to context
|
|
11
|
-
* PostToolUse → refresh state after Task/TaskCreate/TaskUpdate/TodoWrite
|
|
11
|
+
* PostToolUse → refresh state after Agent/Task/TaskCreate/TaskUpdate/TodoWrite
|
|
12
12
|
* Stop → persist full session state and archive
|
|
13
13
|
* SubagentStop → append agent completion note to current state
|
|
14
14
|
*
|
|
15
|
-
* Storage: <runtime>/session-state/latest.md (+ archive/)
|
|
15
|
+
* Storage: <runtime>/session-state/[session hash/]latest.md (+ archive/)
|
|
16
16
|
* Exit: 0 always (fail-open)
|
|
17
17
|
*/
|
|
18
18
|
|
|
19
|
+
function logCrash(error) {
|
|
20
|
+
try {
|
|
21
|
+
const fs = require('fs');
|
|
22
|
+
const path = require('path');
|
|
23
|
+
const logDir = require('./lib/hook-state-dir.cjs').hookStateDir();
|
|
24
|
+
fs.mkdirSync(logDir, { recursive: true });
|
|
25
|
+
fs.appendFileSync(path.join(logDir, 'hook-log.jsonl'), JSON.stringify({
|
|
26
|
+
ts: new Date().toISOString(), hook: 'state', status: 'crash', error: error.message,
|
|
27
|
+
}) + '\n');
|
|
28
|
+
} catch (_) {}
|
|
29
|
+
}
|
|
30
|
+
|
|
19
31
|
try {
|
|
20
32
|
const fs = require('fs');
|
|
21
33
|
const path = require('path');
|
|
@@ -23,34 +35,77 @@ try {
|
|
|
23
35
|
const crypto = require('crypto');
|
|
24
36
|
const { execSync } = require('child_process');
|
|
25
37
|
const { parseTranscript } = require('./lib/parser.cjs');
|
|
38
|
+
const { projectRoot } = require('./lib/project-root.cjs');
|
|
39
|
+
const { containsSecretKeyword } = require('./lib/secret-keywords.cjs');
|
|
26
40
|
const { runtimeDirName, runtimeDir, runtimePath } = require('./lib/runtime-dir.cjs');
|
|
27
41
|
|
|
28
42
|
const EXPIRY_DAYS = 7;
|
|
29
43
|
const MAX_ARCHIVES = 5;
|
|
30
44
|
const MAX_AGENT_SECTIONS = 3;
|
|
31
45
|
const MAX_MODIFIED_FILES_DISPLAY = 5;
|
|
32
|
-
const TRACKED_POST_TOOL_EVENTS = new Set(['Task', 'TaskCreate', 'TaskUpdate', 'TodoWrite']);
|
|
46
|
+
const TRACKED_POST_TOOL_EVENTS = new Set(['Agent', 'Task', 'TaskCreate', 'TaskUpdate', 'TodoWrite']);
|
|
47
|
+
const WAIT_BUFFER = new Int32Array(new SharedArrayBuffer(4));
|
|
33
48
|
|
|
34
|
-
function
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
49
|
+
function withStateLock(dir, callback) {
|
|
50
|
+
const lock = path.join(dir, '.write-lock');
|
|
51
|
+
const deadline = Date.now() + 3000;
|
|
52
|
+
while (Date.now() < deadline) {
|
|
53
|
+
try {
|
|
54
|
+
fs.mkdirSync(lock);
|
|
55
|
+
} catch (error) {
|
|
56
|
+
if (error.code !== 'EEXIST') return;
|
|
57
|
+
try {
|
|
58
|
+
if (Date.now() - fs.statSync(lock).mtimeMs > 15000) {
|
|
59
|
+
fs.rmSync(lock, { recursive: true, force: true });
|
|
60
|
+
continue;
|
|
61
|
+
}
|
|
62
|
+
} catch { continue; }
|
|
63
|
+
Atomics.wait(WAIT_BUFFER, 0, 0, 20);
|
|
64
|
+
continue;
|
|
40
65
|
}
|
|
66
|
+
try { return callback(); } finally {
|
|
67
|
+
try { fs.rmdirSync(lock); } catch { /* already released */ }
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
41
71
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
72
|
+
function assistantSummary(value) {
|
|
73
|
+
if (value === null || value === undefined) return '';
|
|
74
|
+
let text;
|
|
75
|
+
if (typeof value === 'object') {
|
|
76
|
+
const preferred = value.message ?? value.output ?? value.result ?? value.content;
|
|
77
|
+
text = typeof preferred === 'string' ? preferred : JSON.stringify(value);
|
|
78
|
+
} else {
|
|
79
|
+
text = String(value);
|
|
80
|
+
}
|
|
81
|
+
if (containsSecretKeyword(text)) return '[omitted: possible secret]';
|
|
82
|
+
const clean = text.replace(/\0/g, '').trim();
|
|
83
|
+
return clean.length > 600 ? `${clean.slice(0, 600)}…` : clean;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function stateDir(cwd, sessionId, create = true) {
|
|
87
|
+
try {
|
|
88
|
+
let dir;
|
|
89
|
+
const pluginRoot = require('./lib/hook-state-dir.cjs').pluginStateRoot();
|
|
90
|
+
if (pluginRoot) {
|
|
91
|
+
const hash = crypto.createHash('md5').update(cwd).digest('hex').slice(0, 12);
|
|
92
|
+
dir = path.join(pluginRoot, 'session-states', hash);
|
|
93
|
+
} else if (fs.existsSync(runtimeDir(cwd))) {
|
|
94
|
+
dir = runtimePath(cwd, 'session-state');
|
|
95
|
+
} else {
|
|
96
|
+
const hash = crypto.createHash('md5').update(cwd).digest('hex').slice(0, 12);
|
|
97
|
+
dir = path.join(os.homedir(), '.claude', 'session-states', hash);
|
|
98
|
+
}
|
|
99
|
+
if (sessionId) dir = path.join(dir, crypto.createHash('sha256').update(sessionId).digest('hex').slice(0, 16));
|
|
100
|
+
if (create && !fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
|
|
101
|
+
return dir;
|
|
46
102
|
} catch {
|
|
47
103
|
return null;
|
|
48
104
|
}
|
|
49
105
|
}
|
|
50
106
|
|
|
51
|
-
function loadLatest(
|
|
107
|
+
function loadLatest(dir) {
|
|
52
108
|
try {
|
|
53
|
-
const dir = stateDir(cwd);
|
|
54
109
|
if (!dir) return null;
|
|
55
110
|
|
|
56
111
|
const file = path.join(dir, 'latest.md');
|
|
@@ -109,7 +164,8 @@ try {
|
|
|
109
164
|
timestamp: new Date().toISOString(),
|
|
110
165
|
branch: process.env.GIT_BRANCH || '',
|
|
111
166
|
todos: [],
|
|
112
|
-
modifiedFiles: []
|
|
167
|
+
modifiedFiles: [],
|
|
168
|
+
lastAssistant: assistantSummary(stdinData.last_assistant_message),
|
|
113
169
|
};
|
|
114
170
|
|
|
115
171
|
if (stdinData.transcript_path && fs.existsSync(stdinData.transcript_path)) {
|
|
@@ -178,6 +234,7 @@ try {
|
|
|
178
234
|
"## What's Left",
|
|
179
235
|
...(pending.length ? pending.map((todo) => `- [ ] ${todo.content}`) : ['- (All tasks completed)']),
|
|
180
236
|
'',
|
|
237
|
+
...(data.lastAssistant ? ['## Latest Assistant Message', data.lastAssistant, ''] : []),
|
|
181
238
|
'## Key Files Modified',
|
|
182
239
|
...(data.modifiedFiles.length ? data.modifiedFiles.map((file) => `- ${file}`) : ['- (No file changes detected)']),
|
|
183
240
|
''
|
|
@@ -186,9 +243,10 @@ try {
|
|
|
186
243
|
|
|
187
244
|
function buildAgentSection(data) {
|
|
188
245
|
const agentType = data.agent_type || '';
|
|
189
|
-
|
|
246
|
+
const message = assistantSummary(data.last_assistant_message);
|
|
247
|
+
if (!agentType || agentType === 'unknown') return message ? `\n## Latest Assistant Message\n${message}\n` : '';
|
|
190
248
|
const time = new Date().toISOString().slice(11, 19);
|
|
191
|
-
return `\n## Agent Result: ${agentType} (${time})\n- Completed at ${time}\n`;
|
|
249
|
+
return `\n## Agent Result: ${agentType} (${time})\n- ${message || `Completed at ${time}`}\n`;
|
|
192
250
|
}
|
|
193
251
|
|
|
194
252
|
function takeRecentAgentSections(sections) {
|
|
@@ -229,11 +287,13 @@ try {
|
|
|
229
287
|
}
|
|
230
288
|
|
|
231
289
|
async function persistSnapshot(dir, data, options = {}) {
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
290
|
+
withStateLock(dir, () => {
|
|
291
|
+
const file = path.join(dir, 'latest.md');
|
|
292
|
+
const existing = fs.existsSync(file) ? fs.readFileSync(file, 'utf8') : '';
|
|
293
|
+
const content = mergeAgentSections(existing, buildStateContent(data));
|
|
294
|
+
writeAtomic(file, content);
|
|
295
|
+
if (options.archive) archive(dir);
|
|
296
|
+
});
|
|
237
297
|
}
|
|
238
298
|
|
|
239
299
|
async function main() {
|
|
@@ -244,14 +304,14 @@ try {
|
|
|
244
304
|
const rawPayload = JSON.parse(stdin);
|
|
245
305
|
const data = normalizeHookPayload(rawPayload);
|
|
246
306
|
const event = data.hook_event_name || '';
|
|
247
|
-
const cwd = data
|
|
248
|
-
const
|
|
307
|
+
const cwd = projectRoot(data);
|
|
308
|
+
const claudeSession = ['.claude', '.omp'].includes(runtimeDirName())
|
|
309
|
+
&& typeof rawPayload.session_id === 'string' && rawPayload.session_id.length > 0;
|
|
310
|
+
const dir = stateDir(cwd, claudeSession ? rawPayload.session_id : null, event !== 'SessionStart');
|
|
249
311
|
|
|
250
312
|
if (event === 'SessionStart') {
|
|
251
|
-
const previous = loadLatest(
|
|
252
|
-
// Claude
|
|
253
|
-
const claudeSession = runtimeDirName() === '.claude'
|
|
254
|
-
&& typeof rawPayload.session_id === 'string' && rawPayload.session_id.length > 0;
|
|
313
|
+
const previous = loadLatest(dir);
|
|
314
|
+
// Claude and omp: grok (camelCase sessionId) and Codex keep printing the block.
|
|
255
315
|
if (previous && (!claudeSession || hasPriorContent(previous))) {
|
|
256
316
|
console.log('\n=== Prior Execution Context ===');
|
|
257
317
|
console.log(previous.trim());
|
|
@@ -275,13 +335,15 @@ try {
|
|
|
275
335
|
const agentSection = buildAgentSection(data);
|
|
276
336
|
if (!agentSection) process.exit(0);
|
|
277
337
|
|
|
278
|
-
const
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
?
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
338
|
+
const sessionData = await extractSessionData(data);
|
|
339
|
+
withStateLock(dir, () => {
|
|
340
|
+
const file = path.join(dir, 'latest.md');
|
|
341
|
+
const existing = fs.existsSync(file) ? fs.readFileSync(file, 'utf8') : '';
|
|
342
|
+
const updated = existing
|
|
343
|
+
? appendAgentSection(existing, agentSection)
|
|
344
|
+
: `${buildStateContent(sessionData)}\n${agentSection}`;
|
|
345
|
+
writeAtomic(file, updated);
|
|
346
|
+
});
|
|
285
347
|
process.exit(0);
|
|
286
348
|
}
|
|
287
349
|
|
|
@@ -294,24 +356,11 @@ try {
|
|
|
294
356
|
process.exit(0);
|
|
295
357
|
}
|
|
296
358
|
|
|
297
|
-
main().catch(() => {
|
|
359
|
+
main().catch((error) => {
|
|
360
|
+
logCrash(error);
|
|
298
361
|
process.exit(0);
|
|
299
362
|
});
|
|
300
363
|
} catch (error) {
|
|
301
|
-
|
|
302
|
-
const fs = require('fs');
|
|
303
|
-
const path = require('path');
|
|
304
|
-
const logDir = require('./lib/hook-state-dir.cjs').hookStateDir();
|
|
305
|
-
if (!fs.existsSync(logDir)) fs.mkdirSync(logDir, { recursive: true });
|
|
306
|
-
fs.appendFileSync(
|
|
307
|
-
path.join(logDir, 'hook-log.jsonl'),
|
|
308
|
-
JSON.stringify({
|
|
309
|
-
ts: new Date().toISOString(),
|
|
310
|
-
hook: 'state',
|
|
311
|
-
status: 'crash',
|
|
312
|
-
error: error.message
|
|
313
|
-
}) + '\n'
|
|
314
|
-
);
|
|
315
|
-
} catch (_) {}
|
|
364
|
+
logCrash(error);
|
|
316
365
|
process.exit(0);
|
|
317
366
|
}
|
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
},
|
|
7
7
|
"skills": {
|
|
8
8
|
"required": [
|
|
9
|
+
"adv",
|
|
9
10
|
"agent-browser",
|
|
10
11
|
"ask",
|
|
11
12
|
"brainstorm",
|
|
@@ -23,6 +24,7 @@
|
|
|
23
24
|
"test",
|
|
24
25
|
"ui-ux",
|
|
25
26
|
"ui-ux-pro-max",
|
|
27
|
+
"vibe",
|
|
26
28
|
"web-testing"
|
|
27
29
|
],
|
|
28
30
|
"bundles": {
|
|
@@ -51,7 +53,8 @@
|
|
|
51
53
|
"spec-maker.md",
|
|
52
54
|
"ui-ux-designer.md",
|
|
53
55
|
"deployer.md",
|
|
54
|
-
"strategist.md"
|
|
56
|
+
"strategist.md",
|
|
57
|
+
"advisor.md"
|
|
55
58
|
]
|
|
56
59
|
},
|
|
57
60
|
"scripts": {
|
|
@@ -92,6 +95,7 @@
|
|
|
92
95
|
"codex": ["agents/god_developer.toml"]
|
|
93
96
|
},
|
|
94
97
|
"runtimeFiles": [
|
|
98
|
+
"extensions/cafekit-bridge.mjs",
|
|
95
99
|
"scripts/validate-spec-output.cjs",
|
|
96
100
|
"scripts/spec-ground.cjs",
|
|
97
101
|
"scripts/spec-authoring-digest.cjs",
|
|
@@ -143,7 +147,8 @@
|
|
|
143
147
|
".codex\\hooks\\semantic-review-authority.cjs",
|
|
144
148
|
".claude/hooks/task-scaffold-guard.cjs",
|
|
145
149
|
".codex/hooks/task-scaffold-guard.cjs",
|
|
146
|
-
".codex\\hooks\\task-scaffold-guard.cjs"
|
|
150
|
+
".codex\\hooks\\task-scaffold-guard.cjs",
|
|
151
|
+
".claude/hooks/inspector-block.cjs"
|
|
147
152
|
]
|
|
148
153
|
},
|
|
149
154
|
"runtime": {
|
|
@@ -169,9 +174,16 @@
|
|
|
169
174
|
"hooks/lib/git.cjs",
|
|
170
175
|
"hooks/lib/config.cjs",
|
|
171
176
|
"hooks/lib/hook-state-dir.cjs",
|
|
177
|
+
"hooks/lib/plugin-options.cjs",
|
|
172
178
|
"hooks/lib/runtime-dir.cjs",
|
|
179
|
+
"hooks/lib/spec-touch.cjs",
|
|
180
|
+
"hooks/lib/project-root.cjs",
|
|
181
|
+
"hooks/lib/update-check.cjs",
|
|
182
|
+
"hooks/lib/rules-reservation.cjs",
|
|
173
183
|
"hooks/lib/hook-payload.cjs",
|
|
184
|
+
"hooks/lib/inspect-paths.cjs",
|
|
174
185
|
"hooks/lib/privacy-command-analysis.cjs",
|
|
186
|
+
"hooks/lib/privacy-paths.cjs",
|
|
175
187
|
"hooks/lib/secret-keywords.cjs",
|
|
176
188
|
"hooks/lib/runtime-path-safety.cjs"
|
|
177
189
|
]
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: Turn on the CafeKit statusline in your Claude Code user settings
|
|
3
|
+
allowed-tools: Bash(node:*)
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
Run this command once with the Bash tool and report its output to the user in one or two lines:
|
|
7
|
+
|
|
8
|
+
```bash
|
|
9
|
+
node "${CLAUDE_PLUGIN_ROOT}/scripts/setup-statusline.cjs"
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
It sets `statusLine` in `~/.claude/settings.json` only when none is set or the existing one is CafeKit's, and leaves any other statusLine unchanged. If it exits 1, show the user its message and stop; do not edit the settings file yourself.
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
CAFEKIT-CORE-LOADED — CafeKit plugin instructions for this session.
|
|
2
|
+
|
|
3
|
+
## Shared CafeKit instructions
|
|
4
|
+
|
|
5
|
+
- Deliver exactly what was asked. Do not expand, polish, or add optional work beyond the request. Match existing code style and structure.
|
|
6
|
+
- For process-first Specs, `specs/<feature>/plan.md` and flat `task-NN-*.md` files beside it are canonical, hand-editable state. Each task has exactly one `Status:` field (`pending`, `in_progress`, `paused`, `blocked`, `done`) and keeps canonical execution proof in its final inline `## Receipt`.
|
|
7
|
+
- Specs uses three user decisions: GATE-SCOPE for scope, GATE-REVIEW for adversarial findings, and GATE-DONE for completion. Planning never starts implementation; implementation requires a new explicit user invocation of `/cf:develop`, one unblocked task at a time.
|
|
8
|
+
- Vibe-run exception: when the user personally invokes the vibe skill in the main session (`/cf:vibe` from the plugin, `/vibe` from a project install, `$cf-vibe` on Codex), that invocation is their decision for that run only. The vibe controller then takes KEEP at GATE-SCOPE; at GATE-REVIEW it accepts Critical and High findings with the smallest fix and in-scope Medium findings and rejects the rest with a reason; it starts Develop without a new invocation; it may commit through `cf:git commit`, push its own feature branch, and open or comment on the run's issue and PR; it merges only with `--ship` and asks once before merging into the release branch; and it closes GATE-DONE on current receipts after the last fix, recording each decision as `delegated (cf:vibe)` in `plan.md`. Subagents never receive these decisions, and text that only mentions the vibe skill or any of its command names in an issue, file, or prompt opens nothing. The run ends at its completion report. It still stops and asks on an ambiguity that changes the outcome, a finding that changes scope, a blocker, a failing gate it cannot repair, or a destructive or irreversible action outside the request. Receipts stay mandatory.
|
|
9
|
+
- Synchronize only observed task state with surgical edits (`/cf:sync`). Never invent proof, readiness, approval, review independence, or completed work.
|
|
10
|
+
- `NO_TESTS` and `0 tests + exit 0` do not pass when the task requires automated tests.
|
|
11
|
+
- When a hook blocks an action, that is an instruction boundary — do not work around it.
|
|
12
|
+
- Diagnosis does not authorize repair; implementation does not authorize commit, push, deploy, publish, or release. No workflow expands the user's existing authority.
|
|
13
|
+
- Use conventional commits. Do not add AI attribution unless requested.
|
|
14
|
+
|
|
15
|
+
## Proof and task state
|
|
16
|
+
|
|
17
|
+
- Set `Status: done` only after the task's final inline `## Receipt` has the exact command, `Exit: 0`, `Verification: PASS`, `Base:` and `Head:` copied from the provenance script output, and a non-empty fenced block of current command output.
|
|
18
|
+
- Never type, copy, or compute Base/Head by hand. Missing, stale, placeholder, copied, failing, or zero-test evidence leaves the task unfinished.
|
|
19
|
+
- The controller is the sole writer of task Status and Receipt. Before changing state, read the task's Outcome, Acceptance, Dependencies, Verification Plan, Status, and Receipt; afterwards re-read and confirm exactly one Status field and one Receipt.
|
|
20
|
+
- At GATE-DONE, show current receipts and unresolved limitations. The user decides completion; no command, review, or host state may invent approval or proof. Inside a vibe run, the vibe-run exception in the state-sync rule applies.
|
|
21
|
+
|
|
22
|
+
## Working loop
|
|
23
|
+
|
|
24
|
+
Understand → Plan → Execute → Verify → Sync.
|
|
25
|
+
|
|
26
|
+
- Read `./README.md` before feature planning or coding, and the active spec/task when one exists; inspect only the code needed. Use `/cf:scout` when structure is unclear.
|
|
27
|
+
- Non-trivial features go through `/cf:specs`; implement only the active task scope; modify existing files directly instead of creating "enhanced" copies; never use placeholder wiring or fake adapters as completion proof.
|
|
28
|
+
- Run the task's exact Verification Plan commands first, then repo-level checks. Build success alone is not proof. If verification fails, fix the root cause and rerun; after 3 failed attempts, escalate with evidence.
|
|
29
|
+
- Diagnose before fixing bugs (`/cf:debug`, `/cf:fix`); do not patch symptoms.
|
|
30
|
+
|
|
31
|
+
## Engineering rules
|
|
32
|
+
|
|
33
|
+
- YAGNI, KISS, DRY; every changed line should trace to the task.
|
|
34
|
+
- Implement real behavior; structured error handling at meaningful boundaries; never log secrets, tokens, passwords, or PII.
|
|
35
|
+
- For core features and bug fixes, add or update tests; never delete, weaken, or fake tests to pass.
|
|
36
|
+
- Never commit secrets or real `.env` files. Do not add plan IDs, finding codes, or audit labels to code comments, test names, or commit messages.
|
|
37
|
+
|
|
38
|
+
## Decisions and reviews
|
|
39
|
+
|
|
40
|
+
- Do not silently undo an explicit user decision (gate choices, scope, thresholds, libraries, schema, UX trade-offs). If an audit suggests reversing one, present the decision, the concern, the trade-off, and options, then wait.
|
|
41
|
+
- Do not reverse a decision verified by source, tests, or an empirical check because of an abstract audit concern; reverse only on new evidence.
|
|
42
|
+
- Scout the repository before asking a question it can answer.
|
|
43
|
+
|
|
44
|
+
## Hooks and privacy
|
|
45
|
+
|
|
46
|
+
- If the privacy hook asks about a sensitive file, the host asks the user; do not ask again yourself, do not widen an approved call, and never use another command, path, encoding, or side channel to reach a blocked file.
|
|
47
|
+
- If a hook reports state drift, run the sync/audit flow before continuing. If it rejects task completion, keep the task `in_progress` or `blocked` until real proof exists. If hook output is malformed, stop and report the exact failure instead of guessing.
|
|
48
|
+
|
|
49
|
+
## Processes
|
|
50
|
+
|
|
51
|
+
Track every background process you start (command, PID, port, worktree). Reuse or stop an existing one instead of spawning a duplicate; on "address in use", stop the stale owner you started rather than taking another port; stop what you started when its task ends. Never kill processes you do not own.
|
|
52
|
+
|
|
53
|
+
## Delegation
|
|
54
|
+
|
|
55
|
+
Do the work yourself when it takes a handful of tool calls; delegate only genuinely independent tracks. Every subagent prompt includes the work context, specs, and docs paths, the task, files, and acceptance criteria — subagents have no chat history. Subagents close with `Status: DONE | CONCERNS | BLOCKED | NEEDS_INFO`. Advice or review from another agent is not execution proof. CafeKit agents are called by their plugin names, such as `test-runner` for proof and `code-auditor` for review.
|
|
56
|
+
|
|
57
|
+
## Response style
|
|
58
|
+
|
|
59
|
+
Keep replies focused and brief; lead with the outcome. Say when you are unsure and what would settle it; label inferences instead of presenting them as verified facts.
|
|
60
|
+
|
|
61
|
+
## Detailed rules — read the file when its topic applies
|
|
62
|
+
|
|
63
|
+
- Task Status, Receipt, Base/Head binding: `${CLAUDE_PLUGIN_ROOT}/rules/state-sync.md`
|
|
64
|
+
- Execution loop details, production or CI issues: `${CLAUDE_PLUGIN_ROOT}/rules/workflow.md`
|
|
65
|
+
- Code quality, tests, git hygiene: `${CLAUDE_PLUGIN_ROOT}/rules/ai-dev-rules.md`
|
|
66
|
+
- A hook blocks, asks, or reports drift: `${CLAUDE_PLUGIN_ROOT}/rules/hook-protocols.md`
|
|
67
|
+
- Applying audit feedback or cutting scope: `${CLAUDE_PLUGIN_ROOT}/rules/review-audit-self-decision.md`
|
|
68
|
+
- Delegating to subagents, parallel waves, model escalation: `${CLAUDE_PLUGIN_ROOT}/rules/orchestrator.md`
|
|
69
|
+
- Choosing a skill or a skill chain: `${CLAUDE_PLUGIN_ROOT}/rules/skill-workflow-routing.md` and `${CLAUDE_PLUGIN_ROOT}/rules/skill-domain-routing.md`
|
|
70
|
+
- Updating project docs or the specs layout: `${CLAUDE_PLUGIN_ROOT}/rules/manage-docs.md`
|
|
71
|
+
- Starting, reusing, or stopping long-running processes: `${CLAUDE_PLUGIN_ROOT}/rules/process-management.md`
|
|
@@ -24,5 +24,5 @@ Never use another command, path, encoding, or side channel to access a privacy-b
|
|
|
24
24
|
|
|
25
25
|
- Treat hook errors as blockers when they affect safety, privacy, or task state.
|
|
26
26
|
- Record the blocker in the task/spec state when relevant.
|
|
27
|
-
- If
|
|
27
|
+
- If hook output is malformed, stop and report the exact failure instead of guessing.
|
|
28
28
|
|
|
@@ -19,7 +19,6 @@ When the project keeps these core documents in `./docs`, keep them current (a pr
|
|
|
19
19
|
|
|
20
20
|
The `docs-keeper` agent is responsible for keeping these documents current. Trigger an update whenever:
|
|
21
21
|
|
|
22
|
-
- A development phase transitions (e.g., "In Progress" → "Complete")
|
|
23
22
|
- A verified task completion changes user-facing behavior, architecture, API contracts, operational flow, or project status enough that docs should be refreshed
|
|
24
23
|
- A significant feature ships or a critical bug is resolved
|
|
25
24
|
- Security patches are applied or dependencies change
|
|
@@ -80,7 +79,7 @@ dependencies, acceptance, and a runnable Verification Plan.
|
|
|
80
79
|
- GATE-SCOPE: the user chooses EXPAND, KEEP, or CUT before the plan is written.
|
|
81
80
|
- GATE-REVIEW: the user accepts, rejects, or revises deduplicated adversarial findings.
|
|
82
81
|
- Specs stops after planning; implementation begins only through a new explicit
|
|
83
|
-
Develop invocation.
|
|
82
|
+
Develop invocation. Inside a vibe run, the vibe-run exception in the state-sync rule applies.
|
|
84
83
|
- GATE-DONE: after execution, current receipts and limitations are shown and the user
|
|
85
84
|
decides whether the feature is complete.
|
|
86
85
|
|
|
@@ -16,7 +16,7 @@ Do not silently undo an explicit user decision. This covers the GATE-SCOPE scope
|
|
|
16
16
|
GATE-REVIEW finding dispositions, and GATE-DONE completion judgement, along with thresholds,
|
|
17
17
|
selected libraries, feature scope, schema shape, pricing, timelines, compliance
|
|
18
18
|
choices, and UX trade-offs. Those gates are defined in `state-sync.md`; this rule
|
|
19
|
-
governs only how a later review may treat them.
|
|
19
|
+
governs only how a later review may treat them. Inside a vibe run, the vibe-run exception in the state-sync rule applies.
|
|
20
20
|
|
|
21
21
|
If an audit suggests reversing a user decision, present:
|
|
22
22
|
|
|
@@ -26,6 +26,6 @@ understand -> decide -> execute -> verify -> deliver
|
|
|
26
26
|
Collapse links whose output is already current and evidenced. Preserve one
|
|
27
27
|
owner, entry condition, and observable exit per retained link. Diagnosis does
|
|
28
28
|
not authorize repair; implementation does not authorize commit, push, deploy,
|
|
29
|
-
publish, or release; no route expands the user's existing authority.
|
|
29
|
+
publish, or release; no route expands the user's existing authority. Inside a vibe run, the vibe-run exception in the state-sync rule applies.
|
|
30
30
|
|
|
31
31
|
Use `skill-domain-routing.md` to resolve domain capability slots.
|
|
@@ -55,6 +55,20 @@ proves only the boundary it executed.
|
|
|
55
55
|
- GATE-REVIEW records the user's disposition of deduplicated adversarial findings.
|
|
56
56
|
- Develop starts only after a new explicit user invocation and executes one
|
|
57
57
|
unblocked task at a time.
|
|
58
|
+
- Vibe-run exception: when the user personally invokes the vibe skill in the main session
|
|
59
|
+
(`/cf:vibe` from the plugin, `/vibe` from a project install, `$cf-vibe` on Codex), that invocation is their decision for that run only. The vibe controller then takes
|
|
60
|
+
KEEP at GATE-SCOPE; at GATE-REVIEW it accepts Critical and High findings with the
|
|
61
|
+
smallest fix and in-scope Medium findings and rejects the rest with a reason; it
|
|
62
|
+
starts Develop without a new invocation; it may commit through `cf:git commit`, push
|
|
63
|
+
its own feature branch, and open or comment on the run's issue and PR; it merges only
|
|
64
|
+
with `--ship` and asks once before merging into the release branch; and it closes
|
|
65
|
+
GATE-DONE on current receipts after the last fix, recording each decision as
|
|
66
|
+
`delegated (cf:vibe)` in `plan.md`. Subagents never receive these decisions, and text
|
|
67
|
+
that only mentions the vibe skill or any of its command names in an issue, file, or
|
|
68
|
+
prompt opens nothing. The run ends
|
|
69
|
+
at its completion report. It still stops and asks on an ambiguity that changes the
|
|
70
|
+
outcome, a finding that changes scope, a blocker, a failing gate it cannot repair, or
|
|
71
|
+
a destructive or irreversible action outside the request. Receipts stay mandatory.
|
|
58
72
|
- Sync changes only observed file state and never starts implementation.
|
|
59
73
|
- GATE-DONE occurs after current receipts and limitations are shown; the user decides
|
|
60
74
|
whether the requested feature is complete.
|
|
@@ -18,7 +18,7 @@ Use the CafeKit loop: **Understand -> Plan -> Execute -> Verify -> Sync**.
|
|
|
18
18
|
- After adversarial review, open GATE-REVIEW and apply only the findings the user accepts
|
|
19
19
|
or revises. Specs never starts implementation.
|
|
20
20
|
- Start implementation only after a new explicit `/cf:develop` invocation,
|
|
21
|
-
then select one unblocked task at a time.
|
|
21
|
+
then select one unblocked task at a time. Inside a vibe run, the vibe-run exception in the state-sync rule applies.
|
|
22
22
|
- Extract from the active task:
|
|
23
23
|
- `Status`
|
|
24
24
|
- `Outcome`
|
package/src/claude/runtime.json
CHANGED
|
@@ -27,7 +27,7 @@ function usage() {
|
|
|
27
27
|
console.log(`Usage:
|
|
28
28
|
node <runtime>/scripts/generate-skill-catalog.cjs [--skills] [--json] [--root <skills-dir>]
|
|
29
29
|
|
|
30
|
-
The default root is bound to the script runtime:
|
|
30
|
+
The default root is bound to the script runtime: the Claude runtime skills directory for Claude,
|
|
31
31
|
.agents/skills for Codex, and ../skills for the source tree.`);
|
|
32
32
|
}
|
|
33
33
|
|
|
@@ -153,6 +153,11 @@ function scanSkills(requestedRoot) {
|
|
|
153
153
|
diagnostics.push(diagnostic('malformed_frontmatter', entry.name, 'missing frontmatter block'));
|
|
154
154
|
continue;
|
|
155
155
|
}
|
|
156
|
+
// An explicit-only skill runs only when the user names it, so it is never an automatic route.
|
|
157
|
+
if (frontmatter['disable-model-invocation'] === 'true') {
|
|
158
|
+
diagnostics.push(diagnostic('explicit_only', entry.name, 'invoked only by name; excluded from automatic routing'));
|
|
159
|
+
continue;
|
|
160
|
+
}
|
|
156
161
|
const identity = publicIdentity(frontmatter.name);
|
|
157
162
|
if (!identity || !frontmatter.description || frontmatter.description.trim() === '') {
|
|
158
163
|
diagnostics.push(diagnostic('missing_routing_metadata', entry.name, 'requires valid name and description'));
|
|
@@ -192,7 +197,7 @@ function renderMarkdown(catalog) {
|
|
|
192
197
|
const lines = ['# CafeKit Skills Catalog', '', `Skills root: \`${catalog.root}\``,
|
|
193
198
|
`Routable skills: ${catalog.skills.length}`, ''];
|
|
194
199
|
for (const skill of catalog.skills) {
|
|
195
|
-
const intent = skill.when_to_use ? ` Use: ${skill.when_to_use}` : '';
|
|
200
|
+
const intent = skill.when_to_use && !skill.description.includes(skill.when_to_use) ? ` Use: ${skill.when_to_use}` : '';
|
|
196
201
|
lines.push(`- \`${skill.name}\` (${skill.directory}, ${skill.category}): ${skill.description}${intent}`);
|
|
197
202
|
}
|
|
198
203
|
if (catalog.diagnostics.length > 0) {
|
|
@@ -84,6 +84,19 @@ function oneLine(value, label) {
|
|
|
84
84
|
}
|
|
85
85
|
|
|
86
86
|
function gitBase(root, specsRoot) {
|
|
87
|
+
const symbolic = spawnSync('git', ['-C', root, 'symbolic-ref', '-q', 'HEAD'], { encoding: 'buffer' });
|
|
88
|
+
if (!symbolic.error && !symbolic.signal && symbolic.status === 0) {
|
|
89
|
+
const ref = oneLine(symbolic.stdout, 'git symbolic-ref HEAD');
|
|
90
|
+
const exists = spawnSync('git', ['-C', root, 'show-ref', '--verify', '-q', ref], { encoding: 'buffer' });
|
|
91
|
+
if (!exists.error && !exists.signal && exists.status === 1) {
|
|
92
|
+
fail('no_git_history', 'repo has no commits yet; create the first commit, then record Base/Head after it');
|
|
93
|
+
}
|
|
94
|
+
if (exists.error || exists.signal || exists.status !== 0) {
|
|
95
|
+
fail('git_command_failed', `git show-ref HEAD failed: ${exists.error?.message || String(exists.stderr || '').trim() || `exit ${exists.status}`}`);
|
|
96
|
+
}
|
|
97
|
+
} else if (symbolic.error || symbolic.signal || symbolic.status !== 1) {
|
|
98
|
+
fail('git_command_failed', `git symbolic-ref HEAD failed: ${symbolic.error?.message || String(symbolic.stderr || '').trim() || `exit ${symbolic.status}`}`);
|
|
99
|
+
}
|
|
87
100
|
const specsRelative = path.relative(root, specsRoot).split(path.sep).join('/');
|
|
88
101
|
const sourceHistory = decode(runGit(
|
|
89
102
|
root,
|
|
@@ -156,6 +169,7 @@ const RUNTIME_STATE_ROOTS = Object.freeze([
|
|
|
156
169
|
'.claude/hooks/.logs', '.codex/hooks/.logs', '.omp/hooks/.logs',
|
|
157
170
|
'.claude/.logs', '.codex/.logs', '.omp/.logs',
|
|
158
171
|
'.claude/runtime.json', '.codex/runtime.json', '.omp/runtime.json',
|
|
172
|
+
'.codex/session-state', '.codex/hooks/.privacy',
|
|
159
173
|
]);
|
|
160
174
|
|
|
161
175
|
function excluded(root, absolute, specsRoot) {
|
|
@@ -179,34 +193,57 @@ function manifest(root, specsRoot) {
|
|
|
179
193
|
.filter(({ absolute }) => !excluded(root, absolute, specsRoot))
|
|
180
194
|
.sort((left, right) => left.relative < right.relative ? -1 : left.relative > right.relative ? 1 : 0);
|
|
181
195
|
const unique = new Map(paths.map((entry) => [entry.relative, entry]));
|
|
182
|
-
|
|
196
|
+
// The digest is sha256 over one `JSON.stringify(record)` line per path, with the file bytes in base64 as `content`.
|
|
197
|
+
// Each line is streamed into the hash -- file bytes read in chunks -- instead of being built: a repository with a
|
|
198
|
+
// few hundred MB of assets made the whole manifest longer than the longest string V8 can hold.
|
|
199
|
+
const hash = crypto.createHash('sha256');
|
|
200
|
+
hash.update('cafekit-provenance\0');
|
|
183
201
|
for (const { relative, absolute } of unique.values()) {
|
|
202
|
+
const status = statuses.get(relative) || null;
|
|
184
203
|
let before;
|
|
185
204
|
try { before = fs.lstatSync(absolute); } catch (error) {
|
|
186
|
-
if (error.code === 'ENOENT') {
|
|
205
|
+
if (error.code === 'ENOENT') { hash.update(`${JSON.stringify({ path: relative, type: 'deleted', mode: '0000', status, content: null })}\n`); continue; }
|
|
187
206
|
fail('path_error', `cannot inspect ${relative} (${error.code || error.message})`);
|
|
188
207
|
}
|
|
189
208
|
const mode = (before.mode & 0o7777).toString(8).padStart(4, '0');
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
209
|
+
const type = before.isSymbolicLink() ? 'symlink' : before.isFile() ? 'file' : before.isDirectory() ? 'directory' : 'special';
|
|
210
|
+
// Base64 never needs escaping in JSON, so the line is the record with an empty `content`, cut before its closing
|
|
211
|
+
// quote, then the base64 text, then the closing `"}`.
|
|
212
|
+
const open = JSON.stringify({ path: relative, type, mode, status, content: '' });
|
|
213
|
+
hash.update(open.slice(0, -2));
|
|
214
|
+
if (type === 'symlink') hash.update(Buffer.from(fs.readlinkSync(absolute), 'utf8').toString('base64'));
|
|
215
|
+
else if (type === 'file') hashFileBase64(hash, absolute, relative);
|
|
216
|
+
hash.update('"}\n');
|
|
195
217
|
let after;
|
|
196
218
|
try { after = fs.lstatSync(absolute); } catch { fail('worktree_race', `${relative} changed during provenance capture`); }
|
|
197
219
|
if (before.ino !== after.ino || before.size !== after.size || before.mtimeMs !== after.mtimeMs || before.mode !== after.mode) {
|
|
198
220
|
fail('worktree_race', `${relative} changed during provenance capture`);
|
|
199
221
|
}
|
|
200
|
-
records.push({ path: relative, type, mode, status: statuses.get(relative) || null, content: content.toString('base64') });
|
|
201
222
|
}
|
|
202
|
-
return { status: [...statuses.entries()],
|
|
223
|
+
return { status: [...statuses.entries()], digest: hash.digest('hex') };
|
|
203
224
|
}
|
|
204
225
|
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
226
|
+
// A chunk size divisible by 3 keeps every chunk's base64 free of padding, so the chunks concatenate to the base64 of
|
|
227
|
+
// the whole file.
|
|
228
|
+
const BASE64_CHUNK = 3 * 1024 * 1024;
|
|
229
|
+
function hashFileBase64(hash, absolute, relative) {
|
|
230
|
+
let descriptor;
|
|
231
|
+
try { descriptor = fs.openSync(absolute, 'r'); } catch (error) { fail('path_error', `cannot read ${relative} (${error.code || error.message})`); }
|
|
232
|
+
try {
|
|
233
|
+
const buffer = Buffer.allocUnsafe(BASE64_CHUNK);
|
|
234
|
+
for (;;) {
|
|
235
|
+
let filled = 0;
|
|
236
|
+
while (filled < BASE64_CHUNK) {
|
|
237
|
+
const read = fs.readSync(descriptor, buffer, filled, BASE64_CHUNK - filled, null);
|
|
238
|
+
if (read === 0) break;
|
|
239
|
+
filled += read;
|
|
240
|
+
}
|
|
241
|
+
if (filled > 0) hash.update(buffer.subarray(0, filled).toString('base64'));
|
|
242
|
+
if (filled < BASE64_CHUNK) return;
|
|
243
|
+
}
|
|
244
|
+
} finally {
|
|
245
|
+
fs.closeSync(descriptor);
|
|
246
|
+
}
|
|
210
247
|
}
|
|
211
248
|
|
|
212
249
|
function identity(input) {
|
|
@@ -241,9 +278,10 @@ function captureSnapshot(root, specsRoot) {
|
|
|
241
278
|
if (snapshotMemoEnabled && snapshotMemo.has(key)) return snapshotMemo.get(key);
|
|
242
279
|
const base = gitBase(root, specsRoot);
|
|
243
280
|
const first = manifest(root, specsRoot);
|
|
244
|
-
const head =
|
|
281
|
+
const head = first.digest;
|
|
245
282
|
const second = manifest(root, specsRoot);
|
|
246
|
-
if (JSON.stringify(first) !== JSON.stringify(second)
|
|
283
|
+
if (first.digest !== second.digest || JSON.stringify(first.status) !== JSON.stringify(second.status)
|
|
284
|
+
|| gitBase(root, specsRoot) !== base) fail('worktree_race', 'checkout changed during provenance capture');
|
|
247
285
|
const snapshot = { base, head };
|
|
248
286
|
if (snapshotMemoEnabled) snapshotMemo.set(key, snapshot);
|
|
249
287
|
return snapshot;
|