@evo-dev/evodev 0.0.1-alpha → 0.0.1-alpha.2

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 (38) hide show
  1. package/dist/.agents/skills/grilling/SKILL.md +10 -0
  2. package/dist/.claude-plugin/marketplace.json +2 -2
  3. package/dist/assets/agents/review/code-reviewer/examples.md +1 -1
  4. package/dist/assets/agents/review/code-reviewer/prompt.md +1 -1
  5. package/dist/assets/agents/review/code-reviewer/verification.md +1 -1
  6. package/dist/assets/skills/coding/knowledge-distillation/SKILL.md +249 -0
  7. package/dist/assets/skills/coding/knowledge-distillation/manifest.json +10 -0
  8. package/dist/assets/skills/coding/knowledge-distillation/references/knowledge-distillation-methods.md +126 -0
  9. package/dist/assets/workflows/rd-bug-fix/WORKFLOW.json +1 -1
  10. package/dist/assets/workflows/rd-code-review/WORKFLOW.json +1 -1
  11. package/dist/assets/workflows/rd-docs-update/WORKFLOW.json +1 -1
  12. package/dist/assets/workflows/rd-feature-implementation/WORKFLOW.json +1 -1
  13. package/dist/assets/workflows/rd-refactor/WORKFLOW.json +1 -1
  14. package/dist/assets/workflows/rd-release-readiness/WORKFLOW.json +1 -1
  15. package/dist/assets/workflows/rd-security-boundary-review/WORKFLOW.json +2 -2
  16. package/dist/assets/workflows/rd-test-generation/WORKFLOW.json +1 -1
  17. package/dist/index.js +18925 -6426
  18. package/dist/plugins/evodev/.claude-plugin/plugin.json +2 -2
  19. package/dist/plugins/evodev/.codex-plugin/plugin.json +8 -6
  20. package/dist/plugins/evodev/.mcp.json +6 -0
  21. package/dist/plugins/evodev/hooks/codex-hooks.json +10 -10
  22. package/dist/plugins/evodev/hooks/codex.ts +596 -34
  23. package/dist/plugins/evodev/hooks/hooks.json +18 -18
  24. package/dist/plugins/evodev/hooks/hooks.ts +417 -25
  25. package/dist/plugins/evodev/hooks/index.ts +15 -0
  26. package/dist/plugins/evodev/hooks/paths.ts +44 -1
  27. package/dist/plugins/evodev/hooks/plugin.ts +160 -9
  28. package/dist/plugins/evodev/hooks/runtime.ts +227 -43
  29. package/dist/plugins/evodev/hooks/transform-agent.ts +30 -0
  30. package/dist/plugins/evodev/hooks/workspace-core.ts +154 -0
  31. package/dist/plugins/evodev/package.json +3 -3
  32. package/dist/plugins/evodev/skills/engineering-discipline/SKILL.md +63 -0
  33. package/dist/plugins/evodev/skills/engineering-discipline/anti-patterns.md +21 -0
  34. package/dist/plugins/evodev/skills/engineering-discipline/examples.md +19 -0
  35. package/dist/plugins/evodev/skills/engineering-discipline/verification.md +11 -0
  36. package/dist/plugins/evodev/skills/knowledge-distillation/SKILL.md +249 -0
  37. package/dist/plugins/evodev/skills/knowledge-distillation/references/knowledge-distillation-methods.md +126 -0
  38. package/package.json +3 -6
@@ -1,8 +1,12 @@
1
+ import type { Dirent } from "node:fs";
2
+ import { mkdir, readFile, readdir, stat, writeFile } from "node:fs/promises";
3
+ import { dirname } from "node:path";
1
4
  import type {
2
5
  CodeAgentCapabilities,
3
6
  CodeAgentPlugin,
4
7
  DoctorCheck,
5
8
  DoctorContext,
9
+ HookEventType,
6
10
  PluginDetectionResult,
7
11
  PluginInstallInput,
8
12
  PluginInstallResult,
@@ -11,9 +15,29 @@ import type {
11
15
  SyncSkillsInput,
12
16
  } from "@evo-dev/core/plugins";
13
17
  import { runNodeCommand } from "./command-runner.ts";
18
+ import { CODEX_HOOK_RUNTIME_COMMAND } from "./hooks.ts";
19
+ import { type CodexPaths, resolveCodexAgentTargetPath, resolveCodexPaths } from "./paths.ts";
20
+ import { transformCodexAgent } from "./transform-agent.ts";
21
+ import {
22
+ type WorkspaceCoreHydrationResult,
23
+ hydrateWorkspaceCoreDependency,
24
+ isSafePluginCacheSegment,
25
+ } from "./workspace-core.ts";
14
26
 
15
- const CODEX_RESERVED_MESSAGE =
16
- "Codex asset sync is reserved until Codex capabilities and formats are verified.";
27
+ const CODEX_SKILL_BUNDLE_MESSAGE =
28
+ "Codex skills are bundled in the EvoDev Codex plugin; direct skill sync is skipped.";
29
+ const CODEX_CAPABILITY_HOOK_EVENTS: HookEventType[] = [
30
+ "SessionStart",
31
+ "UserPromptSubmit",
32
+ "PreToolUse",
33
+ "PermissionRequest",
34
+ "PostToolUse",
35
+ "PreCompact",
36
+ "PostCompact",
37
+ "SubagentStart",
38
+ "SubagentStop",
39
+ "Stop",
40
+ ];
17
41
 
18
42
  export interface CodexCommandResult {
19
43
  exitCode: number;
@@ -29,9 +53,55 @@ export interface CodexPluginOptions {
29
53
  commandRunner?: CodexCommandRunner;
30
54
  pluginSelector?: string;
31
55
  pluginMarketplaceSource?: string;
56
+ pluginMarketplaceFallbackSource?: string;
57
+ paths?: CodexPaths;
58
+ syncFileSystem?: CodexSyncFileSystem;
32
59
  }
33
60
 
61
+ export interface CodexSyncFileSystem {
62
+ readFile(path: string): Promise<string>;
63
+ writeFile(path: string, content: string, options?: { overwrite?: boolean }): Promise<void>;
64
+ fileExists(path: string): Promise<boolean>;
65
+ ensureDir(path: string): Promise<void>;
66
+ }
67
+
68
+ const nodeSyncFileSystem: CodexSyncFileSystem = {
69
+ async readFile(path: string): Promise<string> {
70
+ return readFile(path, "utf8");
71
+ },
72
+ async writeFile(
73
+ path: string,
74
+ content: string,
75
+ options: { overwrite?: boolean } = {},
76
+ ): Promise<void> {
77
+ await writeFile(path, content, {
78
+ encoding: "utf8",
79
+ flag: options.overwrite === true ? "w" : "wx",
80
+ });
81
+ },
82
+ async fileExists(path: string): Promise<boolean> {
83
+ try {
84
+ const fileStat = await stat(path);
85
+ return fileStat.isFile();
86
+ } catch (error) {
87
+ if (isNotFoundError(error)) {
88
+ return false;
89
+ }
90
+
91
+ throw error;
92
+ }
93
+ },
94
+ async ensureDir(path: string): Promise<void> {
95
+ await mkdir(path, { recursive: true });
96
+ },
97
+ };
98
+
34
99
  export function createCodexPlugin(options: CodexPluginOptions = {}): CodeAgentPlugin {
100
+ const syncFileSystem = options.syncFileSystem ?? nodeSyncFileSystem;
101
+ const getSyncPaths = (homeDir?: string) =>
102
+ options.paths ??
103
+ (homeDir === undefined ? resolveCodexPathsFromHome() : resolveCodexPaths({ homeDir }));
104
+
35
105
  return {
36
106
  id: "codex",
37
107
  name: "Codex",
@@ -40,9 +110,9 @@ export function createCodexPlugin(options: CodexPluginOptions = {}): CodeAgentPl
40
110
  },
41
111
  async getCapabilities(): Promise<CodeAgentCapabilities> {
42
112
  return {
43
- skills: { supported: false, format: "codex-skill" },
44
- agents: { supported: false, format: "codex-agent" },
45
- hooks: { supported: false, events: [] },
113
+ skills: { supported: true, format: "codex-skill" },
114
+ agents: { supported: true, format: "codex-agent" },
115
+ hooks: { supported: true, events: [...CODEX_CAPABILITY_HOOK_EVENTS] },
46
116
  };
47
117
  },
48
118
  async doctor(context: DoctorContext): Promise<DoctorCheck[]> {
@@ -52,29 +122,30 @@ export function createCodexPlugin(options: CodexPluginOptions = {}): CodeAgentPl
52
122
  return [
53
123
  detectionToDoctorCheck(detection),
54
124
  {
55
- id: "codex.assets.reserved",
56
- status: enabled ? "warn" : "skip",
125
+ id: "codex.skills.plugin-bundled",
126
+ status: enabled ? "pass" : "skip",
57
127
  message: enabled
58
- ? `${CODEX_RESERVED_MESSAGE} Code Agent plugin install is supported, but Codex skill/agent asset sync remains disabled.`
59
- : `${CODEX_RESERVED_MESSAGE} Codex is disabled in settings.`,
128
+ ? "Codex skills are packaged in the EvoDev plugin and loaded through Codex plugin installation."
129
+ : `${CODEX_SKILL_BUNDLE_MESSAGE} Codex is disabled in settings.`,
60
130
  },
131
+ codexAgentsDoctorCheck(enabled, getSyncPaths(context.homeDir)),
132
+ await codexStopHooksDoctorCheck(getSyncPaths(context.homeDir)),
61
133
  ];
62
134
  },
63
135
  async syncSkills(input: SyncSkillsInput): Promise<SyncResult> {
64
- const result = createSkippedSyncResult(input.targetPlugin);
136
+ const result = createEmptySyncResult(input.targetPlugin);
65
137
  for (const skill of input.skills) {
66
138
  result.skipped.push(skill.registryKey);
139
+ if (skill.manifest.targets.includes("codex")) {
140
+ result.warnings.push(
141
+ `${CODEX_SKILL_BUNDLE_MESSAGE} Refresh or reinstall the Codex plugin to update ${skill.registryKey}.`,
142
+ );
143
+ }
67
144
  }
68
- addCodexSyncDiagnostic(result, input.dryRun, "skill");
69
145
  return result;
70
146
  },
71
147
  async syncAgents(input: SyncAgentsInput): Promise<SyncResult> {
72
- const result = createSkippedSyncResult(input.targetPlugin);
73
- for (const agent of input.agents) {
74
- result.skipped.push(agent.registryKey);
75
- }
76
- addCodexSyncDiagnostic(result, input.dryRun, "agent");
77
- return result;
148
+ return syncCodexAgents(input, getSyncPaths(), syncFileSystem);
78
149
  },
79
150
  async installPlugin(input: PluginInstallInput): Promise<PluginInstallResult> {
80
151
  return installCodexPlugin(input, options);
@@ -118,24 +189,116 @@ async function detectCodex(
118
189
 
119
190
  async function installCodexPlugin(
120
191
  input: PluginInstallInput,
121
- options: Pick<CodexPluginOptions, "commandRunner" | "pluginSelector" | "pluginMarketplaceSource">,
192
+ options: Pick<
193
+ CodexPluginOptions,
194
+ | "commandRunner"
195
+ | "pluginSelector"
196
+ | "pluginMarketplaceSource"
197
+ | "pluginMarketplaceFallbackSource"
198
+ | "paths"
199
+ >,
122
200
  ): Promise<PluginInstallResult> {
123
201
  const command = "codex";
124
202
  const selector = options.pluginSelector ?? "evodev@evodev";
125
203
  const args = ["plugin", "add", selector];
126
204
  const runner = options.commandRunner ?? new NodeCodexCommandRunner();
205
+ const primaryResult = await installCodexPluginOnce({
206
+ input,
207
+ command,
208
+ selector,
209
+ args,
210
+ runner,
211
+ marketplaceSource: options.pluginMarketplaceSource,
212
+ paths: options.paths,
213
+ });
214
+
215
+ const fallbackSource = options.pluginMarketplaceFallbackSource;
216
+ if (
217
+ primaryResult.status !== "failed" ||
218
+ options.pluginMarketplaceSource === undefined ||
219
+ fallbackSource === undefined ||
220
+ fallbackSource === options.pluginMarketplaceSource ||
221
+ !isSshGitMarketplaceSource(options.pluginMarketplaceSource)
222
+ ) {
223
+ return primaryResult;
224
+ }
225
+
226
+ const fallbackResult = await installCodexPluginOnce({
227
+ input,
228
+ command,
229
+ selector,
230
+ args,
231
+ runner,
232
+ marketplaceSource: fallbackSource,
233
+ removeMarketplaceBeforeAdd: true,
234
+ paths: options.paths,
235
+ });
236
+
237
+ return {
238
+ ...fallbackResult,
239
+ warnings: [
240
+ `Codex SSH marketplace source failed (${primaryResult.message}); retried with fallback source: ${fallbackSource}.`,
241
+ ...fallbackResult.warnings,
242
+ ],
243
+ };
244
+ }
245
+
246
+ async function installCodexPluginOnce(input: {
247
+ input: PluginInstallInput;
248
+ command: string;
249
+ selector: string;
250
+ args: string[];
251
+ runner: CodexCommandRunner;
252
+ marketplaceSource?: string;
253
+ removeMarketplaceBeforeAdd?: boolean;
254
+ paths?: CodexPaths;
255
+ }): Promise<PluginInstallResult> {
256
+ const { command, selector, args, runner } = input;
257
+ const warnings: string[] = [];
127
258
 
128
259
  try {
129
- if (options.pluginMarketplaceSource !== undefined) {
260
+ if (input.marketplaceSource !== undefined) {
261
+ if (
262
+ input.removeMarketplaceBeforeAdd === true ||
263
+ (input.input.force === true && isGitMarketplaceSource(input.marketplaceSource))
264
+ ) {
265
+ const marketplaceName = extractMarketplaceNameFromSelector(selector);
266
+ if (marketplaceName === undefined) {
267
+ warnings.push(
268
+ `Skipped Codex marketplace remove before refresh because selector does not include a marketplace name: ${selector}`,
269
+ );
270
+ } else {
271
+ const removeMarketplaceResult = await runner.run(command, [
272
+ "plugin",
273
+ "marketplace",
274
+ "remove",
275
+ marketplaceName,
276
+ ]);
277
+ const removeMarketplaceMessage =
278
+ removeMarketplaceResult.stderr ?? removeMarketplaceResult.stdout;
279
+ if (
280
+ removeMarketplaceResult.exitCode !== 0 &&
281
+ !isNotInstalledMessage(removeMarketplaceMessage)
282
+ ) {
283
+ warnings.push(
284
+ `Codex marketplace refresh remove did not complete: ${
285
+ removeMarketplaceMessage ??
286
+ `Codex marketplace remove exited with code ${removeMarketplaceResult.exitCode}`
287
+ }`,
288
+ );
289
+ }
290
+ }
291
+ }
292
+
130
293
  const marketplaceResult = await runner.run(command, [
131
294
  "plugin",
132
295
  "marketplace",
133
296
  "add",
134
- options.pluginMarketplaceSource,
297
+ input.marketplaceSource,
135
298
  ]);
136
299
  if (marketplaceResult.exitCode !== 0) {
137
300
  return createPluginInstallResult(
138
- input.targetPlugin,
301
+ input.input.targetPlugin,
139
302
  "failed",
140
303
  command,
141
304
  args,
@@ -144,21 +307,78 @@ async function installCodexPlugin(
144
307
  `Codex marketplace add exited with code ${marketplaceResult.exitCode}`,
145
308
  );
146
309
  }
310
+
311
+ if (isGitMarketplaceSource(input.marketplaceSource)) {
312
+ const marketplaceName = extractMarketplaceNameFromSelector(selector);
313
+ if (marketplaceName === undefined) {
314
+ warnings.push(
315
+ `Skipped Codex marketplace upgrade because selector does not include a marketplace name: ${selector}`,
316
+ );
317
+ } else {
318
+ const upgradeResult = await runner.run(command, [
319
+ "plugin",
320
+ "marketplace",
321
+ "upgrade",
322
+ marketplaceName,
323
+ ]);
324
+ if (upgradeResult.exitCode !== 0) {
325
+ return createPluginInstallResult(
326
+ input.input.targetPlugin,
327
+ "failed",
328
+ command,
329
+ args,
330
+ upgradeResult.stderr ??
331
+ upgradeResult.stdout ??
332
+ `Codex marketplace upgrade exited with code ${upgradeResult.exitCode}`,
333
+ );
334
+ }
335
+ }
336
+ }
337
+ }
338
+
339
+ if (input.input.force === true) {
340
+ const removeArgs = ["plugin", "remove", selector];
341
+ const removeResult = await runner.run(command, removeArgs);
342
+ const removeMessage = removeResult.stderr ?? removeResult.stdout;
343
+ if (removeResult.exitCode !== 0 && !isNotInstalledMessage(removeMessage)) {
344
+ warnings.push(
345
+ `Codex plugin refresh remove did not complete: ${
346
+ removeMessage ?? `Codex plugin remove exited with code ${removeResult.exitCode}`
347
+ }`,
348
+ );
349
+ }
147
350
  }
148
351
 
149
352
  const result = await runner.run(command, args);
150
353
  if (result.exitCode === 0) {
354
+ const hydration = await hydrateCodexWorkspaceCoreDependency({
355
+ paths: input.paths,
356
+ selector,
357
+ });
358
+ warnings.push(...hydration.warnings);
359
+ if (hydration.errors.length > 0) {
360
+ return createPluginInstallResult(
361
+ input.input.targetPlugin,
362
+ "failed",
363
+ command,
364
+ args,
365
+ hydration.errors.join("\n"),
366
+ warnings,
367
+ );
368
+ }
369
+
151
370
  return createPluginInstallResult(
152
- input.targetPlugin,
371
+ input.input.targetPlugin,
153
372
  "installed",
154
373
  command,
155
374
  args,
156
375
  result.stdout,
376
+ warnings,
157
377
  );
158
378
  }
159
379
 
160
380
  return createPluginInstallResult(
161
- input.targetPlugin,
381
+ input.input.targetPlugin,
162
382
  "failed",
163
383
  command,
164
384
  args,
@@ -166,7 +386,7 @@ async function installCodexPlugin(
166
386
  );
167
387
  } catch (error) {
168
388
  return createPluginInstallResult(
169
- input.targetPlugin,
389
+ input.input.targetPlugin,
170
390
  "failed",
171
391
  command,
172
392
  args,
@@ -175,6 +395,149 @@ async function installCodexPlugin(
175
395
  }
176
396
  }
177
397
 
398
+ async function hydrateCodexWorkspaceCoreDependency(input: {
399
+ paths?: CodexPaths;
400
+ selector: string;
401
+ }): Promise<WorkspaceCoreHydrationResult> {
402
+ const warnings: string[] = [];
403
+ const errors: string[] = [];
404
+
405
+ if (input.paths === undefined) {
406
+ return { warnings, errors };
407
+ }
408
+
409
+ const marketplaceName = extractMarketplaceNameFromSelector(input.selector);
410
+ const pluginName = extractPluginNameFromSelector(input.selector);
411
+ if (marketplaceName === undefined || pluginName === undefined) {
412
+ warnings.push(
413
+ `Skipped @evo-dev/core hydration because Codex plugin selector is not marketplace-qualified: ${input.selector}`,
414
+ );
415
+ return { warnings, errors };
416
+ }
417
+ if (!isSafePluginCacheSegment(marketplaceName) || !isSafePluginCacheSegment(pluginName)) {
418
+ warnings.push(
419
+ `Skipped @evo-dev/core hydration because Codex plugin selector contains an unsafe cache segment: ${input.selector}`,
420
+ );
421
+ return { warnings, errors };
422
+ }
423
+
424
+ const cachePluginRoot = `${input.paths.codexDir}/plugins/cache/${marketplaceName}/${pluginName}`;
425
+ const marketplaceCoreRoot = `${input.paths.codexDir}/.tmp/marketplaces/${marketplaceName}/packages/core`;
426
+ const hydration = await hydrateWorkspaceCoreDependency({
427
+ sourceCoreRoot: marketplaceCoreRoot,
428
+ cachePluginRoot,
429
+ runtimeLabel: "Codex",
430
+ });
431
+ const hookPatch = await patchCodexInstalledHookRuntimeCommands({ cachePluginRoot });
432
+ return {
433
+ warnings: [...warnings, ...hydration.warnings, ...hookPatch.warnings],
434
+ errors: [...errors, ...hydration.errors, ...hookPatch.errors],
435
+ };
436
+ }
437
+
438
+ async function patchCodexInstalledHookRuntimeCommands(input: {
439
+ cachePluginRoot: string;
440
+ }): Promise<WorkspaceCoreHydrationResult> {
441
+ const warnings: string[] = [];
442
+ const errors: string[] = [];
443
+ const installedPluginRoot = await findLatestInstalledCodexPluginRoot(input.cachePluginRoot);
444
+ if (installedPluginRoot === null) {
445
+ warnings.push(
446
+ `Skipped Codex hook runtime command patch because the plugin cache was not found: ${input.cachePluginRoot}`,
447
+ );
448
+ return { warnings, errors };
449
+ }
450
+
451
+ const hooksPath = `${installedPluginRoot}/hooks/codex-hooks.json`;
452
+ try {
453
+ const hooksText = await readTextFileIfExists(hooksPath);
454
+ if (hooksText === null || hooksText.trim() === "") {
455
+ warnings.push(
456
+ `Skipped Codex hook runtime command patch because hooks file is missing: ${hooksPath}`,
457
+ );
458
+ } else {
459
+ const hooksConfig = JSON.parse(hooksText) as unknown;
460
+ if (rewriteCodexHookRuntimeCommands(hooksConfig)) {
461
+ await writeFile(hooksPath, `${JSON.stringify(hooksConfig, null, 2)}\n`, "utf8");
462
+ }
463
+ }
464
+ } catch (error) {
465
+ errors.push(
466
+ `Cannot patch Codex hook runtime command in installed plugin cache: ${describeError(error)}`,
467
+ );
468
+ }
469
+
470
+ const hooksSourcePath = `${installedPluginRoot}/hooks/hooks.ts`;
471
+ try {
472
+ const hooksSource = await readTextFileIfExists(hooksSourcePath);
473
+ if (hooksSource !== null) {
474
+ const next = hooksSource.replaceAll(
475
+ 'bun run "${PLUGIN_ROOT}/hooks/runtime.ts" hook runtime --target codex',
476
+ CODEX_HOOK_RUNTIME_COMMAND,
477
+ );
478
+ if (next !== hooksSource) {
479
+ await writeFile(hooksSourcePath, next, "utf8");
480
+ }
481
+ }
482
+ } catch (error) {
483
+ errors.push(
484
+ `Cannot patch Codex hook runtime source in installed plugin cache: ${describeError(error)}`,
485
+ );
486
+ }
487
+
488
+ return { warnings, errors };
489
+ }
490
+
491
+ function rewriteCodexHookRuntimeCommands(value: unknown): boolean {
492
+ if (!isRecord(value)) return false;
493
+ const root = isRecord(value.hooks) ? value.hooks : value;
494
+ let changed = false;
495
+ for (const groups of Object.values(root)) {
496
+ if (!Array.isArray(groups)) continue;
497
+ for (const group of groups) {
498
+ if (!isRecord(group) || !Array.isArray(group.hooks)) continue;
499
+ for (const hook of group.hooks) {
500
+ if (!isRecord(hook) || hook.type !== "command" || typeof hook.command !== "string") {
501
+ continue;
502
+ }
503
+ if (
504
+ hook.command.includes("hook runtime --target codex") &&
505
+ hook.command !== CODEX_HOOK_RUNTIME_COMMAND
506
+ ) {
507
+ hook.command = CODEX_HOOK_RUNTIME_COMMAND;
508
+ changed = true;
509
+ }
510
+ }
511
+ }
512
+ }
513
+ return changed;
514
+ }
515
+
516
+ async function findLatestInstalledCodexPluginRoot(cachePluginRoot: string): Promise<string | null> {
517
+ let entries: Dirent[];
518
+ try {
519
+ entries = await readdir(cachePluginRoot, { withFileTypes: true });
520
+ } catch (error) {
521
+ if (isNotFoundError(error)) return null;
522
+ throw error;
523
+ }
524
+
525
+ const versionDirs = await Promise.all(
526
+ entries
527
+ .filter((entry) => entry.isDirectory())
528
+ .map(async (entry) => {
529
+ const path = `${cachePluginRoot}/${entry.name}`;
530
+ const pathStat = await stat(path);
531
+ return { path, mtimeMs: pathStat.mtimeMs };
532
+ }),
533
+ );
534
+ if (versionDirs.length === 0) return null;
535
+ versionDirs.sort(
536
+ (left, right) => right.mtimeMs - left.mtimeMs || right.path.localeCompare(left.path),
537
+ );
538
+ return versionDirs[0].path;
539
+ }
540
+
178
541
  class NodeCodexCommandRunner implements CodexCommandRunner {
179
542
  async run(command: string, args: string[]): Promise<CodexCommandResult> {
180
543
  return runNodeCommand(command, args);
@@ -193,7 +556,151 @@ function detectionToDoctorCheck(detection: PluginDetectionResult): DoctorCheck {
193
556
  return { id: "codex.available", status: "warn", message: detection.message };
194
557
  }
195
558
 
196
- function createSkippedSyncResult(targetPlugin: string): SyncResult {
559
+ async function codexStopHooksDoctorCheck(paths: CodexPaths): Promise<DoctorCheck> {
560
+ const hooksText = await readTextFileIfExists(paths.hooksPath);
561
+ if (hooksText === null || hooksText.trim() === "") {
562
+ return {
563
+ id: "codex.hooks.stop",
564
+ status: "skip",
565
+ message: `No user-level Codex hooks file found at: ${paths.hooksPath}`,
566
+ };
567
+ }
568
+
569
+ let hooksConfig: unknown;
570
+ try {
571
+ hooksConfig = JSON.parse(hooksText) as unknown;
572
+ } catch {
573
+ return {
574
+ id: "codex.hooks.stop",
575
+ status: "warn",
576
+ message: `Codex hooks file is not valid JSON; existing Stop hook failures may come from: ${paths.hooksPath}`,
577
+ };
578
+ }
579
+
580
+ const stopHooks = collectCodexCommandHooks(hooksConfig, "Stop");
581
+ if (stopHooks.length === 0) {
582
+ return {
583
+ id: "codex.hooks.stop",
584
+ status: "skip",
585
+ message: `No user-level Codex Stop hooks configured in: ${paths.hooksPath}`,
586
+ };
587
+ }
588
+
589
+ const hooksMissingTimeout = stopHooks.filter((hook) => hook.timeout === null);
590
+ if (hooksMissingTimeout.length > 0) {
591
+ return {
592
+ id: "codex.hooks.stop",
593
+ status: "warn",
594
+ message: `Codex Stop hook command(s) without timeout may be killed by the host and show "hook exited without a status code": ${hooksMissingTimeout
595
+ .map((hook) => hook.label)
596
+ .join(", ")}`,
597
+ };
598
+ }
599
+
600
+ return {
601
+ id: "codex.hooks.stop",
602
+ status: "pass",
603
+ message: `Codex Stop hooks have explicit timeouts in: ${paths.hooksPath}`,
604
+ };
605
+ }
606
+
607
+ function collectCodexCommandHooks(
608
+ hooksConfig: unknown,
609
+ eventName: string,
610
+ ): Array<{ label: string; timeout: number | null }> {
611
+ if (!isRecord(hooksConfig)) return [];
612
+ const root = isRecord(hooksConfig.hooks) ? hooksConfig.hooks : hooksConfig;
613
+ const groups = isRecord(root) && Array.isArray(root[eventName]) ? root[eventName] : [];
614
+ const hooks: Array<{ label: string; timeout: number | null }> = [];
615
+
616
+ groups.forEach((group, groupIndex) => {
617
+ if (!isRecord(group) || !Array.isArray(group.hooks)) return;
618
+ group.hooks.forEach((hook, hookIndex) => {
619
+ if (!isRecord(hook) || typeof hook.command !== "string" || hook.type !== "command") return;
620
+ hooks.push({
621
+ label: `${eventName}[${groupIndex}].hooks[${hookIndex}] ${summarizeHookCommand(
622
+ hook.command,
623
+ )}`,
624
+ timeout:
625
+ typeof hook.timeout === "number" && Number.isFinite(hook.timeout) ? hook.timeout : null,
626
+ });
627
+ });
628
+ });
629
+
630
+ return hooks;
631
+ }
632
+
633
+ function summarizeHookCommand(command: string): string {
634
+ const compact = command.replace(/\s+/g, " ").trim();
635
+ if (compact.includes("@dp/ab-agent-collect-event")) return "@dp/ab-agent-collect-event";
636
+ if (compact.includes("flux-hooks")) return "flux-hooks";
637
+ if (compact.includes("flux-bits-report")) return "flux-bits-report";
638
+ if (compact.includes("hook runtime --target codex")) return "evodev codex hook runtime";
639
+ return compact.length <= 80 ? compact : `${compact.slice(0, 77)}...`;
640
+ }
641
+
642
+ async function readTextFileIfExists(path: string): Promise<string | null> {
643
+ try {
644
+ return await readFile(path, "utf8");
645
+ } catch (error) {
646
+ if (isNotFoundError(error)) return null;
647
+ throw error;
648
+ }
649
+ }
650
+
651
+ function isRecord(value: unknown): value is Record<string, unknown> {
652
+ return typeof value === "object" && value !== null && !Array.isArray(value);
653
+ }
654
+
655
+ async function syncCodexAgents(
656
+ input: SyncAgentsInput,
657
+ paths: CodexPaths,
658
+ fileSystem: CodexSyncFileSystem,
659
+ ): Promise<SyncResult> {
660
+ const result = createEmptySyncResult(input.targetPlugin);
661
+
662
+ for (const asset of input.agents) {
663
+ if (!asset.manifest.targets.includes("codex")) {
664
+ result.skipped.push(asset.registryKey);
665
+ continue;
666
+ }
667
+
668
+ try {
669
+ const source = await fileSystem.readFile(asset.entryPath);
670
+ const transformed = transformCodexAgent({ asset, source });
671
+ const targetPath = resolveCodexAgentTargetPath(transformed.name, paths);
672
+ const targetExists = await fileSystem.fileExists(targetPath);
673
+
674
+ if (targetExists && input.force !== true) {
675
+ result.skipped.push(asset.registryKey);
676
+ result.warnings.push(`Skipped existing Codex agent target: ${targetPath}`);
677
+ continue;
678
+ }
679
+
680
+ if (!input.dryRun) {
681
+ await fileSystem.ensureDir(dirname(targetPath));
682
+ await fileSystem.writeFile(targetPath, ensureTrailingNewline(transformed.content), {
683
+ overwrite: input.force === true,
684
+ });
685
+ }
686
+
687
+ if (targetExists) {
688
+ result.warnings.push(
689
+ `${input.dryRun ? "Would update" : "Updated"} existing Codex agent target: ${targetPath}`,
690
+ );
691
+ }
692
+ result.syncedAgents.push(asset.registryKey);
693
+ } catch (error) {
694
+ result.errors.push(
695
+ `Failed to sync Codex agent ${asset.registryKey}: ${describeError(error)}`,
696
+ );
697
+ }
698
+ }
699
+
700
+ return result;
701
+ }
702
+
703
+ function createEmptySyncResult(targetPlugin: string): SyncResult {
197
704
  return {
198
705
  targetPlugin,
199
706
  syncedSkills: [],
@@ -204,14 +711,20 @@ function createSkippedSyncResult(targetPlugin: string): SyncResult {
204
711
  };
205
712
  }
206
713
 
207
- function addCodexSyncDiagnostic(
208
- result: SyncResult,
209
- dryRun: boolean,
210
- assetKind: "skill" | "agent",
211
- ): void {
212
- const message = `Codex ${assetKind} sync unsupported: capabilities are unverified; no writes planned in I10.`;
213
- if (dryRun) result.warnings.push(message);
214
- else result.errors.push(message);
714
+ function codexAgentsDoctorCheck(enabled: boolean, paths: CodexPaths): DoctorCheck {
715
+ if (!enabled) {
716
+ return {
717
+ id: "codex.paths.agents",
718
+ status: "skip",
719
+ message: `Codex agent sync is disabled in settings. Planned user-level path: ${paths.agentsDir}`,
720
+ };
721
+ }
722
+
723
+ return {
724
+ id: "codex.paths.agents",
725
+ status: "pass",
726
+ message: `Codex agents will sync to user-level TOML files under: ${paths.agentsDir}`,
727
+ };
215
728
  }
216
729
 
217
730
  function createPluginInstallResult(
@@ -220,6 +733,7 @@ function createPluginInstallResult(
220
733
  command: string,
221
734
  args: string[],
222
735
  message?: string,
736
+ warnings: string[] = [],
223
737
  ): PluginInstallResult {
224
738
  const normalizedMessage = message?.trim() || `${command} ${args.join(" ")}`;
225
739
  return {
@@ -228,11 +742,39 @@ function createPluginInstallResult(
228
742
  command,
229
743
  args,
230
744
  message: normalizedMessage,
231
- warnings: [],
745
+ warnings,
232
746
  errors: status === "failed" ? [normalizedMessage] : [],
233
747
  };
234
748
  }
235
749
 
750
+ function isNotInstalledMessage(message: string | undefined): boolean {
751
+ return message !== undefined && /(not\s+installed|not\s+found|no\s+installed)/i.test(message);
752
+ }
753
+
754
+ function extractMarketplaceNameFromSelector(selector: string): string | undefined {
755
+ const separator = selector.lastIndexOf("@");
756
+ if (separator <= 0 || separator === selector.length - 1) return undefined;
757
+ return selector.slice(separator + 1);
758
+ }
759
+
760
+ function extractPluginNameFromSelector(selector: string): string | undefined {
761
+ const separator = selector.lastIndexOf("@");
762
+ const pluginName = separator <= 0 ? selector : selector.slice(0, separator);
763
+ return pluginName.trim() === "" ? undefined : pluginName;
764
+ }
765
+
766
+ function isGitMarketplaceSource(source: string): boolean {
767
+ const trimmed = source.trim();
768
+ if (/^(https?|ssh|git):\/\//i.test(trimmed)) return true;
769
+ if (/^git@[^:]+:.+/i.test(trimmed)) return true;
770
+ return /^[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+(?:\.git)?$/.test(trimmed);
771
+ }
772
+
773
+ function isSshGitMarketplaceSource(source: string): boolean {
774
+ const trimmed = source.trim();
775
+ return /^ssh:\/\//i.test(trimmed) || /^git@[^:]+:.+/i.test(trimmed);
776
+ }
777
+
236
778
  function isMissingExit(exitCode: number, stderr?: string): boolean {
237
779
  if (exitCode === 127) return true;
238
780
  const normalized = stderr?.toLowerCase() ?? "";
@@ -248,6 +790,12 @@ function isCommandMissingError(error: unknown): boolean {
248
790
  );
249
791
  }
250
792
 
793
+ function isNotFoundError(error: unknown): boolean {
794
+ return (
795
+ error instanceof Error && "code" in error && (error as NodeJS.ErrnoException).code === "ENOENT"
796
+ );
797
+ }
798
+
251
799
  function describeError(error: unknown): string {
252
800
  if (error instanceof Error) {
253
801
  return error.message;
@@ -255,3 +803,17 @@ function describeError(error: unknown): string {
255
803
 
256
804
  return String(error);
257
805
  }
806
+
807
+ function resolveCodexPathsFromHome(): CodexPaths {
808
+ const home = process.env.HOME;
809
+
810
+ if (home === undefined || home.trim() === "") {
811
+ throw new Error("Cannot resolve Codex paths: HOME is not set");
812
+ }
813
+
814
+ return resolveCodexPaths({ homeDir: home });
815
+ }
816
+
817
+ function ensureTrailingNewline(content: string): string {
818
+ return content.endsWith("\n") ? content : `${content}\n`;
819
+ }