@evo-dev/evodev 0.0.1-alpha.1 → 0.0.1-alpha.10
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/dist/.claude-plugin/marketplace.json +2 -2
- package/dist/assets/skills/coding/knowledge-distillation/SKILL.md +117 -114
- package/dist/assets/skills/coding/knowledge-distillation/references/knowledge-distillation-methods.md +11 -7
- package/dist/assets/team/agents/code-reviewer.md +48 -0
- package/dist/assets/team/agents/docs-maintainer.md +51 -0
- package/dist/assets/team/agents/implementation-engineer.md +51 -0
- package/dist/assets/team/agents/product-scope-analyst.md +58 -0
- package/dist/assets/team/agents/release-engineer.md +55 -0
- package/dist/assets/team/agents/security-boundary-reviewer.md +50 -0
- package/dist/assets/team/agents/solution-architect.md +51 -0
- package/dist/assets/team/agents/verification-engineer.md +51 -0
- package/dist/assets/team/team.md +102 -0
- package/dist/index.js +28597 -14815
- package/dist/plugins/evodev/.claude-plugin/plugin.json +1 -1
- package/dist/plugins/evodev/.codex-plugin/plugin.json +1 -1
- package/dist/plugins/evodev/hooks/codex-hooks.json +10 -10
- package/dist/plugins/evodev/hooks/codex.ts +206 -3
- package/dist/plugins/evodev/hooks/hooks.json +18 -18
- package/dist/plugins/evodev/hooks/hooks.ts +209 -22
- package/dist/plugins/evodev/hooks/runtime.ts +144 -68
- package/dist/plugins/evodev/hooks/workspace-core.ts +0 -27
- package/dist/plugins/evodev/package.json +1 -1
- package/dist/plugins/evodev/skills/knowledge-distillation/SKILL.md +117 -114
- package/dist/plugins/evodev/skills/knowledge-distillation/references/knowledge-distillation-methods.md +11 -7
- package/dist/ui/app.js +9 -0
- package/dist/ui/styles.css +2 -0
- package/package.json +8 -3
- package/dist/evodev +0 -11
|
@@ -1,6 +1,9 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
1
|
+
import type { Dirent } from "node:fs";
|
|
2
|
+
import { copyFile, mkdir, readFile, readdir, stat, writeFile } from "node:fs/promises";
|
|
3
|
+
import { dirname, join } from "node:path";
|
|
4
|
+
import { fileURLToPath } from "node:url";
|
|
3
5
|
import {
|
|
6
|
+
CANONICAL_HOOK_EVENT_TYPES,
|
|
4
7
|
type CanonicalHookEventType,
|
|
5
8
|
type HookEventV1,
|
|
6
9
|
type HookInputEventType,
|
|
@@ -76,9 +79,20 @@ export interface CodexHookConfig {
|
|
|
76
79
|
}
|
|
77
80
|
|
|
78
81
|
export const CLAUDE_HOOK_RUNTIME_COMMAND =
|
|
79
|
-
'
|
|
80
|
-
export const
|
|
81
|
-
|
|
82
|
+
'cd "${CLAUDE_PLUGIN_ROOT}" && bun run hooks/runtime.ts hook runtime --target claude';
|
|
83
|
+
export const DEFAULT_CLAUDE_PLUGIN_SELECTOR = "evodev@evo-dev";
|
|
84
|
+
export const CODEX_HOOK_RUNTIME_COMMAND = [
|
|
85
|
+
"sh -lc '",
|
|
86
|
+
'root="${PLUGIN_ROOT:-}"; ',
|
|
87
|
+
'if [ -z "$root" ]; then ',
|
|
88
|
+
'for candidate in "$HOME/.codex/plugins/cache/evodev/evodev"/* "$HOME/.codex/plugins/evodev"; do ',
|
|
89
|
+
'if [ -f "$candidate/hooks/runtime.ts" ]; then root="$candidate"; break; fi; ',
|
|
90
|
+
"done; ",
|
|
91
|
+
"fi; ",
|
|
92
|
+
'if [ -z "$root" ] || [ ! -f "$root/hooks/runtime.ts" ]; then exit 0; fi; ',
|
|
93
|
+
'cd "$root" && bun run hooks/runtime.ts hook runtime --target codex || exit 0',
|
|
94
|
+
"'",
|
|
95
|
+
].join("");
|
|
82
96
|
const CODEX_FEATURE_FLAGS = ["plugins", "plugin_hooks", "hooks"] as const;
|
|
83
97
|
const CLAUDE_TOOL_EVENTS = new Set<CanonicalHookEventType>([
|
|
84
98
|
"PreToolUse",
|
|
@@ -127,6 +141,10 @@ const CODEX_EVENT_MATCHERS: Partial<Record<CanonicalHookEventType, string>> = {
|
|
|
127
141
|
SubagentStop: "*",
|
|
128
142
|
Stop: "*",
|
|
129
143
|
};
|
|
144
|
+
const RUNTIME_HOOK_EVENT_TYPES = [...CANONICAL_HOOK_EVENT_TYPES, "AgentStop"] as const;
|
|
145
|
+
const RUNTIME_HOOK_EVENT_TYPE_BY_KEY = new Map<string, HookInputEventType>(
|
|
146
|
+
RUNTIME_HOOK_EVENT_TYPES.map((type) => [normalizeRuntimeHookEventName(type), type]),
|
|
147
|
+
);
|
|
130
148
|
|
|
131
149
|
export function planClaudeHookInstallDryRun(
|
|
132
150
|
input: ClaudeHookInstallDryRunInput,
|
|
@@ -140,7 +158,7 @@ export function planClaudeHookInstallDryRun(
|
|
|
140
158
|
action: "merge-with-backup",
|
|
141
159
|
targetPath: paths.settingsPath,
|
|
142
160
|
reason:
|
|
143
|
-
"would
|
|
161
|
+
"would reconcile EvoDev-managed user-level hook entries with Claude plugin ownership without touching user-owned hooks",
|
|
144
162
|
},
|
|
145
163
|
],
|
|
146
164
|
warnings: ["Project .claude, .codex, CLAUDE.md, and AGENTS.md are not targeted."],
|
|
@@ -172,10 +190,37 @@ export async function installClaudeHooks(input: {
|
|
|
172
190
|
homeDir: string;
|
|
173
191
|
paths?: ClaudePaths;
|
|
174
192
|
now?: string;
|
|
193
|
+
pluginSelector?: string;
|
|
194
|
+
pluginManaged?: boolean;
|
|
175
195
|
}): Promise<HookInstallResult> {
|
|
176
196
|
const paths = input.paths ?? resolveClaudePaths({ homeDir: input.homeDir });
|
|
177
197
|
const existing = await readTextIfExists(paths.settingsPath);
|
|
178
|
-
const
|
|
198
|
+
const pluginSelector = input.pluginSelector ?? DEFAULT_CLAUDE_PLUGIN_SELECTOR;
|
|
199
|
+
if (input.pluginManaged === true || isClaudePluginEnabled(existing, pluginSelector)) {
|
|
200
|
+
const next = removeClaudeManagedUserHooks(existing);
|
|
201
|
+
if (next === null) {
|
|
202
|
+
return {
|
|
203
|
+
target: "claude",
|
|
204
|
+
targetPath: paths.settingsPath,
|
|
205
|
+
backupPath: null,
|
|
206
|
+
changed: false,
|
|
207
|
+
warnings: [],
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
return writeMergedConfig({
|
|
211
|
+
target: "claude",
|
|
212
|
+
path: paths.settingsPath,
|
|
213
|
+
existing,
|
|
214
|
+
next,
|
|
215
|
+
now: input.now,
|
|
216
|
+
warnings: [],
|
|
217
|
+
});
|
|
218
|
+
}
|
|
219
|
+
const runtimeCommand = await resolveClaudeUserHookRuntimeCommand({
|
|
220
|
+
paths,
|
|
221
|
+
pluginSelector,
|
|
222
|
+
});
|
|
223
|
+
const next = mergeClaudeSettingsHooks(existing, createClaudeHookConfig(runtimeCommand).hooks);
|
|
179
224
|
return writeMergedConfig({
|
|
180
225
|
target: "claude",
|
|
181
226
|
path: paths.settingsPath,
|
|
@@ -186,6 +231,101 @@ export async function installClaudeHooks(input: {
|
|
|
186
231
|
});
|
|
187
232
|
}
|
|
188
233
|
|
|
234
|
+
async function resolveClaudeUserHookRuntimeCommand(input: {
|
|
235
|
+
paths: ClaudePaths;
|
|
236
|
+
pluginSelector: string;
|
|
237
|
+
}): Promise<string> {
|
|
238
|
+
const runtimePath =
|
|
239
|
+
(await resolveInstalledClaudePluginRuntimePath(input)) ?? (await resolveLocalRuntimePath());
|
|
240
|
+
const pluginRoot = dirname(dirname(runtimePath));
|
|
241
|
+
return `cd ${shellQuote(pluginRoot)} && bun run hooks/runtime.ts hook runtime --target claude`;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
async function resolveInstalledClaudePluginRuntimePath(input: {
|
|
245
|
+
paths: ClaudePaths;
|
|
246
|
+
pluginSelector: string;
|
|
247
|
+
}): Promise<string | null> {
|
|
248
|
+
const marketplaceName = extractMarketplaceNameFromSelector(input.pluginSelector);
|
|
249
|
+
const pluginName = extractPluginNameFromSelector(input.pluginSelector);
|
|
250
|
+
if (marketplaceName === null || pluginName === null) return null;
|
|
251
|
+
if (!isSafePluginCacheSegment(marketplaceName) || !isSafePluginCacheSegment(pluginName)) {
|
|
252
|
+
return null;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
const cacheRoot = join(input.paths.claudeDir, "plugins", "cache", marketplaceName, pluginName);
|
|
256
|
+
let entries: Dirent[];
|
|
257
|
+
try {
|
|
258
|
+
entries = await readdir(cacheRoot, { withFileTypes: true });
|
|
259
|
+
} catch (error) {
|
|
260
|
+
if (isNotFoundError(error)) return null;
|
|
261
|
+
throw error;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
const candidates = (
|
|
265
|
+
await Promise.all(
|
|
266
|
+
entries
|
|
267
|
+
.filter((entry) => entry.isDirectory())
|
|
268
|
+
.map(async (entry) => {
|
|
269
|
+
const runtimePath = join(cacheRoot, entry.name, "hooks", "runtime.ts");
|
|
270
|
+
try {
|
|
271
|
+
const runtimeStat = await stat(runtimePath);
|
|
272
|
+
return runtimeStat.isFile()
|
|
273
|
+
? { path: runtimePath, mtimeMs: runtimeStat.mtimeMs }
|
|
274
|
+
: null;
|
|
275
|
+
} catch (error) {
|
|
276
|
+
if (isNotFoundError(error)) return null;
|
|
277
|
+
throw error;
|
|
278
|
+
}
|
|
279
|
+
}),
|
|
280
|
+
)
|
|
281
|
+
).filter((candidate): candidate is { path: string; mtimeMs: number } => candidate !== null);
|
|
282
|
+
|
|
283
|
+
if (candidates.length === 0) return null;
|
|
284
|
+
candidates.sort(
|
|
285
|
+
(left, right) => right.mtimeMs - left.mtimeMs || right.path.localeCompare(left.path),
|
|
286
|
+
);
|
|
287
|
+
return candidates[0].path;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
function extractMarketplaceNameFromSelector(selector: string): string | null {
|
|
291
|
+
const separator = selector.lastIndexOf("@");
|
|
292
|
+
if (separator <= 0 || separator === selector.length - 1) return null;
|
|
293
|
+
const marketplaceName = selector.slice(separator + 1).trim();
|
|
294
|
+
return marketplaceName === "" ? null : marketplaceName;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
function extractPluginNameFromSelector(selector: string): string | null {
|
|
298
|
+
const separator = selector.lastIndexOf("@");
|
|
299
|
+
const pluginName = (separator <= 0 ? selector : selector.slice(0, separator)).trim();
|
|
300
|
+
return pluginName === "" ? null : pluginName;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
function isSafePluginCacheSegment(segment: string): boolean {
|
|
304
|
+
return /^[A-Za-z0-9._-]+$/.test(segment) && segment !== "." && segment !== "..";
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
async function resolveLocalRuntimePath(): Promise<string> {
|
|
308
|
+
const currentDir = dirname(fileURLToPath(import.meta.url));
|
|
309
|
+
const candidates = [
|
|
310
|
+
join(currentDir, "runtime.ts"),
|
|
311
|
+
join(currentDir, "plugins", "evodev", "hooks", "runtime.ts"),
|
|
312
|
+
];
|
|
313
|
+
|
|
314
|
+
for (const candidate of candidates) {
|
|
315
|
+
try {
|
|
316
|
+
const candidateStat = await stat(candidate);
|
|
317
|
+
if (candidateStat.isFile()) return candidate;
|
|
318
|
+
} catch (error) {
|
|
319
|
+
if (isNotFoundError(error)) continue;
|
|
320
|
+
throw error;
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
throw new Error(
|
|
325
|
+
`Cannot resolve Claude user-level hook runtime path. Checked: ${candidates.join(", ")}`,
|
|
326
|
+
);
|
|
327
|
+
}
|
|
328
|
+
|
|
189
329
|
export async function installCodexHooks(input: {
|
|
190
330
|
homeDir: string;
|
|
191
331
|
paths?: CodexPaths;
|
|
@@ -225,12 +365,11 @@ export function normalizeClaudeRuntimeHookPayload(input: {
|
|
|
225
365
|
payload: Record<string, unknown>;
|
|
226
366
|
receivedAt?: string;
|
|
227
367
|
}): HookEventV1 {
|
|
228
|
-
const type = input.payload.hook_event_name ?? input.payload.hookEventName;
|
|
229
|
-
if (typeof type !== "string" || type.trim() === "") {
|
|
230
|
-
throw new Error("Claude hook payload is missing hook_event_name.");
|
|
231
|
-
}
|
|
232
368
|
return normalizeClaudeHookPayload({
|
|
233
|
-
type:
|
|
369
|
+
type: readRuntimeHookEventType(
|
|
370
|
+
input.payload.hook_event_name ?? input.payload.hookEventName,
|
|
371
|
+
"Claude",
|
|
372
|
+
),
|
|
234
373
|
payload: input.payload,
|
|
235
374
|
receivedAt: input.receivedAt,
|
|
236
375
|
});
|
|
@@ -254,21 +393,34 @@ export function normalizeCodexRuntimeHookPayload(input: {
|
|
|
254
393
|
payload: Record<string, unknown>;
|
|
255
394
|
receivedAt?: string;
|
|
256
395
|
}): HookEventV1 {
|
|
257
|
-
const type =
|
|
258
|
-
input.payload.hook_event_name ??
|
|
259
|
-
input.payload.hookEventName ??
|
|
260
|
-
input.payload.event ??
|
|
261
|
-
input.payload.name;
|
|
262
|
-
if (typeof type !== "string" || type.trim() === "") {
|
|
263
|
-
throw new Error("Codex hook payload is missing hook_event_name.");
|
|
264
|
-
}
|
|
265
396
|
return normalizeCodexHookPayload({
|
|
266
|
-
type:
|
|
397
|
+
type: readRuntimeHookEventType(
|
|
398
|
+
input.payload.hook_event_name ??
|
|
399
|
+
input.payload.hookEventName ??
|
|
400
|
+
input.payload.event ??
|
|
401
|
+
input.payload.name,
|
|
402
|
+
"Codex",
|
|
403
|
+
),
|
|
267
404
|
payload: input.payload,
|
|
268
405
|
receivedAt: input.receivedAt,
|
|
269
406
|
});
|
|
270
407
|
}
|
|
271
408
|
|
|
409
|
+
function readRuntimeHookEventType(value: unknown, target: "Claude" | "Codex"): HookInputEventType {
|
|
410
|
+
if (typeof value !== "string" || value.trim() === "") {
|
|
411
|
+
throw new Error(`${target} hook payload is missing hook_event_name.`);
|
|
412
|
+
}
|
|
413
|
+
const normalized = RUNTIME_HOOK_EVENT_TYPE_BY_KEY.get(normalizeRuntimeHookEventName(value));
|
|
414
|
+
if (normalized === undefined) {
|
|
415
|
+
throw new Error(`Unsupported hook event type: ${value}`);
|
|
416
|
+
}
|
|
417
|
+
return normalized;
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
function normalizeRuntimeHookEventName(value: string): string {
|
|
421
|
+
return value.replace(/[^A-Za-z0-9]/g, "").toLowerCase();
|
|
422
|
+
}
|
|
423
|
+
|
|
272
424
|
export function createClaudeHookConfig(
|
|
273
425
|
command: string = CLAUDE_HOOK_RUNTIME_COMMAND,
|
|
274
426
|
): ClaudeHookConfig {
|
|
@@ -283,7 +435,7 @@ export function createClaudeHookConfig(
|
|
|
283
435
|
}
|
|
284
436
|
return {
|
|
285
437
|
description:
|
|
286
|
-
"EvoDev observes Claude Code lifecycle events through evodev hook runtime for local trace and
|
|
438
|
+
"EvoDev observes Claude Code lifecycle events through evodev hook runtime for local trace and action-required advisories.",
|
|
287
439
|
hooks,
|
|
288
440
|
};
|
|
289
441
|
}
|
|
@@ -367,6 +519,37 @@ function mergeClaudeSettingsHooks(
|
|
|
367
519
|
return `${JSON.stringify({ ...settings, hooks: nextHooks }, null, 2)}\n`;
|
|
368
520
|
}
|
|
369
521
|
|
|
522
|
+
function isClaudePluginEnabled(existing: string | null, pluginSelector: string): boolean {
|
|
523
|
+
if (existing === null) return false;
|
|
524
|
+
const settings = parseJsonConfig(existing, "Claude settings");
|
|
525
|
+
const enabledPlugins = isRecord(settings.enabledPlugins) ? settings.enabledPlugins : {};
|
|
526
|
+
return enabledPlugins[pluginSelector] === true;
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
function removeClaudeManagedUserHooks(existing: string | null): string | null {
|
|
530
|
+
if (existing === null) return null;
|
|
531
|
+
const settings = parseJsonConfig(existing, "Claude settings");
|
|
532
|
+
if (!isRecord(settings.hooks)) return existing;
|
|
533
|
+
|
|
534
|
+
const nextHooks: Record<string, unknown> = {};
|
|
535
|
+
let changed = false;
|
|
536
|
+
for (const [eventName, groups] of Object.entries(settings.hooks)) {
|
|
537
|
+
if (!Array.isArray(groups)) {
|
|
538
|
+
nextHooks[eventName] = groups;
|
|
539
|
+
continue;
|
|
540
|
+
}
|
|
541
|
+
const retained = groups.filter((group) => !isEvoDevHookGroup(group, "claude"));
|
|
542
|
+
changed ||= retained.length !== groups.length;
|
|
543
|
+
if (retained.length > 0 || groups.length === 0) nextHooks[eventName] = retained;
|
|
544
|
+
}
|
|
545
|
+
if (!changed) return existing;
|
|
546
|
+
|
|
547
|
+
const nextSettings = { ...settings };
|
|
548
|
+
if (Object.keys(nextHooks).length === 0) nextSettings.hooks = undefined;
|
|
549
|
+
else nextSettings.hooks = nextHooks;
|
|
550
|
+
return `${JSON.stringify(nextSettings, null, 2)}\n`;
|
|
551
|
+
}
|
|
552
|
+
|
|
370
553
|
function parseJsonConfig(existing: string | null, label: string): Record<string, unknown> {
|
|
371
554
|
if (existing === null || existing.trim() === "") return {};
|
|
372
555
|
const parsed = JSON.parse(existing) as unknown;
|
|
@@ -452,6 +635,10 @@ function backupTimestamp(value?: string): string {
|
|
|
452
635
|
return source.replace(/[^0-9A-Za-z]/g, "").slice(0, 20) || "now";
|
|
453
636
|
}
|
|
454
637
|
|
|
638
|
+
function shellQuote(value: string): string {
|
|
639
|
+
return `'${value.replace(/'/g, "'\\''")}'`;
|
|
640
|
+
}
|
|
641
|
+
|
|
455
642
|
async function fileExists(path: string): Promise<boolean> {
|
|
456
643
|
try {
|
|
457
644
|
return (await stat(path)).isFile();
|
|
@@ -1,22 +1,20 @@
|
|
|
1
1
|
#!/usr/bin/env bun
|
|
2
2
|
|
|
3
|
-
import {
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
resolveTraceTeamContext,
|
|
13
|
-
} from "@evo-dev/core";
|
|
14
|
-
import { normalizeClaudeRuntimeHookPayload, normalizeCodexRuntimeHookPayload } from "./hooks.ts";
|
|
3
|
+
import type { HookEventV1, HookRuntimeResult } from "@evo-dev/core";
|
|
4
|
+
|
|
5
|
+
export type HookRuntimeCoreApi = typeof import("@evo-dev/core");
|
|
6
|
+
export type HookRuntimeNormalizerApi = typeof import("./hooks.ts");
|
|
7
|
+
|
|
8
|
+
export interface HookRuntimeApi {
|
|
9
|
+
core: HookRuntimeCoreApi;
|
|
10
|
+
normalizers: HookRuntimeNormalizerApi;
|
|
11
|
+
}
|
|
15
12
|
|
|
16
13
|
export interface HookRuntimeCliOptions {
|
|
17
14
|
argv?: string[];
|
|
18
15
|
environment?: Record<string, string | undefined>;
|
|
19
16
|
homeDir?: string;
|
|
17
|
+
loadRuntimeApi?: () => Promise<HookRuntimeApi>;
|
|
20
18
|
stdin?: string | (() => Promise<string>);
|
|
21
19
|
write?: (message: string) => void;
|
|
22
20
|
}
|
|
@@ -26,7 +24,9 @@ export async function runHookRuntimeCli(options: HookRuntimeCliOptions = {}): Pr
|
|
|
26
24
|
const environment = options.environment ?? process.env;
|
|
27
25
|
const write = options.write ?? process.stdout.write.bind(process.stdout);
|
|
28
26
|
const startedAt = new Date();
|
|
27
|
+
const loadRuntimeApi = options.loadRuntimeApi ?? loadDefaultHookRuntimeApi;
|
|
29
28
|
let homeDir = options.homeDir ?? process.env.HOME ?? "~";
|
|
29
|
+
let runtimeApi: HookRuntimeApi | null = null;
|
|
30
30
|
let target = "unknown";
|
|
31
31
|
let payload: Record<string, unknown> | null = null;
|
|
32
32
|
let stdinBytes: number | null = null;
|
|
@@ -45,8 +45,10 @@ export async function runHookRuntimeCli(options: HookRuntimeCliOptions = {}): Pr
|
|
|
45
45
|
homeDir = options.homeDir ?? process.env.HOME ?? "~";
|
|
46
46
|
const stdin = await readHookRuntimeStdin(options.stdin);
|
|
47
47
|
stdinBytes = Buffer.byteLength(stdin, "utf8");
|
|
48
|
+
runtimeApi = await loadRuntimeApi();
|
|
48
49
|
payload = JSON.parse(stdin) as Record<string, unknown>;
|
|
49
|
-
await
|
|
50
|
+
await appendExecutionEventSafely({
|
|
51
|
+
core: runtimeApi.core,
|
|
50
52
|
homeDir,
|
|
51
53
|
phase: "started",
|
|
52
54
|
target,
|
|
@@ -56,28 +58,32 @@ export async function runHookRuntimeCli(options: HookRuntimeCliOptions = {}): Pr
|
|
|
56
58
|
environment,
|
|
57
59
|
});
|
|
58
60
|
|
|
59
|
-
const settings = await readHookSettings(homeDir);
|
|
61
|
+
const settings = await readHookSettings(runtimeApi.core, homeDir);
|
|
60
62
|
const event =
|
|
61
63
|
flags.target === "codex"
|
|
62
|
-
? normalizeCodexRuntimeHookPayload({
|
|
64
|
+
? runtimeApi.normalizers.normalizeCodexRuntimeHookPayload({
|
|
63
65
|
payload,
|
|
64
66
|
receivedAt: new Date().toISOString(),
|
|
65
67
|
})
|
|
66
|
-
: normalizeClaudeRuntimeHookPayload({
|
|
68
|
+
: runtimeApi.normalizers.normalizeClaudeRuntimeHookPayload({
|
|
67
69
|
payload,
|
|
68
70
|
receivedAt: new Date().toISOString(),
|
|
69
71
|
});
|
|
70
|
-
const result = await handleHookRuntime({
|
|
72
|
+
const result = await runtimeApi.core.handleHookRuntime({
|
|
71
73
|
target: flags.target,
|
|
72
74
|
homeDir,
|
|
73
75
|
settings: settings.hooks,
|
|
76
|
+
teamRuntimeDisplayMode: settings.teamRuntime.displayMode,
|
|
74
77
|
event,
|
|
75
78
|
rawPayload: payload,
|
|
76
79
|
receivedAt: event.time.receivedAt,
|
|
77
80
|
environment,
|
|
81
|
+
runtimeInjectionEnabled: settings.memory.runtimeInjection,
|
|
82
|
+
sessionMemoryPolicy: settings.memory.sessionMemory,
|
|
78
83
|
});
|
|
79
|
-
const formatted = formatHookRuntimeOutput(result);
|
|
80
|
-
await
|
|
84
|
+
const formatted = runtimeApi.core.formatHookRuntimeOutput(result);
|
|
85
|
+
await appendExecutionEventSafely({
|
|
86
|
+
core: runtimeApi.core,
|
|
81
87
|
homeDir,
|
|
82
88
|
phase: "completed",
|
|
83
89
|
target,
|
|
@@ -86,14 +92,14 @@ export async function runHookRuntimeCli(options: HookRuntimeCliOptions = {}): Pr
|
|
|
86
92
|
stdinBytes,
|
|
87
93
|
event,
|
|
88
94
|
result,
|
|
89
|
-
formatted,
|
|
90
95
|
environment,
|
|
91
96
|
durationMs: Date.now() - startedAt.getTime(),
|
|
92
97
|
});
|
|
93
98
|
if (formatted.length > 0) write(formatted);
|
|
94
99
|
return 0;
|
|
95
100
|
} catch (error) {
|
|
96
|
-
await
|
|
101
|
+
await appendExecutionEventSafely({
|
|
102
|
+
core: runtimeApi?.core,
|
|
97
103
|
homeDir,
|
|
98
104
|
phase: "failed",
|
|
99
105
|
target,
|
|
@@ -104,10 +110,15 @@ export async function runHookRuntimeCli(options: HookRuntimeCliOptions = {}): Pr
|
|
|
104
110
|
environment,
|
|
105
111
|
durationMs: Date.now() - startedAt.getTime(),
|
|
106
112
|
});
|
|
107
|
-
|
|
113
|
+
return 0;
|
|
108
114
|
}
|
|
109
115
|
}
|
|
110
116
|
|
|
117
|
+
async function loadDefaultHookRuntimeApi(): Promise<HookRuntimeApi> {
|
|
118
|
+
const [core, normalizers] = await Promise.all([import("@evo-dev/core"), import("./hooks.ts")]);
|
|
119
|
+
return { core, normalizers };
|
|
120
|
+
}
|
|
121
|
+
|
|
111
122
|
function parseHookRuntimeFlags(argv: string[]): { target: string } {
|
|
112
123
|
let target: string | undefined;
|
|
113
124
|
for (let index = 0; index < argv.length; index += 1) {
|
|
@@ -138,18 +149,19 @@ async function readHookRuntimeStdin(stdin: HookRuntimeCliOptions["stdin"]): Prom
|
|
|
138
149
|
return Buffer.concat(chunks).toString("utf8");
|
|
139
150
|
}
|
|
140
151
|
|
|
141
|
-
async function readHookSettings(homeDir: string) {
|
|
152
|
+
async function readHookSettings(core: HookRuntimeCoreApi, homeDir: string) {
|
|
142
153
|
try {
|
|
143
|
-
return await createCoreConfigStore(homeDir).readSettings();
|
|
154
|
+
return await core.createCoreConfigStore(homeDir).readSettings();
|
|
144
155
|
} catch (error) {
|
|
145
156
|
if (isNotFoundError(error)) {
|
|
146
|
-
return createDefaultSettings();
|
|
157
|
+
return core.createDefaultSettings();
|
|
147
158
|
}
|
|
148
|
-
|
|
159
|
+
return core.createDefaultSettings();
|
|
149
160
|
}
|
|
150
161
|
}
|
|
151
162
|
|
|
152
|
-
async function
|
|
163
|
+
async function appendExecutionEventSafely(input: {
|
|
164
|
+
core?: HookRuntimeCoreApi;
|
|
153
165
|
homeDir: string;
|
|
154
166
|
phase: "started" | "completed" | "failed";
|
|
155
167
|
target: string;
|
|
@@ -158,56 +170,121 @@ async function appendTraceLogSafely(input: {
|
|
|
158
170
|
stdinBytes: number | null;
|
|
159
171
|
event?: HookEventV1;
|
|
160
172
|
result?: HookRuntimeResult;
|
|
161
|
-
formatted?: string;
|
|
162
173
|
durationMs?: number | null;
|
|
163
174
|
error?: unknown;
|
|
164
175
|
environment?: Record<string, string | undefined>;
|
|
165
176
|
}): Promise<void> {
|
|
177
|
+
if (input.core === undefined) return;
|
|
178
|
+
|
|
166
179
|
try {
|
|
167
|
-
|
|
180
|
+
const sessionKey =
|
|
181
|
+
input.payload === null ? "session-local" : input.core.resolveTraceSessionKey(input.payload);
|
|
182
|
+
const team = input.core.resolveTraceTeamContext({
|
|
183
|
+
homeDir: input.homeDir,
|
|
184
|
+
environment: input.environment,
|
|
185
|
+
payload: input.payload,
|
|
186
|
+
});
|
|
187
|
+
const cwd =
|
|
188
|
+
input.payload !== null && typeof input.payload.cwd === "string" ? input.payload.cwd : null;
|
|
189
|
+
const workspace =
|
|
190
|
+
team === null && cwd !== null
|
|
191
|
+
? await input.core.resolveProjectWorkspaceFromCwd({ homeDir: input.homeDir, cwd })
|
|
192
|
+
: null;
|
|
193
|
+
const traceRefId = await recordTraceRefSafely(input);
|
|
194
|
+
await input.core.appendEvoDevExecutionEvent(
|
|
168
195
|
input.homeDir,
|
|
169
|
-
|
|
170
|
-
|
|
196
|
+
input.core.createEvoDevExecutionEvent({
|
|
197
|
+
eventId: input.event?.eventId,
|
|
171
198
|
target: input.target,
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
payload: input.payload,
|
|
183
|
-
}),
|
|
184
|
-
event:
|
|
185
|
-
input.event === undefined
|
|
186
|
-
? null
|
|
187
|
-
: {
|
|
188
|
-
eventId: input.event.eventId,
|
|
189
|
-
type: input.event.type,
|
|
190
|
-
summary: input.event.payload.summary,
|
|
191
|
-
metadata: input.event.payload.metadata,
|
|
192
|
-
decision: input.event.decision,
|
|
193
|
-
},
|
|
194
|
-
result:
|
|
195
|
-
input.result === undefined
|
|
196
|
-
? null
|
|
197
|
-
: {
|
|
198
|
-
enabled: input.result.enabled,
|
|
199
|
-
summary: input.result.summary,
|
|
200
|
-
stateWrites: input.result.stateWrites,
|
|
201
|
-
warnings: input.result.warnings,
|
|
202
|
-
formattedResponse: input.formatted?.trim() || null,
|
|
203
|
-
durationMs: input.durationMs ?? null,
|
|
204
|
-
},
|
|
205
|
-
error: input.error,
|
|
199
|
+
eventType: input.event?.type ?? runtimePhaseEventType(input.phase),
|
|
200
|
+
sessionKey,
|
|
201
|
+
projectKey: team?.projectKey ?? workspace?.projectKey ?? null,
|
|
202
|
+
runId: team?.runId ?? null,
|
|
203
|
+
roleId: team?.roleId ?? null,
|
|
204
|
+
taskId: input.event?.scope.taskId ?? null,
|
|
205
|
+
summary: summarizeExecutionEvent(input),
|
|
206
|
+
metadata: createExecutionEventMetadata(input),
|
|
207
|
+
decision: input.event?.decision ?? null,
|
|
208
|
+
codeAgentTraceRefId: traceRefId,
|
|
206
209
|
}),
|
|
207
210
|
);
|
|
208
211
|
} catch {
|
|
209
|
-
//
|
|
212
|
+
// Execution event logging must never change hook runtime behavior.
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
async function recordTraceRefSafely(input: {
|
|
217
|
+
core?: HookRuntimeCoreApi;
|
|
218
|
+
homeDir: string;
|
|
219
|
+
target: string;
|
|
220
|
+
payload: Record<string, unknown> | null;
|
|
221
|
+
event?: HookEventV1;
|
|
222
|
+
environment?: Record<string, string | undefined>;
|
|
223
|
+
}): Promise<string | null> {
|
|
224
|
+
if (
|
|
225
|
+
input.core === undefined ||
|
|
226
|
+
input.payload === null ||
|
|
227
|
+
(input.target !== "claude" && input.target !== "codex")
|
|
228
|
+
) {
|
|
229
|
+
return null;
|
|
210
230
|
}
|
|
231
|
+
try {
|
|
232
|
+
const record = await input.core.recordCodeAgentTraceRefFromHook({
|
|
233
|
+
homeDir: input.homeDir,
|
|
234
|
+
target: input.target,
|
|
235
|
+
payload: input.payload,
|
|
236
|
+
environment: input.environment,
|
|
237
|
+
now:
|
|
238
|
+
input.event?.time.receivedAt === undefined || input.event.time.receivedAt === "dry-run"
|
|
239
|
+
? undefined
|
|
240
|
+
: input.event.time.receivedAt,
|
|
241
|
+
});
|
|
242
|
+
return record?.ref.id ?? null;
|
|
243
|
+
} catch {
|
|
244
|
+
return null;
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
function runtimePhaseEventType(phase: "started" | "completed" | "failed"): string {
|
|
249
|
+
if (phase === "started") return "HookRuntimeStarted";
|
|
250
|
+
if (phase === "completed") return "HookRuntimeCompleted";
|
|
251
|
+
return "HookRuntimeFailed";
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
function summarizeExecutionEvent(input: {
|
|
255
|
+
phase: "started" | "completed" | "failed";
|
|
256
|
+
event?: HookEventV1;
|
|
257
|
+
result?: HookRuntimeResult;
|
|
258
|
+
error?: unknown;
|
|
259
|
+
}): string {
|
|
260
|
+
if (input.phase === "failed") return "Hook runtime failed before completion.";
|
|
261
|
+
if (input.result !== undefined) return input.result.summary;
|
|
262
|
+
if (input.event !== undefined) return input.event.payload.summary;
|
|
263
|
+
return "Hook runtime started.";
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
function createExecutionEventMetadata(input: {
|
|
267
|
+
phase: "started" | "completed" | "failed";
|
|
268
|
+
stdinBytes: number | null;
|
|
269
|
+
event?: HookEventV1;
|
|
270
|
+
result?: HookRuntimeResult;
|
|
271
|
+
durationMs?: number | null;
|
|
272
|
+
}): Record<string, unknown> {
|
|
273
|
+
return {
|
|
274
|
+
phase: input.phase,
|
|
275
|
+
runtimeSurface: "plugin",
|
|
276
|
+
stdinBytes: input.stdinBytes,
|
|
277
|
+
durationMs: input.durationMs ?? null,
|
|
278
|
+
enabled: input.result?.enabled,
|
|
279
|
+
stateWriteCount: input.result?.stateWrites.length,
|
|
280
|
+
warningCount: input.result?.warnings.length,
|
|
281
|
+
commandClass: input.event?.payload.metadata.commandClass,
|
|
282
|
+
exitCode: input.event?.payload.metadata.exitCode,
|
|
283
|
+
status: input.event?.payload.metadata.status ?? input.phase,
|
|
284
|
+
redactionCount: input.event?.payload.redactionCount,
|
|
285
|
+
redactionLabels: input.event?.payload.redactions,
|
|
286
|
+
normalizedEventType: input.event?.type,
|
|
287
|
+
};
|
|
211
288
|
}
|
|
212
289
|
|
|
213
290
|
function isNotFoundError(error: unknown): boolean {
|
|
@@ -222,7 +299,6 @@ if (import.meta.main) {
|
|
|
222
299
|
try {
|
|
223
300
|
process.exitCode = await runHookRuntimeCli();
|
|
224
301
|
} catch (error) {
|
|
225
|
-
|
|
226
|
-
process.exitCode = 1;
|
|
302
|
+
process.exitCode = 0;
|
|
227
303
|
}
|
|
228
304
|
}
|
|
@@ -108,7 +108,6 @@ async function writeRuntimeCorePackage(input: {
|
|
|
108
108
|
>;
|
|
109
109
|
const runtimePackageJson = {
|
|
110
110
|
...sourcePackageJson,
|
|
111
|
-
exports: rewritePackageExportsToSource(sourcePackageJson.exports),
|
|
112
111
|
files: ["src", "assets", "package.json"],
|
|
113
112
|
};
|
|
114
113
|
await writeFile(
|
|
@@ -118,32 +117,6 @@ async function writeRuntimeCorePackage(input: {
|
|
|
118
117
|
);
|
|
119
118
|
}
|
|
120
119
|
|
|
121
|
-
function rewritePackageExportsToSource(exportsValue: unknown): unknown {
|
|
122
|
-
if (typeof exportsValue === "string") {
|
|
123
|
-
return rewriteDistPathToSource(exportsValue);
|
|
124
|
-
}
|
|
125
|
-
if (Array.isArray(exportsValue)) {
|
|
126
|
-
return exportsValue.map((value) => rewritePackageExportsToSource(value));
|
|
127
|
-
}
|
|
128
|
-
if (exportsValue !== null && typeof exportsValue === "object") {
|
|
129
|
-
return Object.fromEntries(
|
|
130
|
-
Object.entries(exportsValue).map(([key, value]) => [
|
|
131
|
-
key,
|
|
132
|
-
rewritePackageExportsToSource(value),
|
|
133
|
-
]),
|
|
134
|
-
);
|
|
135
|
-
}
|
|
136
|
-
return exportsValue;
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
function rewriteDistPathToSource(path: string): string {
|
|
140
|
-
if (!path.startsWith("./dist/") || !path.endsWith(".js")) {
|
|
141
|
-
return path;
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
return `./src/${path.slice("./dist/".length, -".js".length)}.ts`;
|
|
145
|
-
}
|
|
146
|
-
|
|
147
120
|
async function isDirectory(path: string): Promise<boolean> {
|
|
148
121
|
try {
|
|
149
122
|
return (await stat(path)).isDirectory();
|