@haposoft/cafekit 0.18.2 → 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 +1 -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__/codex-hooks.test.js +38 -6
- package/bin/__tests__/codex-implicit-policy.test.js +160 -0
- package/bin/__tests__/codex-native.test.js +15 -8
- 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-runtime-layout.test.js +81 -0
- package/bin/__tests__/codex-setup-agents.test.js +216 -0
- package/bin/__tests__/legacy-hooks-retired.test.js +63 -13
- 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 +81 -5
- package/bin/__tests__/omp-hooks.test.js +24 -0
- package/bin/__tests__/plugin-safety-keys.test.js +121 -0
- package/bin/__tests__/privacy-paths.test.js +184 -0
- package/bin/__tests__/usage-hook-retired.test.js +24 -6
- package/bin/__tests__/vibe-run.test.js +215 -0
- package/bin/__tests__/vibe-skill.test.js +549 -0
- package/bin/install.js +99 -1
- package/bin/lib/backup.js +43 -24
- package/bin/lib/codex-install.js +7 -0
- package/bin/lib/context.js +5 -0
- 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 +5 -3
- package/bin/phases/codex-hooks.js +38 -0
- package/bin/phases/codex-runtime.js +106 -74
- package/bin/phases/copy-payload.js +49 -1
- 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 +1 -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/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 +7 -1
- package/src/claude/hooks/__tests__/privacy-block.test.js +33 -9
- package/src/claude/hooks/__tests__/setup-statusline.test.js +179 -0
- package/src/claude/hooks/__tests__/spec-gate.test.js +86 -1
- package/src/claude/hooks/agent.cjs +5 -2
- package/src/claude/hooks/inspect-block.cjs +3 -1
- package/src/claude/hooks/lib/hook-state-dir.cjs +19 -1
- package/src/claude/hooks/lib/plugin-options.cjs +47 -0
- package/src/claude/hooks/lib/privacy-command-analysis.cjs +130 -11
- package/src/claude/hooks/lib/privacy-paths.cjs +370 -10
- package/src/claude/hooks/lib/spec-touch.cjs +14 -1
- package/src/claude/hooks/plugin-context.cjs +80 -0
- package/src/claude/hooks/privacy-block.cjs +2 -1
- package/src/claude/hooks/rules.cjs +5 -2
- package/src/claude/hooks/session.cjs +4 -1
- package/src/claude/hooks/spec-state.cjs +5 -4
- package/src/claude/hooks/state.cjs +5 -1
- package/src/claude/migration-manifest.json +6 -1
- package/src/claude/plugin/commands/setup-statusline.md +12 -0
- package/src/claude/plugin/core.md +71 -0
- 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/scripts/generate-skill-catalog.cjs +5 -0
- package/src/claude/scripts/provenance.cjs +40 -16
- package/src/claude/scripts/setup-statusline.cjs +88 -0
- 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/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 +4 -2
- package/src/claude/skills/git/references/merge-pr.md +125 -0
- 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 +1 -1
- package/src/codex/hooks/inspect-block.cjs +7 -4
- package/src/codex/hooks/lib/hook-context.cjs +118 -4
- package/src/codex/hooks/lib/plugin-options.cjs +2 -0
- package/src/codex/hooks/lib/privacy-state.cjs +18 -1
- package/src/codex/hooks/lib/state-store.cjs +5 -4
- package/src/codex/hooks/precompact.cjs +9 -8
- package/src/codex/hooks/privacy-approval.cjs +2 -2
- package/src/codex/hooks/privacy-block.cjs +5 -4
- package/src/codex/hooks/rules.cjs +12 -6
- package/src/codex/hooks/session.cjs +22 -10
- 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/scripts/setup-agents.cjs +124 -0
- package/src/common/AGENTS.md +1 -1
- package/src/omp/extensions/{cafekit-bridge.mjs → cafekit-bridge.js} +73 -5
- package/src/omp/hooks/privacy-block.cjs +2 -1
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Install the CafeKit agents shipped in the Codex plugin into the user's Codex agents
|
|
6
|
+
* directory ($CODEX_HOME/agents, default ~/.codex/agents). Codex loads no agents from a
|
|
7
|
+
* plugin, so this runs once after the plugin is installed or updated.
|
|
8
|
+
*
|
|
9
|
+
* Every shipped file starts with a marker naming the plugin, its version and the sha256
|
|
10
|
+
* of the body below it. A file counts as CafeKit's own only when its marker hash still
|
|
11
|
+
* matches its body; only those are replaced. Anything else stays byte for byte:
|
|
12
|
+
* a missing marker, an edited body, or a symlink.
|
|
13
|
+
*
|
|
14
|
+
* A file it cannot read is kept and reported, and the other agents still install.
|
|
15
|
+
* Takes no arguments: the target is never chosen by the caller.
|
|
16
|
+
* Exit: 0 done, 1 failure, 2 usage.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
const fs = require('fs');
|
|
20
|
+
const os = require('os');
|
|
21
|
+
const path = require('path');
|
|
22
|
+
const crypto = require('crypto');
|
|
23
|
+
const { spawnSync } = require('child_process');
|
|
24
|
+
|
|
25
|
+
const MARKER = /^# CafeKit agent: plugin=(\S+) version=(\S+) sha256=([0-9a-f]{64})\n/;
|
|
26
|
+
|
|
27
|
+
const sha256 = (text) => crypto.createHash('sha256').update(text).digest('hex');
|
|
28
|
+
|
|
29
|
+
/** 'absent' | 'symlink' | 'foreign' | 'modified' | 'own' */
|
|
30
|
+
function ownership(file) {
|
|
31
|
+
let stat;
|
|
32
|
+
try {
|
|
33
|
+
stat = fs.lstatSync(file);
|
|
34
|
+
} catch (error) {
|
|
35
|
+
if (error.code === 'ENOENT') return 'absent';
|
|
36
|
+
throw error;
|
|
37
|
+
}
|
|
38
|
+
if (stat.isSymbolicLink()) return 'symlink';
|
|
39
|
+
if (!stat.isFile()) return 'foreign';
|
|
40
|
+
const text = fs.readFileSync(file, 'utf8');
|
|
41
|
+
const marker = text.match(MARKER);
|
|
42
|
+
if (!marker) return 'foreign';
|
|
43
|
+
return sha256(text.slice(marker[0].length)) === marker[3] ? 'own' : 'modified';
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Write beside the target and rename over it, so a reader never sees half a file.
|
|
47
|
+
function writeAtomic(file, data, mode) {
|
|
48
|
+
const temp = path.join(path.dirname(file), `.${path.basename(file)}.${process.pid}.${crypto.randomBytes(6).toString('hex')}.tmp`);
|
|
49
|
+
try {
|
|
50
|
+
fs.writeFileSync(temp, data, { mode, flag: 'wx' });
|
|
51
|
+
fs.chmodSync(temp, mode);
|
|
52
|
+
fs.renameSync(temp, file);
|
|
53
|
+
} catch (error) {
|
|
54
|
+
fs.rmSync(temp, { force: true });
|
|
55
|
+
throw error;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Codex reads agents from its home, so a session started with CODEX_HOME installs there.
|
|
60
|
+
// A relative CODEX_HOME is refused rather than guessed, so it never falls back to ~/.codex.
|
|
61
|
+
function codexHome() {
|
|
62
|
+
const configured = process.env.CODEX_HOME;
|
|
63
|
+
if (!configured) return path.join(os.homedir(), '.codex');
|
|
64
|
+
if (!path.isAbsolute(configured)) throw new Error(`CODEX_HOME must be an absolute path: ${configured}`);
|
|
65
|
+
return configured;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function repositoryRoot() {
|
|
69
|
+
const result = spawnSync('git', ['rev-parse', '--show-toplevel'], { encoding: 'utf8', timeout: 5000 });
|
|
70
|
+
return result.status === 0 && result.stdout.trim() ? result.stdout.trim() : process.cwd();
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function main(argv) {
|
|
74
|
+
if (argv.length > 0) {
|
|
75
|
+
process.stderr.write('setup-agents takes no arguments; it installs into $CODEX_HOME/agents (default ~/.codex/agents).\n');
|
|
76
|
+
return 2;
|
|
77
|
+
}
|
|
78
|
+
const sourceDir = path.join(__dirname, '..', 'agents');
|
|
79
|
+
const targetDir = path.join(codexHome(), 'agents');
|
|
80
|
+
const repoAgents = path.join(repositoryRoot(), '.codex', 'agents');
|
|
81
|
+
const files = fs.readdirSync(sourceDir).filter((name) => name.endsWith('.toml')).sort();
|
|
82
|
+
fs.mkdirSync(targetDir, { recursive: true });
|
|
83
|
+
|
|
84
|
+
const counts = {};
|
|
85
|
+
for (const name of files) {
|
|
86
|
+
const shipped = fs.readFileSync(path.join(sourceDir, name), 'utf8');
|
|
87
|
+
const target = path.join(targetDir, name);
|
|
88
|
+
let state;
|
|
89
|
+
try {
|
|
90
|
+
state = ownership(target);
|
|
91
|
+
} catch {
|
|
92
|
+
state = 'unreadable';
|
|
93
|
+
}
|
|
94
|
+
let action;
|
|
95
|
+
if (state === 'absent') {
|
|
96
|
+
writeAtomic(target, shipped, 0o644);
|
|
97
|
+
action = 'written';
|
|
98
|
+
} else if (state === 'own') {
|
|
99
|
+
if (fs.readFileSync(target, 'utf8') === shipped) {
|
|
100
|
+
action = 'unchanged';
|
|
101
|
+
} else {
|
|
102
|
+
writeAtomic(target, shipped, fs.statSync(target).mode & 0o777);
|
|
103
|
+
action = 'replaced';
|
|
104
|
+
}
|
|
105
|
+
} else {
|
|
106
|
+
action = { symlink: 'kept (symlink)', foreign: 'kept (not installed by CafeKit)', modified: 'kept (modified)', unreadable: 'kept (unreadable)' }[state];
|
|
107
|
+
}
|
|
108
|
+
counts[action] = (counts[action] || 0) + 1;
|
|
109
|
+
process.stdout.write(`${action}: ${target}\n`);
|
|
110
|
+
if (fs.existsSync(path.join(repoAgents, name))) {
|
|
111
|
+
process.stdout.write(`collision: ${path.join(repoAgents, name)} has the same agent name in this repository\n`);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
const summary = Object.entries(counts).map(([action, count]) => `${count} ${action}`).join(', ');
|
|
115
|
+
process.stdout.write(`CafeKit agents in ${targetDir}: ${summary}\n`);
|
|
116
|
+
return 0;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
try {
|
|
120
|
+
process.exitCode = main(process.argv.slice(2));
|
|
121
|
+
} catch (error) {
|
|
122
|
+
process.stderr.write(`setup-agents failed: ${error.message}\n`);
|
|
123
|
+
process.exitCode = 1;
|
|
124
|
+
}
|
package/src/common/AGENTS.md
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
keeps canonical execution proof in its final inline `## Receipt`.
|
|
9
9
|
- Specs uses three user decisions: GATE-SCOPE for scope, GATE-REVIEW for adversarial findings,
|
|
10
10
|
and GATE-DONE for completion. Planning never starts implementation; implementation
|
|
11
|
-
requires a new explicit user invocation.
|
|
11
|
+
requires a new explicit user invocation. A vibe run the user invokes delegates these gates as the state-sync rule's vibe-run exception states.
|
|
12
12
|
- Synchronize only observed task state with surgical edits. Never invent proof,
|
|
13
13
|
readiness, approval, review independence, or completed work.
|
|
14
14
|
- `NO_TESTS` and `0 tests + exit 0` do not pass when the task requires automated tests.
|
|
@@ -9,7 +9,11 @@
|
|
|
9
9
|
* payload the scripts read, runs the scripts as child processes, and translates their
|
|
10
10
|
* verdicts back into omp's contract.
|
|
11
11
|
*
|
|
12
|
-
*
|
|
12
|
+
* Facts read from the installed omp binary drive the design:
|
|
13
|
+
* - omp auto-loads only `<name>.{ts,js}` and `<dir>/index.{ts,js}` from `.omp/extensions/`, so this
|
|
14
|
+
* file is `.js`; an `.mjs` bridge is never loaded and every gate is silently off.
|
|
15
|
+
* - omp is a Bun binary: `process.execPath` is omp itself, not node, so the hooks are run
|
|
16
|
+
* with a `node` found on PATH (see hookRunner).
|
|
13
17
|
* - omp fails CLOSED at `tool_call`: a handler that throws or exceeds
|
|
14
18
|
* `extensionHandlers.toolCallTimeoutMs` (default 30000) is replaced by omp's own
|
|
15
19
|
* `{ block: true, reason: "Extension … timed out" }`. The bridge therefore keeps a
|
|
@@ -26,7 +30,7 @@
|
|
|
26
30
|
|
|
27
31
|
import { spawn } from 'node:child_process';
|
|
28
32
|
import { randomUUID } from 'node:crypto';
|
|
29
|
-
import { existsSync } from 'node:fs';
|
|
33
|
+
import { accessSync, constants, existsSync, realpathSync, statSync } from 'node:fs';
|
|
30
34
|
import path from 'node:path';
|
|
31
35
|
import { fileURLToPath } from 'node:url';
|
|
32
36
|
|
|
@@ -62,6 +66,22 @@ function sessionIdFrom(ctx) {
|
|
|
62
66
|
} catch { return null; }
|
|
63
67
|
}
|
|
64
68
|
|
|
69
|
+
/**
|
|
70
|
+
* Only the file paths of a tool result's details. An edit's details also carry the old and
|
|
71
|
+
* new text of every file it changed, which no hook needs on stdin.
|
|
72
|
+
*/
|
|
73
|
+
function resultPaths(details) {
|
|
74
|
+
if (!details || typeof details !== 'object') return {};
|
|
75
|
+
const paths = (list) => (Array.isArray(list) ? list.filter((entry) => typeof entry?.path === 'string').map((entry) => ({ path: entry.path })) : undefined);
|
|
76
|
+
const out = {};
|
|
77
|
+
if (typeof details.path === 'string') out.path = details.path;
|
|
78
|
+
const perFile = paths(details.perFileResults);
|
|
79
|
+
if (perFile) out.perFileResults = perFile;
|
|
80
|
+
const changes = paths(details.fileChanges);
|
|
81
|
+
if (changes) out.fileChanges = changes;
|
|
82
|
+
return out;
|
|
83
|
+
}
|
|
84
|
+
|
|
65
85
|
/** Build the Claude-shaped payload a CafeKit hook reads on stdin. */
|
|
66
86
|
export function shapePayload(kind, event, ctx) {
|
|
67
87
|
const entry = DISPATCH[kind];
|
|
@@ -93,7 +113,7 @@ export function shapePayload(kind, event, ctx) {
|
|
|
93
113
|
case 'tool_result':
|
|
94
114
|
payload.tool_name = event?.toolName ?? event?.tool_name ?? '';
|
|
95
115
|
payload.tool_input = event?.input ?? {};
|
|
96
|
-
payload.tool_response = { content: event?.content ?? [], isError: event?.isError === true };
|
|
116
|
+
payload.tool_response = { content: event?.content ?? [], isError: event?.isError === true, details: resultPaths(event?.details) };
|
|
97
117
|
payload.tool_use_id = event?.toolCallId ?? null;
|
|
98
118
|
break;
|
|
99
119
|
case 'session_stop':
|
|
@@ -143,12 +163,54 @@ export function interpretVerdict({ code, stdout = '', stderr = '' }, hookName) {
|
|
|
143
163
|
return { block: Boolean(block), reason: block?.reason, inject: inject.join('\n').trim() };
|
|
144
164
|
}
|
|
145
165
|
|
|
166
|
+
function isInside(root, target) {
|
|
167
|
+
const rel = path.relative(root, target);
|
|
168
|
+
return rel === '' || (!rel.startsWith('..') && !path.isAbsolute(rel));
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function realOrSelf(target) {
|
|
172
|
+
try { return realpathSync(target); } catch { return path.resolve(target); }
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* The executable that runs a hook. Under node that is `process.execPath`. Under Bun (omp)
|
|
177
|
+
* it is the first `node` on PATH that sits in an absolute PATH entry outside the project,
|
|
178
|
+
* so a repository cannot plant its own `node` (`.`, `node_modules/.bin`) in front of the
|
|
179
|
+
* gates. Returns `{ command }` or `{ error }`; never throws.
|
|
180
|
+
*/
|
|
181
|
+
export function hookRunner({ cwd = process.cwd(), env = process.env, bun = process.versions?.bun, platform = process.platform } = {}) {
|
|
182
|
+
if (!bun) return { command: process.execPath };
|
|
183
|
+
const project = realOrSelf(cwd);
|
|
184
|
+
const names = platform === 'win32'
|
|
185
|
+
? String(env.PATHEXT || '.EXE;.CMD;.BAT;.COM').split(';').filter(Boolean).map((ext) => `node${ext.toLowerCase()}`)
|
|
186
|
+
: ['node'];
|
|
187
|
+
const delimiter = platform === 'win32' ? ';' : ':';
|
|
188
|
+
for (const dir of String(env.PATH || env.Path || '').split(delimiter)) {
|
|
189
|
+
if (!dir || !path.isAbsolute(dir)) continue;
|
|
190
|
+
for (const name of names) {
|
|
191
|
+
const candidate = path.join(dir, name);
|
|
192
|
+
try {
|
|
193
|
+
if (!statSync(candidate).isFile()) continue;
|
|
194
|
+
if (platform !== 'win32') accessSync(candidate, constants.X_OK);
|
|
195
|
+
} catch { continue; }
|
|
196
|
+
if (isInside(project, realOrSelf(candidate))) continue;
|
|
197
|
+
return { command: candidate };
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
return { error: 'node was not found on PATH outside the project' };
|
|
201
|
+
}
|
|
202
|
+
|
|
146
203
|
/** Run one hook as a child process with a hard budget. Never throws. */
|
|
147
204
|
export function dispatchHook(hookPath, payload, { cwd, timeoutMs = BRIDGE_TIMEOUT_MS } = {}) {
|
|
148
205
|
return new Promise((resolve) => {
|
|
206
|
+
const runner = hookRunner({ cwd });
|
|
207
|
+
if (runner.error) {
|
|
208
|
+
resolve({ code: null, signal: null, stdout: '', stderr: runner.error, timedOut: false, failed: true });
|
|
209
|
+
return;
|
|
210
|
+
}
|
|
149
211
|
let child;
|
|
150
212
|
try {
|
|
151
|
-
child = spawn(
|
|
213
|
+
child = spawn(runner.command, [hookPath], {
|
|
152
214
|
cwd,
|
|
153
215
|
env: { ...process.env, PROJECT_ROOT: cwd },
|
|
154
216
|
stdio: ['pipe', 'pipe', 'pipe'],
|
|
@@ -167,7 +229,7 @@ export function dispatchHook(hookPath, payload, { cwd, timeoutMs = BRIDGE_TIMEOU
|
|
|
167
229
|
child.stdout.on('data', (c) => out.push(c));
|
|
168
230
|
child.stderr.on('data', (c) => err.push(c));
|
|
169
231
|
child.on('error', (error) => { clearTimeout(timer); finish({ code: null, stdout: '', stderr: String(error?.message || error), timedOut: false, failed: true }); });
|
|
170
|
-
child.on('close', (code) => { clearTimeout(timer); finish({ code, stdout: Buffer.concat(out).toString('utf8'), stderr: Buffer.concat(err).toString('utf8'), timedOut: false, failed: false }); });
|
|
232
|
+
child.on('close', (code, signal) => { clearTimeout(timer); finish({ code, signal, stdout: Buffer.concat(out).toString('utf8'), stderr: Buffer.concat(err).toString('utf8'), timedOut: false, failed: false }); });
|
|
171
233
|
try { child.stdin.write(JSON.stringify(payload)); child.stdin.end(); } catch { /* close handler still runs */ }
|
|
172
234
|
});
|
|
173
235
|
}
|
|
@@ -206,6 +268,12 @@ export async function runEvent(kind, event, ctx, { timeoutMs = BRIDGE_TIMEOUT_MS
|
|
|
206
268
|
}
|
|
207
269
|
const verdict = interpretVerdict(result, hookName);
|
|
208
270
|
if (verdict.block) return { block: true, reason: verdict.reason, hook: hookName };
|
|
271
|
+
// A gate that dies without a verdict (a broken node shim, a crash, a kill) must not
|
|
272
|
+
// read as an allow: only 0 and 2 are answers a gate gives on purpose.
|
|
273
|
+
if (gating && result.code !== 0 && result.code !== 2) {
|
|
274
|
+
const how = result.signal ? `was killed by ${result.signal}` : `exited with code ${result.code}`;
|
|
275
|
+
return { block: true, reason: `CafeKit gate ${hookName} ${how}`, hook: hookName };
|
|
276
|
+
}
|
|
209
277
|
if (verdict.inject) injections.push(verdict.inject);
|
|
210
278
|
}
|
|
211
279
|
return { block: false, inject: injections.join('\n\n') };
|
|
@@ -56,7 +56,8 @@ try {
|
|
|
56
56
|
const projectRoot = resolveProjectRoot(data);
|
|
57
57
|
const runtime = readRuntime(projectRoot);
|
|
58
58
|
|
|
59
|
-
|
|
59
|
+
// A plugin is trusted once for every repository, so a repository cannot switch it off.
|
|
60
|
+
if (runtime.privacyBlock === false && !require('./lib/plugin-options.cjs').isPlugin()) process.exit(0);
|
|
60
61
|
|
|
61
62
|
const paths = sensitivePaths(toolName, toolInput, cwd);
|
|
62
63
|
if (paths.length) askForPermission(`Sensitive file access requires approval: ${displayNames(paths)}`);
|