@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.
Files changed (28) hide show
  1. package/dist/.claude-plugin/marketplace.json +2 -2
  2. package/dist/assets/skills/coding/knowledge-distillation/SKILL.md +117 -114
  3. package/dist/assets/skills/coding/knowledge-distillation/references/knowledge-distillation-methods.md +11 -7
  4. package/dist/assets/team/agents/code-reviewer.md +48 -0
  5. package/dist/assets/team/agents/docs-maintainer.md +51 -0
  6. package/dist/assets/team/agents/implementation-engineer.md +51 -0
  7. package/dist/assets/team/agents/product-scope-analyst.md +58 -0
  8. package/dist/assets/team/agents/release-engineer.md +55 -0
  9. package/dist/assets/team/agents/security-boundary-reviewer.md +50 -0
  10. package/dist/assets/team/agents/solution-architect.md +51 -0
  11. package/dist/assets/team/agents/verification-engineer.md +51 -0
  12. package/dist/assets/team/team.md +102 -0
  13. package/dist/index.js +28597 -14815
  14. package/dist/plugins/evodev/.claude-plugin/plugin.json +1 -1
  15. package/dist/plugins/evodev/.codex-plugin/plugin.json +1 -1
  16. package/dist/plugins/evodev/hooks/codex-hooks.json +10 -10
  17. package/dist/plugins/evodev/hooks/codex.ts +206 -3
  18. package/dist/plugins/evodev/hooks/hooks.json +18 -18
  19. package/dist/plugins/evodev/hooks/hooks.ts +209 -22
  20. package/dist/plugins/evodev/hooks/runtime.ts +144 -68
  21. package/dist/plugins/evodev/hooks/workspace-core.ts +0 -27
  22. package/dist/plugins/evodev/package.json +1 -1
  23. package/dist/plugins/evodev/skills/knowledge-distillation/SKILL.md +117 -114
  24. package/dist/plugins/evodev/skills/knowledge-distillation/references/knowledge-distillation-methods.md +11 -7
  25. package/dist/ui/app.js +9 -0
  26. package/dist/ui/styles.css +2 -0
  27. package/package.json +8 -3
  28. package/dist/evodev +0 -11
@@ -1,6 +1,9 @@
1
- import { copyFile, mkdir, readFile, stat, writeFile } from "node:fs/promises";
2
- import { dirname } from "node:path";
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
- 'bun run "${CLAUDE_PLUGIN_ROOT}/hooks/runtime.ts" hook runtime --target claude';
80
- export const CODEX_HOOK_RUNTIME_COMMAND =
81
- 'bun run "${PLUGIN_ROOT}/hooks/runtime.ts" hook runtime --target codex';
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 add or replace EvoDev-managed user-level hook entries without touching user-owned hooks",
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 next = mergeClaudeSettingsHooks(existing, createClaudeHookConfig().hooks);
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: type as HookInputEventType,
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: type as HookInputEventType,
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 advisory workflow suggestions.",
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
- type HookEventV1,
5
- type HookRuntimeResult,
6
- appendTraceLogEntry,
7
- createCoreConfigStore,
8
- createDefaultSettings,
9
- createTraceLogEntry,
10
- formatHookRuntimeOutput,
11
- handleHookRuntime,
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 appendTraceLogSafely({
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 appendTraceLogSafely({
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 appendTraceLogSafely({
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
- throw error;
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
- throw error;
159
+ return core.createDefaultSettings();
149
160
  }
150
161
  }
151
162
 
152
- async function appendTraceLogSafely(input: {
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
- await appendTraceLogEntry(
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
- createTraceLogEntry({
170
- phase: input.phase,
196
+ input.core.createEvoDevExecutionEvent({
197
+ eventId: input.event?.eventId,
171
198
  target: input.target,
172
- runtime: {
173
- surface: "plugin",
174
- argv: input.argv,
175
- runtimeFile: import.meta.url,
176
- },
177
- payload: input.payload,
178
- stdinBytes: input.stdinBytes,
179
- team: resolveTraceTeamContext({
180
- homeDir: input.homeDir,
181
- environment: input.environment,
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
- // Trace logging must never change hook runtime behavior.
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
- console.error(error instanceof Error ? error.message : String(error));
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();
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@evo-dev/plugin",
3
- "version": "0.0.1-alpha.1",
3
+ "version": "0.0.1-alpha.10",
4
4
  "type": "module",
5
5
  "exports": {
6
6
  ".": "./hooks/index.ts"