open-multi-agent-kit 0.80.4 → 0.80.6

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 (105) hide show
  1. package/CHANGELOG.md +21 -0
  2. package/dist/config.d.ts +4 -9
  3. package/dist/config.d.ts.map +1 -1
  4. package/dist/config.js +46 -15
  5. package/dist/config.js.map +1 -1
  6. package/dist/core/compaction/branch-summarization.d.ts.map +1 -1
  7. package/dist/core/compaction/branch-summarization.js +6 -1
  8. package/dist/core/compaction/branch-summarization.js.map +1 -1
  9. package/dist/core/compaction/compaction.d.ts +82 -1
  10. package/dist/core/compaction/compaction.d.ts.map +1 -1
  11. package/dist/core/compaction/compaction.js +606 -10
  12. package/dist/core/compaction/compaction.js.map +1 -1
  13. package/dist/core/harness-control-events.d.ts +79 -0
  14. package/dist/core/harness-control-events.d.ts.map +1 -0
  15. package/dist/core/harness-control-events.js +339 -0
  16. package/dist/core/harness-control-events.js.map +1 -0
  17. package/dist/core/harness-control-replay.d.ts +26 -0
  18. package/dist/core/harness-control-replay.d.ts.map +1 -0
  19. package/dist/core/harness-control-replay.js +155 -0
  20. package/dist/core/harness-control-replay.js.map +1 -0
  21. package/dist/core/keybindings.d.ts +12 -0
  22. package/dist/core/keybindings.d.ts.map +1 -1
  23. package/dist/core/keybindings.js +31 -1
  24. package/dist/core/keybindings.js.map +1 -1
  25. package/dist/core/mcp-inventory.d.ts +40 -0
  26. package/dist/core/mcp-inventory.d.ts.map +1 -0
  27. package/dist/core/mcp-inventory.js +104 -0
  28. package/dist/core/mcp-inventory.js.map +1 -0
  29. package/dist/core/settings-manager.d.ts +3 -0
  30. package/dist/core/settings-manager.d.ts.map +1 -1
  31. package/dist/core/settings-manager.js +4 -0
  32. package/dist/core/settings-manager.js.map +1 -1
  33. package/dist/core/slash-commands.d.ts.map +1 -1
  34. package/dist/core/slash-commands.js +4 -1
  35. package/dist/core/slash-commands.js.map +1 -1
  36. package/dist/core/spec-kit/compiler.d.ts +67 -0
  37. package/dist/core/spec-kit/compiler.d.ts.map +1 -0
  38. package/dist/core/spec-kit/compiler.js +268 -0
  39. package/dist/core/spec-kit/compiler.js.map +1 -0
  40. package/dist/core/transaction-coordinator.d.ts +30 -0
  41. package/dist/core/transaction-coordinator.d.ts.map +1 -0
  42. package/dist/core/transaction-coordinator.js +173 -0
  43. package/dist/core/transaction-coordinator.js.map +1 -0
  44. package/dist/migrations.d.ts +35 -0
  45. package/dist/migrations.d.ts.map +1 -1
  46. package/dist/migrations.js +409 -3
  47. package/dist/migrations.js.map +1 -1
  48. package/dist/modes/interactive/components/control-panel.d.ts +29 -0
  49. package/dist/modes/interactive/components/control-panel.d.ts.map +1 -0
  50. package/dist/modes/interactive/components/control-panel.js +90 -0
  51. package/dist/modes/interactive/components/control-panel.js.map +1 -0
  52. package/dist/modes/interactive/components/custom-editor.d.ts.map +1 -1
  53. package/dist/modes/interactive/components/custom-editor.js +21 -13
  54. package/dist/modes/interactive/components/custom-editor.js.map +1 -1
  55. package/dist/modes/interactive/components/index.d.ts +3 -0
  56. package/dist/modes/interactive/components/index.d.ts.map +1 -1
  57. package/dist/modes/interactive/components/index.js +3 -0
  58. package/dist/modes/interactive/components/index.js.map +1 -1
  59. package/dist/modes/interactive/components/mcp-selector.d.ts +28 -0
  60. package/dist/modes/interactive/components/mcp-selector.d.ts.map +1 -0
  61. package/dist/modes/interactive/components/mcp-selector.js +138 -0
  62. package/dist/modes/interactive/components/mcp-selector.js.map +1 -0
  63. package/dist/modes/interactive/components/model-selector.d.ts +12 -3
  64. package/dist/modes/interactive/components/model-selector.d.ts.map +1 -1
  65. package/dist/modes/interactive/components/model-selector.js +120 -27
  66. package/dist/modes/interactive/components/model-selector.js.map +1 -1
  67. package/dist/modes/interactive/components/settings-selector.d.ts +19 -1
  68. package/dist/modes/interactive/components/settings-selector.d.ts.map +1 -1
  69. package/dist/modes/interactive/components/settings-selector.js +21 -8
  70. package/dist/modes/interactive/components/settings-selector.js.map +1 -1
  71. package/dist/modes/interactive/components/skills-selector.d.ts +32 -0
  72. package/dist/modes/interactive/components/skills-selector.d.ts.map +1 -0
  73. package/dist/modes/interactive/components/skills-selector.js +130 -0
  74. package/dist/modes/interactive/components/skills-selector.js.map +1 -0
  75. package/dist/modes/interactive/components/thinking-selector.d.ts +7 -8
  76. package/dist/modes/interactive/components/thinking-selector.d.ts.map +1 -1
  77. package/dist/modes/interactive/components/thinking-selector.js +7 -48
  78. package/dist/modes/interactive/components/thinking-selector.js.map +1 -1
  79. package/dist/modes/interactive/interactive-mode.d.ts +9 -0
  80. package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  81. package/dist/modes/interactive/interactive-mode.js +340 -97
  82. package/dist/modes/interactive/interactive-mode.js.map +1 -1
  83. package/dist/modes/interactive/theme/catppuccin-mocha.json +86 -0
  84. package/dist/modes/interactive/theme/dracula.json +86 -0
  85. package/dist/modes/interactive/theme/gruvbox-dark.json +86 -0
  86. package/dist/modes/interactive/theme/kanagawa-dragon.json +86 -0
  87. package/dist/modes/interactive/theme/omk-control-panel.json +91 -0
  88. package/dist/modes/interactive/theme/theme.d.ts.map +1 -1
  89. package/dist/modes/interactive/theme/theme.js +41 -16
  90. package/dist/modes/interactive/theme/theme.js.map +1 -1
  91. package/dist/modes/interactive/theme/tokyo-night-storm.json +86 -0
  92. package/docs/compaction.md +21 -8
  93. package/docs/quickstart.md +1 -1
  94. package/docs/themes.md +10 -2
  95. package/docs/usage.md +2 -1
  96. package/examples/extensions/custom-provider-anthropic/package-lock.json +2 -2
  97. package/examples/extensions/custom-provider-anthropic/package.json +1 -1
  98. package/examples/extensions/custom-provider-gitlab-duo/package.json +1 -1
  99. package/examples/extensions/gondolin/package-lock.json +2 -2
  100. package/examples/extensions/gondolin/package.json +1 -1
  101. package/examples/extensions/sandbox/package-lock.json +2 -2
  102. package/examples/extensions/sandbox/package.json +1 -1
  103. package/examples/extensions/with-deps/package-lock.json +2 -2
  104. package/examples/extensions/with-deps/package.json +1 -1
  105. package/package.json +4 -4
@@ -1,12 +1,15 @@
1
1
  /**
2
2
  * One-time migrations that run on startup.
3
3
  */
4
+ import { createHash } from "node:crypto";
4
5
  import chalk from "chalk";
5
- import { chmodSync, existsSync, mkdirSync, readdirSync, readFileSync, renameSync, rmSync, writeFileSync } from "fs";
6
- import { dirname, join } from "path";
6
+ import { chmodSync, existsSync, lstatSync, mkdirSync, readdirSync, readFileSync, realpathSync, renameSync, rmSync, writeFileSync, } from "fs";
7
+ import { basename, dirname, join, relative, resolve, sep } from "path";
7
8
  import { CONFIG_DIR_NAME, getAgentDir, getBinDir } from "./config.js";
9
+ import { recordHarnessControlEvent } from "./core/harness-control-events.js";
8
10
  import { migrateKeybindingsConfig } from "./core/keybindings.js";
9
11
  import { isLegacyEnvVarNameConfigValue } from "./core/resolve-config-value.js";
12
+ import { runHarnessControlTransactionSync } from "./core/transaction-coordinator.js";
10
13
  import { stripJsonComments } from "./utils/json.js";
11
14
  const MIGRATION_GUIDE_URL = "https://github.com/dmae97/open-multi-agent-kit/blob/main/packages/coding-agent/CHANGELOG.md#extensions-migration";
12
15
  const EXTENSIONS_DOC_URL = "https://github.com/dmae97/open-multi-agent-kit/blob/main/packages/coding-agent/docs/extensions.md";
@@ -315,11 +318,414 @@ function migrateToolsToBin() {
315
318
  * Check for deprecated hooks/ and tools/ directories.
316
319
  * Note: tools/ may contain fd/rg binaries extracted by pi, so only warn if it has other files.
317
320
  */
321
+ const EXTENSION_SOURCE_EXTENSIONS = new Set([".ts", ".js", ".mts", ".cts", ".mjs", ".cjs", ".tsx", ".jsx"]);
322
+ const LEGACY_MANIFEST_FIELDS = ["hooks", "customTools", "extensions"];
323
+ const MAX_LEGACY_EXTENSION_SCAN_DEPTH = 8;
324
+ function extensionName(name) {
325
+ const dotIndex = name.lastIndexOf(".");
326
+ return dotIndex >= 0 ? name.slice(dotIndex) : "";
327
+ }
328
+ function isExtensionSourceFile(name) {
329
+ return EXTENSION_SOURCE_EXTENSIONS.has(extensionName(name));
330
+ }
331
+ function isObjectRecord(value) {
332
+ return typeof value === "object" && value !== null && !Array.isArray(value);
333
+ }
334
+ function manifestEntrypointReferencesSource(value) {
335
+ if (typeof value === "string")
336
+ return isExtensionSourceFile(value);
337
+ if (Array.isArray(value))
338
+ return value.some((entry) => manifestEntrypointReferencesSource(entry));
339
+ if (isObjectRecord(value))
340
+ return Object.values(value).some((entry) => manifestEntrypointReferencesSource(entry));
341
+ return false;
342
+ }
343
+ function inspectLegacyExtensionManifest(dir, scope) {
344
+ const packageJsonPath = join(dir, "package.json");
345
+ if (!existsSync(packageJsonPath))
346
+ return undefined;
347
+ try {
348
+ const parsed = JSON.parse(readFileSync(packageJsonPath, "utf-8"));
349
+ if (!isObjectRecord(parsed))
350
+ return undefined;
351
+ const manifests = [parsed.pi, parsed.omk].filter(isObjectRecord);
352
+ const hasLegacyManifestArray = manifests.some((manifest) => LEGACY_MANIFEST_FIELDS.some((field) => Array.isArray(manifest[field])));
353
+ const hasManifestEntrypoint = [parsed.main, parsed.module, parsed.exports].some(manifestEntrypointReferencesSource);
354
+ if (!hasLegacyManifestArray && !hasManifestEntrypoint)
355
+ return undefined;
356
+ return {
357
+ code: "LEGACY_EXTENSION_ENTRYPOINT",
358
+ scope,
359
+ path: packageJsonPath,
360
+ classification: "legacy",
361
+ confidence: hasLegacyManifestArray ? 0.96 : 0.82,
362
+ evidence: [hasLegacyManifestArray ? "manifest legacy array" : "manifest entrypoint"],
363
+ recommendedAction: "move-to-extensions",
364
+ };
365
+ }
366
+ catch {
367
+ return {
368
+ code: "LEGACY_EXTENSION_UNKNOWN",
369
+ scope,
370
+ path: packageJsonPath,
371
+ classification: "unknown",
372
+ confidence: 0.5,
373
+ evidence: ["package.json parse failed"],
374
+ recommendedAction: "inspect-manifest",
375
+ };
376
+ }
377
+ }
378
+ function isPathWithin(parent, child) {
379
+ const resolvedParent = resolve(parent);
380
+ const resolvedChild = resolve(child);
381
+ return resolvedChild === resolvedParent || resolvedChild.startsWith(`${resolvedParent}${sep}`);
382
+ }
383
+ function addLegacySourceDiagnostic(diagnostics, scope, entryPath, evidence) {
384
+ diagnostics.push({
385
+ code: "LEGACY_EXTENSION_ENTRYPOINT",
386
+ scope,
387
+ path: entryPath,
388
+ classification: "legacy",
389
+ confidence: 0.98,
390
+ evidence: [evidence],
391
+ recommendedAction: "move-to-extensions",
392
+ });
393
+ }
394
+ function addUnknownExtensionDiagnostic(diagnostics, scope, entryPath, evidence, confidence = 0.4) {
395
+ diagnostics.push({
396
+ code: "LEGACY_EXTENSION_UNKNOWN",
397
+ scope,
398
+ path: entryPath,
399
+ classification: "unknown",
400
+ confidence,
401
+ evidence: [evidence],
402
+ recommendedAction: "inspect-manifest",
403
+ });
404
+ }
405
+ function collectLegacyExtensionDiagnostics(entryPath, hooksRealPath, scope, depth, visitedRealPaths, diagnostics) {
406
+ if (depth > MAX_LEGACY_EXTENSION_SCAN_DEPTH) {
407
+ addUnknownExtensionDiagnostic(diagnostics, scope, entryPath, "maximum scan depth exceeded");
408
+ return;
409
+ }
410
+ let currentPath = entryPath;
411
+ let stats;
412
+ try {
413
+ stats = lstatSync(currentPath);
414
+ if (stats.isSymbolicLink()) {
415
+ const targetPath = realpathSync(currentPath);
416
+ if (!isPathWithin(hooksRealPath, targetPath)) {
417
+ addUnknownExtensionDiagnostic(diagnostics, scope, currentPath, "symlink target outside hooks directory", 0.6);
418
+ return;
419
+ }
420
+ currentPath = targetPath;
421
+ stats = lstatSync(currentPath);
422
+ }
423
+ }
424
+ catch {
425
+ addUnknownExtensionDiagnostic(diagnostics, scope, entryPath, "path stat failed");
426
+ return;
427
+ }
428
+ if (stats.isFile()) {
429
+ if (isExtensionSourceFile(entryPath) || isExtensionSourceFile(currentPath)) {
430
+ addLegacySourceDiagnostic(diagnostics, scope, entryPath, basename(entryPath));
431
+ }
432
+ return;
433
+ }
434
+ if (!stats.isDirectory())
435
+ return;
436
+ let directoryRealPath;
437
+ try {
438
+ directoryRealPath = realpathSync(currentPath);
439
+ }
440
+ catch {
441
+ addUnknownExtensionDiagnostic(diagnostics, scope, entryPath, "directory realpath failed");
442
+ return;
443
+ }
444
+ if (visitedRealPaths.has(directoryRealPath))
445
+ return;
446
+ visitedRealPaths.add(directoryRealPath);
447
+ const manifestDiagnostic = inspectLegacyExtensionManifest(currentPath, scope);
448
+ if (manifestDiagnostic)
449
+ diagnostics.push({ ...manifestDiagnostic, path: join(entryPath, "package.json") });
450
+ let entries;
451
+ try {
452
+ entries = readdirSync(currentPath, { withFileTypes: true });
453
+ }
454
+ catch {
455
+ addUnknownExtensionDiagnostic(diagnostics, scope, entryPath, "directory read failed");
456
+ return;
457
+ }
458
+ for (const entry of entries) {
459
+ if (entry.name.startsWith("."))
460
+ continue;
461
+ collectLegacyExtensionDiagnostics(join(entryPath, entry.name), hooksRealPath, scope, depth + 1, visitedRealPaths, diagnostics);
462
+ }
463
+ }
464
+ function getLegacyExtensionDiagnosticsForHooksDir(hooksDir, scope) {
465
+ if (!existsSync(hooksDir))
466
+ return [];
467
+ try {
468
+ const hooksRealPath = realpathSync(hooksDir);
469
+ const diagnostics = [];
470
+ const visitedRealPaths = new Set();
471
+ for (const entry of readdirSync(hooksDir, { withFileTypes: true })) {
472
+ if (entry.name.startsWith("."))
473
+ continue;
474
+ collectLegacyExtensionDiagnostics(join(hooksDir, entry.name), hooksRealPath, scope, 1, visitedRealPaths, diagnostics);
475
+ }
476
+ return diagnostics;
477
+ }
478
+ catch {
479
+ return [
480
+ {
481
+ code: "LEGACY_EXTENSION_UNKNOWN",
482
+ scope,
483
+ path: hooksDir,
484
+ classification: "unknown",
485
+ confidence: 0.4,
486
+ evidence: ["directory read failed"],
487
+ recommendedAction: "inspect-manifest",
488
+ },
489
+ ];
490
+ }
491
+ }
492
+ function getExtensionDeprecationScanTargets(cwd) {
493
+ const agentDir = getAgentDir();
494
+ const projectDir = join(cwd, CONFIG_DIR_NAME);
495
+ return [
496
+ {
497
+ scope: "global",
498
+ hooksDir: join(agentDir, "hooks"),
499
+ extensionsDir: join(agentDir, "extensions"),
500
+ },
501
+ {
502
+ scope: "project",
503
+ hooksDir: join(projectDir, "hooks"),
504
+ extensionsDir: join(projectDir, "extensions"),
505
+ },
506
+ ];
507
+ }
508
+ export function getExtensionDeprecationDiagnostics(cwd) {
509
+ return getExtensionDeprecationScanTargets(cwd).flatMap((target) => getLegacyExtensionDiagnosticsForHooksDir(target.hooksDir, target.scope));
510
+ }
511
+ function getMigrationSourcePath(diagnostic) {
512
+ const name = basename(diagnostic.path);
513
+ if (name === "package.json" || /^index\.[cm]?[tj]sx?$/.test(name)) {
514
+ return dirname(diagnostic.path);
515
+ }
516
+ return diagnostic.path;
517
+ }
518
+ function createMigrationSourceStat(path) {
519
+ try {
520
+ const stats = lstatSync(path);
521
+ return {
522
+ isDirectory: stats.isDirectory(),
523
+ size: stats.size,
524
+ mtimeMs: stats.mtimeMs,
525
+ mode: stats.mode,
526
+ };
527
+ }
528
+ catch {
529
+ return undefined;
530
+ }
531
+ }
532
+ function createMigrationSourceStatTree(path, depth = 0) {
533
+ const sourceStat = createMigrationSourceStat(path);
534
+ if (!sourceStat)
535
+ return undefined;
536
+ if (!sourceStat.isDirectory || depth >= MAX_LEGACY_EXTENSION_SCAN_DEPTH) {
537
+ return { name: basename(path), sourceStat };
538
+ }
539
+ const children = readdirSync(path)
540
+ .filter((entry) => !entry.startsWith("."))
541
+ .sort()
542
+ .map((entry) => createMigrationSourceStatTree(join(path, entry), depth + 1));
543
+ return { name: basename(path), sourceStat, children };
544
+ }
545
+ function createMigrationSourceContentTree(path, depth = 0) {
546
+ const sourceStat = createMigrationSourceStat(path);
547
+ if (!sourceStat)
548
+ return undefined;
549
+ if (!sourceStat.isDirectory) {
550
+ try {
551
+ return {
552
+ name: basename(path),
553
+ type: "file",
554
+ sha256: createHash("sha256").update(readFileSync(path)).digest("hex"),
555
+ };
556
+ }
557
+ catch {
558
+ return undefined;
559
+ }
560
+ }
561
+ if (depth >= MAX_LEGACY_EXTENSION_SCAN_DEPTH)
562
+ return { name: basename(path), type: "directory", truncated: true };
563
+ const children = readdirSync(path)
564
+ .filter((entry) => !entry.startsWith("."))
565
+ .sort()
566
+ .map((entry) => createMigrationSourceContentTree(join(path, entry), depth + 1));
567
+ return { name: basename(path), type: "directory", children };
568
+ }
569
+ function hashMigrationSourceStat(path) {
570
+ const tree = createMigrationSourceStatTree(path);
571
+ if (!tree)
572
+ return undefined;
573
+ return createHash("sha256").update(JSON.stringify(tree)).digest("hex");
574
+ }
575
+ function hashMigrationSourceContent(path) {
576
+ const tree = createMigrationSourceContentTree(path);
577
+ if (!tree)
578
+ return undefined;
579
+ return createHash("sha256").update(JSON.stringify(tree)).digest("hex");
580
+ }
581
+ function addMigrationSourceStat(action) {
582
+ const sourceStat = createMigrationSourceStat(action.from);
583
+ return {
584
+ ...action,
585
+ sourceStat,
586
+ sourceStatHash: hashMigrationSourceStat(action.from),
587
+ sourceContentHash: hashMigrationSourceContent(action.from),
588
+ };
589
+ }
590
+ export function createExtensionMigrationPlan(cwd) {
591
+ const diagnostics = getExtensionDeprecationDiagnostics(cwd);
592
+ const targets = getExtensionDeprecationScanTargets(cwd);
593
+ const actions = [];
594
+ const seenSources = new Set();
595
+ for (const diagnostic of diagnostics) {
596
+ if (diagnostic.classification !== "legacy")
597
+ continue;
598
+ const target = targets.find((candidate) => candidate.scope === diagnostic.scope);
599
+ if (!target)
600
+ continue;
601
+ const sourcePath = getMigrationSourcePath(diagnostic);
602
+ if (!isPathWithin(target.hooksDir, sourcePath))
603
+ continue;
604
+ if ([...seenSources].some((seenSource) => isPathWithin(seenSource, sourcePath)))
605
+ continue;
606
+ for (const seenSource of [...seenSources]) {
607
+ if (isPathWithin(sourcePath, seenSource)) {
608
+ seenSources.delete(seenSource);
609
+ const actionIndex = actions.findIndex((action) => action.from === seenSource);
610
+ if (actionIndex >= 0)
611
+ actions.splice(actionIndex, 1);
612
+ }
613
+ }
614
+ seenSources.add(sourcePath);
615
+ const relativeSource = relative(target.hooksDir, sourcePath);
616
+ const destinationPath = join(target.extensionsDir, relativeSource);
617
+ const destinationExists = existsSync(destinationPath);
618
+ actions.push(addMigrationSourceStat({
619
+ action: "move",
620
+ scope: diagnostic.scope,
621
+ from: sourcePath,
622
+ to: destinationPath,
623
+ status: destinationExists ? "blocked" : "ready",
624
+ reason: diagnostic.evidence.join(", "),
625
+ blocker: destinationExists ? "target already exists" : undefined,
626
+ }));
627
+ }
628
+ recordHarnessControlEvent("extension.migration.plan", "completed", {
629
+ diagnostics: diagnostics.length,
630
+ legacyDiagnostics: diagnostics.filter((diagnostic) => diagnostic.classification === "legacy").length,
631
+ unknownDiagnostics: diagnostics.filter((diagnostic) => diagnostic.classification === "unknown").length,
632
+ actions: actions.length,
633
+ blockedActions: actions.filter((action) => action.status === "blocked").length,
634
+ });
635
+ return { diagnostics, actions };
636
+ }
637
+ export function applyExtensionMigrationPlan(cwd, plan = createExtensionMigrationPlan(cwd)) {
638
+ const readyActions = plan.actions.filter((action) => action.status === "ready");
639
+ const preflightBlocked = plan.actions.filter((action) => action.status !== "ready");
640
+ if (preflightBlocked.length > 0) {
641
+ recordHarnessControlEvent("extension.migration.apply", "blocked", {
642
+ actions: plan.actions.length,
643
+ blockedActions: preflightBlocked.length,
644
+ reason: "preflight blocked actions present",
645
+ });
646
+ return plan;
647
+ }
648
+ const appliedMoves = [];
649
+ let appliedPlan = plan;
650
+ const transaction = runHarnessControlTransactionSync({
651
+ kind: "extension.migration.apply",
652
+ data: {
653
+ actions: plan.actions.length,
654
+ readyActions: readyActions.length,
655
+ cwd,
656
+ },
657
+ beforeState: plan.actions.map((action) => ({
658
+ from: action.from,
659
+ to: action.to,
660
+ status: action.status,
661
+ sourceStatHash: action.sourceStatHash,
662
+ sourceContentHash: action.sourceContentHash,
663
+ })),
664
+ afterState: () => appliedPlan.actions.map((action) => ({ from: action.from, to: action.to, status: action.status })),
665
+ commit: () => {
666
+ for (const action of readyActions) {
667
+ if (!existsSync(action.from))
668
+ throw new Error(`source missing: ${action.from}`);
669
+ if (existsSync(action.to))
670
+ throw new Error(`target already exists: ${action.to}`);
671
+ const currentContentHash = hashMigrationSourceContent(action.from);
672
+ if (action.sourceContentHash && currentContentHash !== action.sourceContentHash) {
673
+ throw new Error(`source content changed before migration: ${action.from}`);
674
+ }
675
+ const currentHash = hashMigrationSourceStat(action.from);
676
+ if (action.sourceStatHash && currentHash !== action.sourceStatHash) {
677
+ throw new Error(`source changed before migration: ${action.from}`);
678
+ }
679
+ }
680
+ for (const action of readyActions) {
681
+ mkdirSync(dirname(action.to), { recursive: true });
682
+ renameSync(action.from, action.to);
683
+ appliedMoves.push({ from: action.from, to: action.to });
684
+ }
685
+ appliedPlan = {
686
+ diagnostics: plan.diagnostics,
687
+ actions: plan.actions.map((action) => action.status === "ready" ? { ...action, status: "applied" } : action),
688
+ };
689
+ return appliedPlan;
690
+ },
691
+ rollback: () => {
692
+ for (const move of [...appliedMoves].reverse()) {
693
+ if (existsSync(move.to) && !existsSync(move.from)) {
694
+ mkdirSync(dirname(move.from), { recursive: true });
695
+ renameSync(move.to, move.from);
696
+ }
697
+ }
698
+ appliedPlan = {
699
+ diagnostics: plan.diagnostics,
700
+ actions: plan.actions.map((action) => action.status === "ready" ? { ...action, status: "rolled_back" } : action),
701
+ };
702
+ },
703
+ });
704
+ if (transaction.status === "completed")
705
+ return appliedPlan;
706
+ const recoveryJournalPath = transaction.status === "in_doubt" ? join(cwd, ".omk", "runs", "extension-migration-recovery.json") : undefined;
707
+ if (recoveryJournalPath) {
708
+ mkdirSync(dirname(recoveryJournalPath), { recursive: true });
709
+ writeFileSync(recoveryJournalPath, `${JSON.stringify({ status: "in_doubt", appliedMoves, error: String(transaction.error) }, null, 2)}\n`, "utf-8");
710
+ }
711
+ return {
712
+ diagnostics: plan.diagnostics,
713
+ actions: plan.actions.map((action) => action.status === "ready"
714
+ ? {
715
+ ...action,
716
+ status: transaction.status === "in_doubt" ? "in_doubt" : "blocked",
717
+ blocker: transaction.error instanceof Error ? transaction.error.message : String(transaction.error),
718
+ recoveryJournalPath,
719
+ }
720
+ : action),
721
+ };
722
+ }
318
723
  function checkDeprecatedExtensionDirs(baseDir, label) {
319
724
  const hooksDir = join(baseDir, "hooks");
320
725
  const toolsDir = join(baseDir, "tools");
321
726
  const warnings = [];
322
- if (existsSync(hooksDir)) {
727
+ const scope = label === "Global" ? "global" : "project";
728
+ if (getLegacyExtensionDiagnosticsForHooksDir(hooksDir, scope).some((diagnostic) => diagnostic.classification === "legacy")) {
323
729
  warnings.push(`${label} hooks/ directory found. Hooks have been renamed to extensions.`);
324
730
  }
325
731
  if (existsSync(toolsDir)) {