@ngockhoale/ukit 2.2.1 → 2.2.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/CHANGELOG.md +47 -0
- package/manifests/platform.full.yaml +54 -0
- package/package.json +1 -1
- package/templates/.claude/commands/ukit/handoff-clear.md +1 -1
- package/templates/.claude/commands/ukit/handoff-create.md +4 -4
- package/templates/.claude/commands/ukit/handoff-fullstack.md +11 -9
- package/templates/.claude/commands/ukit/handoff-implement.md +2 -2
- package/templates/.claude/commands/ukit/handoff-review.md +1 -1
- package/templates/.claude/commands/ukit/handoff-status.md +1 -1
- package/templates/.claude/hooks/completion-gate.sh +12 -0
- package/templates/.claude/hooks/record-execution.sh +12 -0
- package/templates/.claude/settings.json +31 -0
- package/templates/.claude/ukit/index/unic-gateway.mjs +23 -26
- package/templates/.claude/ukit/runtime/execution-ledger.mjs +354 -0
- package/templates/.claude/ukit/runtime/hook-chain-runner.mjs +115 -0
- package/templates/.omp/RULES.md +40 -12
- package/templates/.omp/config.yml +12 -9
- package/templates/.omp/hooks/pre/ukit-bridge.js +310 -181
- package/templates/AGENTS.md +131 -63
- package/templates/CLAUDE.md +59 -21
|
@@ -1,32 +1,19 @@
|
|
|
1
|
-
// ukit-bridge.js — omp
|
|
1
|
+
// ukit-bridge.js — omp extension bridge for UKit.
|
|
2
2
|
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
// 1. Maps an omp event + tool name onto the ordered list of scripts that
|
|
8
|
-
// `.claude/settings.json` would have run for the equivalent Claude Code
|
|
9
|
-
// event (see HOOK_EVENT_MAP, generated by hand from
|
|
10
|
-
// `templates/.claude/settings.json` — keep them in sync).
|
|
11
|
-
// 2. Builds the same stdin JSON payload the scripts already expect
|
|
12
|
-
// (hook_event_name, tool_name, tool_input, session_id, cwd, ...).
|
|
13
|
-
// 3. Executes each script via `pi.exec()` (never spawns/copies the script
|
|
14
|
-
// body) and translates the exit code back into an omp-shaped result.
|
|
15
|
-
//
|
|
16
|
-
// omp's own `pi.exec` / `pi.on` argument shapes are UNVERIFIED (omp is not
|
|
17
|
-
// installed in this environment, no source/schema vendored — PLAN.md D7).
|
|
18
|
-
// The internal contract chosen below (see doc comments on each exported
|
|
19
|
-
// function) is a reasonable, internally consistent design; it is exercised
|
|
20
|
-
// end-to-end against a fake `pi` in tests/hooks/ompHookBridge.test.js.
|
|
3
|
+
// The scripts under `.claude/hooks/` remain the single source of hook behavior.
|
|
4
|
+
// This adapter normalizes omp's v17.4.2 extension events into Claude Code hook
|
|
5
|
+
// payloads, executes the same ordered script chains, and translates only the
|
|
6
|
+
// result fields that omp consumes.
|
|
21
7
|
|
|
22
8
|
import path from 'node:path';
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
9
|
+
import { fileURLToPath } from 'node:url';
|
|
10
|
+
import {
|
|
11
|
+
evaluateCompletion,
|
|
12
|
+
incrementContinuation,
|
|
13
|
+
readExecutionLedger,
|
|
14
|
+
readRouteState,
|
|
15
|
+
recordExecutionReceipt,
|
|
16
|
+
} from '../../../.claude/ukit/runtime/execution-ledger.mjs';
|
|
30
17
|
|
|
31
18
|
export const HOOK_EVENT_MAP = {
|
|
32
19
|
tool_call: {
|
|
@@ -50,38 +37,32 @@ export const HOOK_EVENT_MAP = {
|
|
|
50
37
|
],
|
|
51
38
|
},
|
|
52
39
|
tool_result: {
|
|
53
|
-
'
|
|
54
|
-
|
|
40
|
+
'Read|Grep|Glob': ['record-execution.sh'],
|
|
41
|
+
'Edit|Write': ['post-edit-verify.sh', 'record-execution.sh'],
|
|
42
|
+
Bash: ['compress-output.sh', 'record-execution.sh'],
|
|
55
43
|
},
|
|
56
|
-
|
|
44
|
+
before_agent_start: ['skill-router.sh', 'vision-router.sh', 'context-window-guard.sh'],
|
|
57
45
|
'session.compacting': ['reinject-context.sh'],
|
|
58
46
|
session_start: ['auto-prune-bash.sh', 'reset-compact-pressure.sh', 'handoff-resume.sh'],
|
|
59
47
|
};
|
|
60
48
|
|
|
61
|
-
// ---------------------------------------------------------------------------
|
|
62
|
-
// Tool-name mapping (PLAN.md D2).
|
|
63
|
-
//
|
|
64
|
-
// omp's write surface is `edit`, `write`, AND `ast_edit` — `ast_edit` must
|
|
65
|
-
// map to the same `Edit` matcher group as `edit`/`write`, or it silently
|
|
66
|
-
// bypasses protect-files.sh / vision-gate.sh. `eval` is omp's shell-capable
|
|
67
|
-
// tool and must map to `Bash` to hit block-dangerous.sh / verification-guard.sh.
|
|
68
|
-
// Any tool name not in this table maps to `null`, which runs ZERO scripts —
|
|
69
|
-
// it must never silently fall back to Bash or Edit.
|
|
70
|
-
// ---------------------------------------------------------------------------
|
|
71
|
-
|
|
72
49
|
const TOOL_NAME_MAP = {
|
|
73
50
|
read: 'Read',
|
|
51
|
+
read_file: 'Read',
|
|
74
52
|
grep: 'Grep',
|
|
75
53
|
glob: 'Glob',
|
|
76
54
|
edit: 'Edit',
|
|
77
55
|
write: 'Write',
|
|
78
56
|
ast_edit: 'Edit',
|
|
57
|
+
apply_patch: 'Edit',
|
|
79
58
|
eval: 'Bash',
|
|
80
59
|
bash: 'Bash',
|
|
60
|
+
shell: 'Bash',
|
|
81
61
|
};
|
|
82
62
|
|
|
83
63
|
export function mapToolName(ompToolName) {
|
|
84
|
-
|
|
64
|
+
const normalized = String(ompToolName ?? '').trim().toLowerCase().replaceAll('-', '_');
|
|
65
|
+
return TOOL_NAME_MAP[normalized] ?? null;
|
|
85
66
|
}
|
|
86
67
|
|
|
87
68
|
function matcherGroupFor(claudeToolName) {
|
|
@@ -97,14 +78,6 @@ function matcherGroupFor(claudeToolName) {
|
|
|
97
78
|
return null;
|
|
98
79
|
}
|
|
99
80
|
|
|
100
|
-
// ---------------------------------------------------------------------------
|
|
101
|
-
// Fail-direction classification (PLAN.md D3). Not a blanket rule -- each
|
|
102
|
-
// script's own header documents its own fail direction; this table is a
|
|
103
|
-
// transcription of those 18 headers, not an invented policy. Gate scripts
|
|
104
|
-
// fail CLOSED (non-zero/throw => block). Advisory scripts fail OPEN
|
|
105
|
-
// (non-zero/throw => log a warning, never block).
|
|
106
|
-
// ---------------------------------------------------------------------------
|
|
107
|
-
|
|
108
81
|
export const FAIL_CLOSED_SCRIPTS = new Set([
|
|
109
82
|
'protect-files.sh',
|
|
110
83
|
'stale-spec-guard.sh',
|
|
@@ -117,6 +90,7 @@ export const FAIL_CLOSED_SCRIPTS = new Set([
|
|
|
117
90
|
|
|
118
91
|
export const ADVISORY_SCRIPTS = new Set([
|
|
119
92
|
'skill-router.sh',
|
|
93
|
+
'record-execution.sh',
|
|
120
94
|
'auto-allow-bash.sh',
|
|
121
95
|
'pre-edit-backup.sh',
|
|
122
96
|
'vision-router.sh',
|
|
@@ -130,46 +104,47 @@ export const ADVISORY_SCRIPTS = new Set([
|
|
|
130
104
|
]);
|
|
131
105
|
|
|
132
106
|
function classifyFailure(scriptName) {
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
107
|
+
return FAIL_CLOSED_SCRIPTS.has(scriptName) ? 'closed' : 'open';
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function runtimeMetadata(event = {}, context = {}) {
|
|
111
|
+
const sessionManager = context?.sessionManager;
|
|
112
|
+
return {
|
|
113
|
+
sessionId: event.sessionId ?? event.session_id ?? sessionManager?.getSessionId?.(),
|
|
114
|
+
cwd: context?.cwd ?? event.cwd,
|
|
115
|
+
transcriptPath: event.transcriptPath ?? event.transcript_path ?? sessionManager?.getSessionFile?.(),
|
|
116
|
+
};
|
|
138
117
|
}
|
|
139
118
|
|
|
140
|
-
|
|
141
|
-
// Payload + result translation.
|
|
142
|
-
// ---------------------------------------------------------------------------
|
|
143
|
-
|
|
144
|
-
/**
|
|
145
|
-
* Builds the same stdin JSON shape the `.sh` scripts already read via
|
|
146
|
-
* `INPUT=$(cat)`. Fields the caller does not supply are simply omitted --
|
|
147
|
-
* the scripts already degrade gracefully when optional fields (e.g.
|
|
148
|
-
* transcript_path, prompt) are absent (see context-window-guard.sh, which
|
|
149
|
-
* exits 0 immediately when transcript_path is missing).
|
|
150
|
-
*/
|
|
151
|
-
function buildHookPayload(hookEventName, { toolName, toolInput, sessionId, cwd, prompt, transcriptPath } = {}) {
|
|
119
|
+
function buildHookPayload(hookEventName, fields = {}) {
|
|
152
120
|
const payload = { hook_event_name: hookEventName };
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
121
|
+
const mappings = [
|
|
122
|
+
['toolName', 'tool_name'],
|
|
123
|
+
['toolInput', 'tool_input'],
|
|
124
|
+
['toolOutput', 'tool_output'],
|
|
125
|
+
['toolResult', 'tool_result'],
|
|
126
|
+
['toolUseId', 'tool_use_id'],
|
|
127
|
+
['sessionId', 'session_id'],
|
|
128
|
+
['cwd', 'cwd'],
|
|
129
|
+
['prompt', 'prompt'],
|
|
130
|
+
['transcriptPath', 'transcript_path'],
|
|
131
|
+
['source', 'source'],
|
|
132
|
+
];
|
|
133
|
+
for (const [field, payloadField] of mappings) {
|
|
134
|
+
if (fields[field] !== undefined && fields[field] !== null) {
|
|
135
|
+
payload[payloadField] = fields[field];
|
|
136
|
+
}
|
|
137
|
+
}
|
|
159
138
|
return payload;
|
|
160
139
|
}
|
|
161
140
|
|
|
162
|
-
/**
|
|
163
|
-
* Translates a single script's `pi.exec` outcome into a bridge-internal
|
|
164
|
-
* verdict. Exit 0 => pass. Exit 2 => this script's own explicit "block"
|
|
165
|
-
* signal, regardless of gate/advisory class (mirrors Claude Code's own
|
|
166
|
-
* "exit 2 = block, stderr = reason" contract). Any other non-zero exit, or
|
|
167
|
-
* a thrown error, is resolved via the script's fail-direction classification.
|
|
168
|
-
*/
|
|
169
141
|
function translateExecResult(scriptName, execResult) {
|
|
170
|
-
const
|
|
142
|
+
const killed = Boolean(execResult?.killed);
|
|
143
|
+
const code = killed ? 1 : (execResult?.code ?? 0);
|
|
171
144
|
const stdout = execResult?.stdout ?? '';
|
|
172
|
-
const stderr =
|
|
145
|
+
const stderr = killed
|
|
146
|
+
? (execResult?.stderr || `${scriptName} was killed before it completed`)
|
|
147
|
+
: (execResult?.stderr ?? '');
|
|
173
148
|
|
|
174
149
|
if (code === 0) {
|
|
175
150
|
return { block: false, stdout, stderr };
|
|
@@ -177,9 +152,7 @@ function translateExecResult(scriptName, execResult) {
|
|
|
177
152
|
if (code === 2) {
|
|
178
153
|
return { block: true, reason: stderr || `${scriptName} exited 2 (blocked)`, stdout, stderr };
|
|
179
154
|
}
|
|
180
|
-
|
|
181
|
-
const direction = classifyFailure(scriptName);
|
|
182
|
-
if (direction === 'closed') {
|
|
155
|
+
if (classifyFailure(scriptName) === 'closed') {
|
|
183
156
|
return {
|
|
184
157
|
block: true,
|
|
185
158
|
reason: stderr || `${scriptName} exited ${code} (failing closed)`,
|
|
@@ -187,150 +160,306 @@ function translateExecResult(scriptName, execResult) {
|
|
|
187
160
|
stderr,
|
|
188
161
|
};
|
|
189
162
|
}
|
|
190
|
-
return {
|
|
163
|
+
return {
|
|
164
|
+
block: false,
|
|
165
|
+
warning: `${scriptName} exited ${code} (failing open): ${stderr || 'no stderr'}`,
|
|
166
|
+
stdout,
|
|
167
|
+
stderr,
|
|
168
|
+
};
|
|
191
169
|
}
|
|
192
170
|
|
|
193
171
|
export { translateExecResult };
|
|
194
172
|
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
// fail-direction / short-circuit / context-accumulation logic in exactly
|
|
198
|
-
// one place.
|
|
199
|
-
// ---------------------------------------------------------------------------
|
|
200
|
-
|
|
201
|
-
/**
|
|
202
|
-
* Runs `scripts` (basenames under `.claude/hooks/`) in order via
|
|
203
|
-
* `pi.exec(absoluteScriptPath, { input: JSON.stringify(payload) })`,
|
|
204
|
-
* short-circuiting on the first block. Returns
|
|
205
|
-
* { block, reason?, context, invoked }
|
|
206
|
-
* where `context` is the concatenation of each script's trimmed, non-empty
|
|
207
|
-
* stdout (used by session.compacting / session_start to surface
|
|
208
|
-
* reinject-context.sh / handoff-resume.sh output back to omp).
|
|
209
|
-
*/
|
|
173
|
+
const HOOK_CHAIN_TIMEOUT_MS = 12000;
|
|
174
|
+
|
|
210
175
|
export async function runScriptChain(pi, scripts, payload, { projectRoot }) {
|
|
211
176
|
const invoked = [];
|
|
212
|
-
const
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
invoked.push(scriptName);
|
|
177
|
+
const context = [];
|
|
178
|
+
if (scripts.length === 0) {
|
|
179
|
+
return { block: false, context, invoked };
|
|
180
|
+
}
|
|
217
181
|
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
182
|
+
const runnerPath = path.join(projectRoot, '.claude', 'ukit', 'runtime', 'hook-chain-runner.mjs');
|
|
183
|
+
const scriptPaths = scripts.map((scriptName) => path.join(projectRoot, '.claude', 'hooks', scriptName));
|
|
184
|
+
let execResult;
|
|
185
|
+
try {
|
|
186
|
+
execResult = await pi.exec(
|
|
187
|
+
process.execPath,
|
|
188
|
+
[runnerPath, JSON.stringify(payload), ...scriptPaths],
|
|
189
|
+
{ cwd: projectRoot, timeout: HOOK_CHAIN_TIMEOUT_MS },
|
|
190
|
+
);
|
|
191
|
+
} catch (error) {
|
|
192
|
+
execResult = { code: 1, stdout: '', stderr: error?.message ?? String(error), killed: false };
|
|
193
|
+
}
|
|
224
194
|
|
|
225
|
-
|
|
195
|
+
if (execResult?.killed) {
|
|
196
|
+
const firstGate = scripts.find((scriptName) => FAIL_CLOSED_SCRIPTS.has(scriptName));
|
|
197
|
+
return firstGate
|
|
198
|
+
? {
|
|
199
|
+
block: true,
|
|
200
|
+
reason: `${firstGate} hook chain was killed before safety gates completed`,
|
|
201
|
+
context,
|
|
202
|
+
invoked,
|
|
203
|
+
}
|
|
204
|
+
: { block: false, context, invoked };
|
|
205
|
+
}
|
|
226
206
|
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
}
|
|
207
|
+
let chainResult;
|
|
208
|
+
try {
|
|
209
|
+
chainResult = JSON.parse(execResult?.stdout || '{}');
|
|
210
|
+
} catch {
|
|
211
|
+
chainResult = { results: [], wrapperError: execResult?.stderr || 'invalid hook-chain output' };
|
|
212
|
+
}
|
|
230
213
|
|
|
231
|
-
|
|
232
|
-
|
|
214
|
+
if (chainResult.wrapperError || execResult?.code) {
|
|
215
|
+
const firstGate = scripts.find((scriptName) => FAIL_CLOSED_SCRIPTS.has(scriptName));
|
|
216
|
+
if (firstGate) {
|
|
217
|
+
return {
|
|
218
|
+
block: true,
|
|
219
|
+
reason: chainResult.wrapperError || execResult?.stderr || `${firstGate} hook chain failed`,
|
|
220
|
+
context,
|
|
221
|
+
invoked,
|
|
222
|
+
};
|
|
233
223
|
}
|
|
224
|
+
pi.logger?.warn?.(`[UKit] hook chain failed open: ${chainResult.wrapperError || execResult?.stderr || 'unknown error'}`);
|
|
225
|
+
}
|
|
234
226
|
|
|
227
|
+
for (const item of chainResult.results ?? []) {
|
|
228
|
+
const scriptName = item?.scriptName;
|
|
229
|
+
if (!scriptName || !scripts.includes(scriptName)) continue;
|
|
230
|
+
invoked.push(scriptName);
|
|
231
|
+
const verdict = translateExecResult(scriptName, item);
|
|
232
|
+
if (verdict.stdout?.trim()) context.push(verdict.stdout.trim());
|
|
233
|
+
if (verdict.warning) pi.logger?.warn?.(`[UKit] ${verdict.warning}`);
|
|
235
234
|
if (verdict.block) {
|
|
236
|
-
return { block: true, reason: verdict.reason, context
|
|
235
|
+
return { block: true, reason: verdict.reason, context, invoked };
|
|
237
236
|
}
|
|
238
237
|
}
|
|
239
238
|
|
|
240
|
-
return { block: false, context
|
|
239
|
+
return { block: false, context, invoked };
|
|
241
240
|
}
|
|
242
241
|
|
|
243
|
-
// ---------------------------------------------------------------------------
|
|
244
|
-
// Event handlers (all exported directly for test import; wired onto `pi.on`
|
|
245
|
-
// by the default export below).
|
|
246
|
-
// ---------------------------------------------------------------------------
|
|
247
|
-
|
|
248
242
|
function scriptsForToolCall(claudeToolName) {
|
|
249
243
|
const matcherGroup = matcherGroupFor(claudeToolName);
|
|
250
|
-
|
|
251
|
-
return HOOK_EVENT_MAP.tool_call[matcherGroup] ?? [];
|
|
244
|
+
return matcherGroup ? (HOOK_EVENT_MAP.tool_call[matcherGroup] ?? []) : [];
|
|
252
245
|
}
|
|
253
246
|
|
|
254
247
|
function scriptsForToolResult(claudeToolName) {
|
|
255
248
|
const matcherGroup = matcherGroupFor(claudeToolName);
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
249
|
+
return matcherGroup ? (HOOK_EVENT_MAP.tool_result[matcherGroup] ?? []) : [];
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
function normalizeToolInput(toolName, input) {
|
|
253
|
+
if (!input || typeof input !== 'object' || Array.isArray(input)) return input ?? {};
|
|
254
|
+
const normalized = { ...input };
|
|
255
|
+
if ((toolName === 'Edit' || toolName === 'Write') && !normalized.file_path) {
|
|
256
|
+
const paths = Array.isArray(normalized.paths) ? normalized.paths.filter(Boolean) : [];
|
|
257
|
+
normalized.file_path = normalized.path || paths[0] || undefined;
|
|
258
|
+
}
|
|
259
|
+
return normalized;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
function looksMutationCapable(input) {
|
|
263
|
+
if (!input || typeof input !== 'object' || Array.isArray(input)) return false;
|
|
264
|
+
const hasTarget = Boolean(input.file_path || input.path || (Array.isArray(input.paths) && input.paths.length));
|
|
265
|
+
const hasMutation = [
|
|
266
|
+
'content',
|
|
267
|
+
'new_string',
|
|
268
|
+
'old_string',
|
|
269
|
+
'patch',
|
|
270
|
+
'replacement',
|
|
271
|
+
'edits',
|
|
272
|
+
].some((key) => input[key] !== undefined);
|
|
273
|
+
return hasTarget && hasMutation;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
function textFromContent(content) {
|
|
277
|
+
return (Array.isArray(content) ? content : [])
|
|
278
|
+
.filter((item) => item?.type === 'text' && typeof item.text === 'string')
|
|
279
|
+
.map((item) => item.text)
|
|
280
|
+
.join('\n');
|
|
259
281
|
}
|
|
260
282
|
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
283
|
+
function hookContextMessage(content) {
|
|
284
|
+
return {
|
|
285
|
+
customType: 'ukit-hook-context',
|
|
286
|
+
content,
|
|
287
|
+
display: false,
|
|
288
|
+
};
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
function sendContext(pi, context, deliverAs) {
|
|
292
|
+
const content = context.filter(Boolean).join('\n').trim();
|
|
293
|
+
if (!content || typeof pi.sendMessage !== 'function') return;
|
|
294
|
+
pi.sendMessage(hookContextMessage(content), { deliverAs });
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
export async function runToolCall(pi, event, { projectRoot, context: extensionContext = {} }) {
|
|
298
|
+
const rawToolName = event.toolName ?? event.tool;
|
|
299
|
+
const toolName = mapToolName(rawToolName);
|
|
300
|
+
if (!toolName && looksMutationCapable(event.input)) {
|
|
301
|
+
return {
|
|
302
|
+
block: true,
|
|
303
|
+
reason: `UKit blocked unmapped mutation-capable tool "${rawToolName}"; add a host adapter mapping before retrying.`,
|
|
304
|
+
context: [],
|
|
305
|
+
invoked: [],
|
|
306
|
+
toolName,
|
|
307
|
+
};
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
const metadata = runtimeMetadata(event, extensionContext);
|
|
267
311
|
const payload = buildHookPayload('PreToolUse', {
|
|
268
312
|
toolName,
|
|
269
|
-
toolInput: event.input,
|
|
270
|
-
|
|
271
|
-
|
|
313
|
+
toolInput: normalizeToolInput(toolName, event.input),
|
|
314
|
+
toolUseId: event.toolCallId,
|
|
315
|
+
...metadata,
|
|
272
316
|
});
|
|
273
|
-
const result = await runScriptChain(pi,
|
|
317
|
+
const result = await runScriptChain(pi, scriptsForToolCall(toolName), payload, { projectRoot });
|
|
318
|
+
if (!result.block) sendContext(pi, result.context, 'steer');
|
|
274
319
|
return { ...result, toolName };
|
|
275
320
|
}
|
|
276
321
|
|
|
277
|
-
export async function runToolResult(pi, event, { projectRoot }) {
|
|
278
|
-
const toolName = mapToolName(event.tool);
|
|
279
|
-
const
|
|
322
|
+
export async function runToolResult(pi, event, { projectRoot, context: extensionContext = {} }) {
|
|
323
|
+
const toolName = mapToolName(event.toolName ?? event.tool);
|
|
324
|
+
const metadata = runtimeMetadata(event, extensionContext);
|
|
325
|
+
const outputText = textFromContent(event.content);
|
|
326
|
+
const toolOutput = {
|
|
327
|
+
...(event.details && typeof event.details === 'object' ? event.details : {}),
|
|
328
|
+
output: outputText,
|
|
329
|
+
stdout: outputText,
|
|
330
|
+
exitCode: event.details?.exitCode,
|
|
331
|
+
isError: Boolean(event.isError),
|
|
332
|
+
details: event.details,
|
|
333
|
+
};
|
|
280
334
|
const payload = buildHookPayload('PostToolUse', {
|
|
281
335
|
toolName,
|
|
282
|
-
toolInput: event.input,
|
|
283
|
-
|
|
284
|
-
|
|
336
|
+
toolInput: normalizeToolInput(toolName, event.input),
|
|
337
|
+
toolOutput,
|
|
338
|
+
toolResult: {
|
|
339
|
+
content: event.content,
|
|
340
|
+
details: event.details,
|
|
341
|
+
isError: Boolean(event.isError),
|
|
342
|
+
},
|
|
343
|
+
toolUseId: event.toolCallId,
|
|
344
|
+
...metadata,
|
|
285
345
|
});
|
|
286
|
-
const result = await runScriptChain(pi,
|
|
287
|
-
|
|
346
|
+
const result = await runScriptChain(pi, scriptsForToolResult(toolName), payload, { projectRoot });
|
|
347
|
+
try {
|
|
348
|
+
await recordExecutionReceipt({
|
|
349
|
+
projectRoot,
|
|
350
|
+
payload,
|
|
351
|
+
toolName,
|
|
352
|
+
harness: 'omp',
|
|
353
|
+
});
|
|
354
|
+
} catch (error) {
|
|
355
|
+
pi.logger?.warn?.(`[UKit] execution receipt failed open: ${error?.message || error}`);
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
const hookOutput = result.context.join('\n').trim();
|
|
359
|
+
if (!hookOutput) return undefined;
|
|
360
|
+
if (toolName === 'Bash') {
|
|
361
|
+
return {
|
|
362
|
+
content: [{ type: 'text', text: hookOutput }],
|
|
363
|
+
details: event.details,
|
|
364
|
+
isError: Boolean(event.isError),
|
|
365
|
+
};
|
|
366
|
+
}
|
|
367
|
+
return {
|
|
368
|
+
content: [
|
|
369
|
+
...(Array.isArray(event.content) ? event.content : []),
|
|
370
|
+
{ type: 'text', text: hookOutput },
|
|
371
|
+
],
|
|
372
|
+
details: event.details,
|
|
373
|
+
isError: Boolean(event.isError),
|
|
374
|
+
};
|
|
288
375
|
}
|
|
289
376
|
|
|
290
|
-
export async function
|
|
377
|
+
export async function runBeforeAgentStart(pi, event, { projectRoot, context: extensionContext = {} }) {
|
|
378
|
+
const metadata = runtimeMetadata(event, extensionContext);
|
|
291
379
|
const payload = buildHookPayload('UserPromptSubmit', {
|
|
292
|
-
sessionId: event.sessionId,
|
|
293
|
-
cwd: event.cwd,
|
|
294
380
|
prompt: event.prompt,
|
|
381
|
+
...metadata,
|
|
295
382
|
});
|
|
296
|
-
|
|
383
|
+
const result = await runScriptChain(pi, HOOK_EVENT_MAP.before_agent_start, payload, { projectRoot });
|
|
384
|
+
const parts = [...result.context];
|
|
385
|
+
if (result.block && result.reason) parts.push(result.reason);
|
|
386
|
+
const content = parts.join('\n').trim();
|
|
387
|
+
return content ? { message: hookContextMessage(content) } : undefined;
|
|
297
388
|
}
|
|
298
389
|
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
const
|
|
303
|
-
|
|
304
|
-
cwd: event.cwd,
|
|
305
|
-
transcriptPath: event.transcriptPath,
|
|
306
|
-
});
|
|
307
|
-
return runScriptChain(pi, HOOK_EVENT_MAP['session.compacting'], payload, { projectRoot });
|
|
390
|
+
export async function runSessionCompacting(pi, event, { projectRoot, context: extensionContext = {} }) {
|
|
391
|
+
const metadata = runtimeMetadata(event, extensionContext);
|
|
392
|
+
const payload = buildHookPayload('PreCompact', metadata);
|
|
393
|
+
const result = await runScriptChain(pi, HOOK_EVENT_MAP['session.compacting'], payload, { projectRoot });
|
|
394
|
+
return result.context.length > 0 ? { context: result.context } : undefined;
|
|
308
395
|
}
|
|
309
396
|
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
397
|
+
export async function runSessionStart(pi, event, { projectRoot, context: extensionContext = {} }) {
|
|
398
|
+
const metadata = runtimeMetadata(event, extensionContext);
|
|
399
|
+
const payload = buildHookPayload('SessionStart', metadata);
|
|
400
|
+
const result = await runScriptChain(pi, HOOK_EVENT_MAP.session_start, payload, { projectRoot });
|
|
401
|
+
sendContext(pi, result.context, 'nextTurn');
|
|
402
|
+
return result;
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
export async function runSessionCompact(pi, event, { projectRoot, context: extensionContext = {} }) {
|
|
406
|
+
const metadata = runtimeMetadata(event, extensionContext);
|
|
407
|
+
const payload = buildHookPayload('SessionStart', { ...metadata, source: 'compact' });
|
|
408
|
+
const result = await runScriptChain(pi, HOOK_EVENT_MAP.session_start, payload, { projectRoot });
|
|
409
|
+
sendContext(pi, result.context, 'nextTurn');
|
|
410
|
+
return undefined;
|
|
321
411
|
}
|
|
322
412
|
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
413
|
+
export async function runSessionStop(
|
|
414
|
+
pi,
|
|
415
|
+
event,
|
|
416
|
+
{
|
|
417
|
+
projectRoot,
|
|
418
|
+
context: extensionContext = {},
|
|
419
|
+
state: suppliedState,
|
|
420
|
+
ledger: suppliedLedger,
|
|
421
|
+
},
|
|
422
|
+
) {
|
|
423
|
+
const metadata = runtimeMetadata(event, extensionContext);
|
|
424
|
+
const payload = buildHookPayload('Stop', metadata);
|
|
425
|
+
const state = suppliedState ?? await readRouteState(projectRoot);
|
|
426
|
+
const ledger = suppliedLedger ?? await readExecutionLedger(projectRoot, payload) ?? {};
|
|
427
|
+
const evaluation = evaluateCompletion({ state, ledger });
|
|
428
|
+
if (!evaluation.continue) {
|
|
429
|
+
if (evaluation.capped) pi.logger?.warn?.(`[UKit] ${evaluation.reason}`);
|
|
430
|
+
return undefined;
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
if (suppliedLedger === undefined) {
|
|
434
|
+
try {
|
|
435
|
+
await incrementContinuation(projectRoot, payload, ledger);
|
|
436
|
+
} catch (error) {
|
|
437
|
+
pi.logger?.warn?.(`[UKit] continuation bookkeeping failed open: ${error?.message || error}`);
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
return {
|
|
441
|
+
continue: true,
|
|
442
|
+
additionalContext: evaluation.reason,
|
|
443
|
+
};
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
function installedProjectRoot() {
|
|
447
|
+
const bridgeDir = path.dirname(fileURLToPath(import.meta.url));
|
|
448
|
+
return path.resolve(bridgeDir, '../../..');
|
|
449
|
+
}
|
|
327
450
|
|
|
328
451
|
export default function hook(pi) {
|
|
329
|
-
const projectRoot = process.env.CLAUDE_PROJECT_DIR ||
|
|
452
|
+
const projectRoot = process.env.CLAUDE_PROJECT_DIR || installedProjectRoot();
|
|
330
453
|
|
|
331
|
-
pi.on('tool_call', (event) =>
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
454
|
+
pi.on('tool_call', (event, context) => {
|
|
455
|
+
return runToolCall(pi, event, { projectRoot, context }).then((result) => (
|
|
456
|
+
result.block ? { block: true, reason: result.reason } : undefined
|
|
457
|
+
));
|
|
458
|
+
});
|
|
459
|
+
pi.on('tool_result', (event, context) => runToolResult(pi, event, { projectRoot, context }));
|
|
460
|
+
pi.on('before_agent_start', (event, context) => runBeforeAgentStart(pi, event, { projectRoot, context }));
|
|
461
|
+
pi.on('session.compacting', (event, context) => runSessionCompacting(pi, event, { projectRoot, context }));
|
|
462
|
+
pi.on('session_start', (event, context) => runSessionStart(pi, event, { projectRoot, context }));
|
|
463
|
+
pi.on('session_compact', (event, context) => runSessionCompact(pi, event, { projectRoot, context }));
|
|
464
|
+
pi.on('session_stop', (event, context) => runSessionStop(pi, event, { projectRoot, context }));
|
|
336
465
|
}
|