goalbuddy 0.3.5 → 0.3.7

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 (83) hide show
  1. package/README.md +46 -12
  2. package/RELEASE-0.3.5.md +4 -4
  3. package/RELEASE-0.3.7.md +127 -0
  4. package/goalbuddy/SKILL.md +53 -23
  5. package/goalbuddy/agents/README.md +1 -1
  6. package/goalbuddy/agents/goal_judge.toml +8 -4
  7. package/goalbuddy/agents/goal_worker.toml +8 -5
  8. package/goalbuddy/scripts/check-goal-state.mjs +129 -0
  9. package/goalbuddy/scripts/render-task-prompt.mjs +83 -5
  10. package/{plugins/goalbuddy/skills/goalbuddy/extend → goalbuddy/surfaces}/local-goal-board/README.md +7 -9
  11. package/{plugins/goalbuddy/skills/goalbuddy/extend → goalbuddy/surfaces}/local-goal-board/examples/sample-goal/state.yaml +5 -5
  12. package/{plugins/goalbuddy/skills/goalbuddy/extend → goalbuddy/surfaces}/local-goal-board/examples/subgoal-parent/state.yaml +3 -3
  13. package/goalbuddy/{extend → surfaces}/local-goal-board/examples/subgoal-parent/subgoals/T004-board-view/state.yaml +3 -3
  14. package/goalbuddy/{extend → surfaces}/local-goal-board/scripts/lib/goal-board.mjs +250 -9
  15. package/goalbuddy/{extend → surfaces}/local-goal-board/scripts/local-goal-board.mjs +4 -4
  16. package/goalbuddy/{extend → surfaces}/local-goal-board/test/local-goal-board.test.mjs +67 -9
  17. package/goalbuddy/templates/agents.md +3 -2
  18. package/goalbuddy/templates/goal.md +27 -4
  19. package/goalbuddy/templates/state.yaml +13 -7
  20. package/internal/assets/goalbuddy-v0.3.7-release.png +0 -0
  21. package/internal/cli/goal-maker.mjs +112 -714
  22. package/package.json +4 -4
  23. package/plugins/goalbuddy/.claude-plugin/plugin.json +3 -4
  24. package/plugins/goalbuddy/.codex-plugin/plugin.json +5 -6
  25. package/plugins/goalbuddy/README.md +4 -3
  26. package/plugins/goalbuddy/agents/goal-judge.md +8 -4
  27. package/plugins/goalbuddy/agents/goal-worker.md +6 -4
  28. package/plugins/goalbuddy/skills/goalbuddy/SKILL.md +53 -23
  29. package/plugins/goalbuddy/skills/goalbuddy/agents/README.md +1 -1
  30. package/plugins/goalbuddy/skills/goalbuddy/agents/goal_judge.toml +8 -4
  31. package/plugins/goalbuddy/skills/goalbuddy/agents/goal_worker.toml +8 -5
  32. package/plugins/goalbuddy/skills/goalbuddy/scripts/check-goal-state.mjs +129 -0
  33. package/plugins/goalbuddy/skills/goalbuddy/scripts/render-task-prompt.mjs +83 -5
  34. package/{goalbuddy/extend → plugins/goalbuddy/skills/goalbuddy/surfaces}/local-goal-board/README.md +7 -9
  35. package/{goalbuddy/extend → plugins/goalbuddy/skills/goalbuddy/surfaces}/local-goal-board/examples/sample-goal/state.yaml +5 -5
  36. package/{goalbuddy/extend → plugins/goalbuddy/skills/goalbuddy/surfaces}/local-goal-board/examples/subgoal-parent/state.yaml +3 -3
  37. package/plugins/goalbuddy/skills/goalbuddy/{extend → surfaces}/local-goal-board/examples/subgoal-parent/subgoals/T004-board-view/state.yaml +3 -3
  38. package/plugins/goalbuddy/skills/goalbuddy/{extend → surfaces}/local-goal-board/scripts/lib/goal-board.mjs +250 -9
  39. package/plugins/goalbuddy/skills/goalbuddy/{extend → surfaces}/local-goal-board/scripts/local-goal-board.mjs +4 -4
  40. package/plugins/goalbuddy/skills/goalbuddy/{extend → surfaces}/local-goal-board/test/local-goal-board.test.mjs +67 -9
  41. package/plugins/goalbuddy/skills/goalbuddy/templates/agents.md +3 -2
  42. package/plugins/goalbuddy/skills/goalbuddy/templates/goal.md +27 -4
  43. package/plugins/goalbuddy/skills/goalbuddy/templates/state.yaml +13 -7
  44. package/examples/extend-catalog-workflow/goal.md +0 -53
  45. package/examples/extend-catalog-workflow/notes/T001-extension-model-map.md +0 -47
  46. package/examples/extend-catalog-workflow/notes/T002-architecture-decision.md +0 -48
  47. package/examples/extend-catalog-workflow/notes/T003-implementation-summary.md +0 -43
  48. package/examples/extend-catalog-workflow/notes/T004-root-extend-folder.md +0 -24
  49. package/examples/extend-catalog-workflow/notes/T005-layout-cleanup.md +0 -46
  50. package/examples/extend-catalog-workflow/notes/T006-catalog-location.md +0 -50
  51. package/examples/extend-catalog-workflow/notes/T999-completion-audit.md +0 -36
  52. package/examples/extend-catalog-workflow/state.yaml +0 -327
  53. package/examples/github-pr-workflow-extension/pr-handoff.md +0 -46
  54. package/goalbuddy/extend/github-projects/README.md +0 -105
  55. package/goalbuddy/extend/github-projects/examples/goal-board-sync/state.yaml +0 -63
  56. package/goalbuddy/extend/github-projects/extension.yaml +0 -43
  57. package/goalbuddy/extend/github-projects/scripts/lib/github-projects.mjs +0 -728
  58. package/goalbuddy/extend/github-projects/scripts/lib/goal-state.mjs +0 -362
  59. package/goalbuddy/extend/github-projects/scripts/sync-github-project.mjs +0 -193
  60. package/goalbuddy/extend/github-projects/test/github-projects.test.mjs +0 -267
  61. package/goalbuddy/extend/local-goal-board/extension.yaml +0 -39
  62. package/internal/assets/extend-release.png +0 -0
  63. package/internal/assets/extend-release.svg +0 -83
  64. package/plugins/goalbuddy/skills/goalbuddy/extend/github-projects/README.md +0 -105
  65. package/plugins/goalbuddy/skills/goalbuddy/extend/github-projects/examples/goal-board-sync/state.yaml +0 -63
  66. package/plugins/goalbuddy/skills/goalbuddy/extend/github-projects/extension.yaml +0 -43
  67. package/plugins/goalbuddy/skills/goalbuddy/extend/github-projects/scripts/lib/github-projects.mjs +0 -728
  68. package/plugins/goalbuddy/skills/goalbuddy/extend/github-projects/scripts/lib/goal-state.mjs +0 -362
  69. package/plugins/goalbuddy/skills/goalbuddy/extend/github-projects/scripts/sync-github-project.mjs +0 -193
  70. package/plugins/goalbuddy/skills/goalbuddy/extend/github-projects/test/github-projects.test.mjs +0 -267
  71. package/plugins/goalbuddy/skills/goalbuddy/extend/local-goal-board/extension.yaml +0 -39
  72. /package/goalbuddy/{extend → surfaces}/local-goal-board/assets/goalbuddy-mark.png +0 -0
  73. /package/goalbuddy/{extend → surfaces}/local-goal-board/examples/sample-goal/notes/T001-scout.md +0 -0
  74. /package/goalbuddy/{extend → surfaces}/local-goal-board/examples/subgoal-parent/goal.md +0 -0
  75. /package/goalbuddy/{extend → surfaces}/local-goal-board/examples/subgoal-parent/notes/.gitkeep +0 -0
  76. /package/goalbuddy/{extend → surfaces}/local-goal-board/examples/subgoal-parent/subgoals/T004-board-view/goal.md +0 -0
  77. /package/goalbuddy/{extend → surfaces}/local-goal-board/examples/subgoal-parent/subgoals/T004-board-view/notes/.gitkeep +0 -0
  78. /package/plugins/goalbuddy/skills/goalbuddy/{extend → surfaces}/local-goal-board/assets/goalbuddy-mark.png +0 -0
  79. /package/plugins/goalbuddy/skills/goalbuddy/{extend → surfaces}/local-goal-board/examples/sample-goal/notes/T001-scout.md +0 -0
  80. /package/plugins/goalbuddy/skills/goalbuddy/{extend → surfaces}/local-goal-board/examples/subgoal-parent/goal.md +0 -0
  81. /package/plugins/goalbuddy/skills/goalbuddy/{extend → surfaces}/local-goal-board/examples/subgoal-parent/notes/.gitkeep +0 -0
  82. /package/plugins/goalbuddy/skills/goalbuddy/{extend → surfaces}/local-goal-board/examples/subgoal-parent/subgoals/T004-board-view/goal.md +0 -0
  83. /package/plugins/goalbuddy/skills/goalbuddy/{extend → surfaces}/local-goal-board/examples/subgoal-parent/subgoals/T004-board-view/notes/.gitkeep +0 -0
@@ -5,13 +5,12 @@ import {
5
5
  mkdirSync,
6
6
  readdirSync,
7
7
  readFileSync,
8
- renameSync,
9
8
  rmSync,
10
9
  writeFileSync,
11
10
  } from "node:fs";
12
11
  import { createHash } from "node:crypto";
13
12
  import { spawnSync } from "node:child_process";
14
- import { basename, dirname, join, normalize, resolve } from "node:path";
13
+ import { basename, dirname, join, resolve } from "node:path";
15
14
  import { homedir } from "node:os";
16
15
  import { fileURLToPath } from "node:url";
17
16
 
@@ -29,7 +28,6 @@ const claudePluginSource = join(packageRoot, "plugins", "goalbuddy");
29
28
  const packageInfo = JSON.parse(readFileSync(join(packageRoot, "package.json"), "utf8"));
30
29
  const defaultCodexHome = process.env.CODEX_HOME || join(homedir(), ".codex");
31
30
  const defaultClaudeHome = process.env.CLAUDE_HOME || join(homedir(), ".claude");
32
- const defaultCatalogUrl = "https://raw.githubusercontent.com/tolibear/goalbuddy/main/extend/catalog.json";
33
31
  const requiredAgentFiles = [
34
32
  "goal_judge.toml",
35
33
  "goal_scout.toml",
@@ -40,15 +38,11 @@ const requiredClaudeAgentFiles = [
40
38
  "goal-judge.md",
41
39
  "goal-worker.md",
42
40
  ];
43
- const bundledCoreExtensionIds = new Set(["github-projects", "local-goal-board"]);
44
41
  const optionsWithValues = new Set([
45
- "--catalog",
46
- "--catalog-url",
47
42
  "--claude-home",
48
43
  "--codex-home",
49
44
  "--goal",
50
45
  "--host",
51
- "--kind",
52
46
  "--port",
53
47
  "--source",
54
48
  "--target",
@@ -83,15 +77,23 @@ async function main() {
83
77
  break;
84
78
  case "install":
85
79
  case "update":
80
+ if (wantsHelp()) {
81
+ usage();
82
+ break;
83
+ }
86
84
  if (installTargetMode() === "all") {
87
85
  await installEverywhere();
88
86
  } else if (installTargetMode() === "codex") {
89
- await installAll();
87
+ installPlugin();
90
88
  } else {
91
89
  await installClaudeAll();
92
90
  }
93
91
  break;
94
92
  case "agents":
93
+ if (wantsHelp()) {
94
+ usage();
95
+ break;
96
+ }
95
97
  if (targetMode() === "codex") {
96
98
  installAgents();
97
99
  } else {
@@ -99,6 +101,10 @@ async function main() {
99
101
  }
100
102
  break;
101
103
  case "doctor":
104
+ if (wantsHelp()) {
105
+ usage();
106
+ break;
107
+ }
102
108
  if (targetMode() === "codex") {
103
109
  doctor();
104
110
  } else {
@@ -110,11 +116,12 @@ async function main() {
110
116
  checkUpdate();
111
117
  break;
112
118
  case "plugin":
119
+ if (wantsHelp()) {
120
+ pluginUsage();
121
+ break;
122
+ }
113
123
  plugin();
114
124
  break;
115
- case "extend":
116
- await extend();
117
- break;
118
125
  case "board":
119
126
  await board();
120
127
  break;
@@ -164,6 +171,10 @@ function hasFlag(name) {
164
171
  return args.includes(name);
165
172
  }
166
173
 
174
+ function wantsHelp() {
175
+ return hasFlag("--help") || hasFlag("-h");
176
+ }
177
+
167
178
  function positional(index) {
168
179
  return positionalArgs()[index] || "";
169
180
  }
@@ -193,12 +204,7 @@ Usage:
193
204
  ${canonicalCliName} agents [--target claude|codex] [--claude-home <path>] [--codex-home <path>] [--force]
194
205
  ${canonicalCliName} doctor [--target claude|codex] [--claude-home <path>] [--codex-home <path>] [--goal-ready]
195
206
  ${canonicalCliName} check-update [--json]
196
- ${canonicalCliName} extend [--catalog-url <url-or-path>] [--kind <kind>] [--json]
197
- ${canonicalCliName} extend <id> [--catalog-url <url-or-path>] [--json]
198
- ${canonicalCliName} extend install <id> [--catalog-url <url-or-path>] [--dry-run] [--force] [--json]
199
- ${canonicalCliName} extend install --all [--catalog-url <url-or-path>] [--dry-run] [--force] [--json]
200
- ${canonicalCliName} extend doctor [<id>] [--codex-home <path>] [--json]
201
- ${canonicalCliName} board <docs/goals/slug> [--catalog-url <url-or-path>] [--host <host>] [--port <port>] [--once] [--json]
207
+ ${canonicalCliName} board <docs/goals/slug> [--host <host>] [--port <port>] [--once] [--json]
202
208
  ${canonicalCliName} prompt <docs/goals/slug> [--task T###] [--board <path/to/state.yaml>] [--json]
203
209
  ${canonicalCliName} parallel-plan <docs/goals/slug> [--json]
204
210
 
@@ -215,8 +221,6 @@ Compatibility:
215
221
  Environment:
216
222
  CODEX_HOME Overrides the default ~/.codex target.
217
223
  CLAUDE_HOME Overrides the default ~/.claude target (and selects Claude Code unless --target codex is set).
218
- GOALBUDDY_EXTEND_CATALOG_URL Overrides the default GitHub-hosted extension catalog.
219
- GOAL_MAKER_EXTEND_CATALOG_URL Legacy fallback for the extension catalog.
220
224
  `);
221
225
  }
222
226
 
@@ -269,16 +273,11 @@ function installClaudeSkill({ quiet = false } = {}) {
269
273
 
270
274
  const previousMetadata = readInstallMetadata(target);
271
275
  const previousFingerprint = existsSync(target) ? directoryFingerprint(target, { exclude: installFingerprintExcludes() }) : "";
272
- const preservedExtensions = preserveInstalledExtensions([target], { tempRoot: claudeHome() });
273
- const extensionTempPath = preservedExtensions.tempPath;
274
- const preservedExtensionIds = preservedExtensions.ids;
275
276
 
276
277
  mkdirSync(dirname(target), { recursive: true });
277
278
  rmSync(target, { recursive: true, force: true });
278
279
  cpSync(skillSource, target, { recursive: true });
279
- restoreInstalledExtensions(target, extensionTempPath);
280
280
  writeInstallMetadata(target, previousMetadata);
281
- cleanupPreservedExtensions([extensionTempPath]);
282
281
 
283
282
  const currentFingerprint = directoryFingerprint(target, { exclude: installFingerprintExcludes() });
284
283
  const status = previousFingerprint
@@ -291,7 +290,6 @@ function installClaudeSkill({ quiet = false } = {}) {
291
290
  path: target,
292
291
  previous_version: previousMetadata?.package_version || "",
293
292
  current_version: packageInfo.version,
294
- preserved_extensions: preservedExtensionIds,
295
293
  };
296
294
  }
297
295
 
@@ -342,7 +340,6 @@ async function buildClaudeInstallReport() {
342
340
  skill: installClaudeSkill({ quiet }),
343
341
  agents: installClaudeAgents({ quiet }),
344
342
  legacy_commands_cleanup: cleanupLegacyClaudeCommands({ quiet }),
345
- extensions: await extensionDiscoverySummary(),
346
343
  warnings: [],
347
344
  };
348
345
 
@@ -443,27 +440,6 @@ function printClaudeInstallReport(report) {
443
440
  if (report.legacy_commands_cleanup?.removed) {
444
441
  console.log(`Removed legacy command: ${report.legacy_commands_cleanup.path}`);
445
442
  }
446
- if (report.skill.preserved_extensions.length) {
447
- console.log(`Preserved extensions: ${report.skill.preserved_extensions.join(", ")}`);
448
- }
449
-
450
- if (report.extensions?.error) {
451
- console.log("");
452
- console.log(`Extensions: unavailable (${report.extensions.error})`);
453
- } else if (report.extensions) {
454
- console.log("");
455
- console.log(`Extensions: ${report.extensions.available_count} available from ${report.extensions.catalog_url}`);
456
- if (report.extensions.recommended?.length) {
457
- console.log("");
458
- console.log("Recommended:");
459
- for (const extension of report.extensions.recommended.slice(0, 3)) {
460
- console.log(` ${extension.name || extension.id}`);
461
- if (extension.summary) console.log(` ${extension.summary}`);
462
- console.log(` Details: npx ${extension.next_command}`);
463
- }
464
- }
465
- }
466
-
467
443
  console.log("");
468
444
  console.log("Next:");
469
445
  console.log(` Restart Claude Code, then run: /goal-prep`);
@@ -483,9 +459,6 @@ function installSkill({ force = true, quiet = false } = {}) {
483
459
 
484
460
  const previousMetadata = readInstallMetadata(target) || readInstallMetadata(legacyTarget);
485
461
  const previousFingerprint = existsSync(target) ? directoryFingerprint(target, { exclude: installFingerprintExcludes() }) : "";
486
- const preservedExtensions = preserveInstalledExtensions([target, legacyTarget], { tempRoot: codexHome() });
487
- const extensionTempPath = preservedExtensions.tempPath;
488
- const preservedExtensionIds = preservedExtensions.ids;
489
462
 
490
463
  mkdirSync(dirname(target), { recursive: true });
491
464
  if (existsSync(target)) {
@@ -500,16 +473,13 @@ function installSkill({ force = true, quiet = false } = {}) {
500
473
  cpSync(skillSource, target, {
501
474
  recursive: true,
502
475
  });
503
- restoreInstalledExtensions(target, extensionTempPath);
504
476
  writeInstallMetadata(target, previousMetadata);
505
477
 
506
478
  mkdirSync(dirname(legacyTarget), { recursive: true });
507
479
  rmSync(legacyTarget, { recursive: true, force: true });
508
480
  mkdirSync(legacyTarget, { recursive: true });
509
481
  writeFileSync(join(legacyTarget, "SKILL.md"), compatibilitySkillBody());
510
- restoreInstalledExtensions(legacyTarget, extensionTempPath);
511
482
  writeInstallMetadata(legacyTarget, previousMetadata);
512
- cleanupPreservedExtensions([extensionTempPath]);
513
483
 
514
484
  const currentFingerprint = directoryFingerprint(target, { exclude: installFingerprintExcludes() });
515
485
  const status = previousFingerprint
@@ -523,7 +493,6 @@ function installSkill({ force = true, quiet = false } = {}) {
523
493
  compatibility_path: legacyTarget,
524
494
  previous_version: previousMetadata?.package_version || "",
525
495
  current_version: packageInfo.version,
526
- preserved_extensions: preservedExtensionIds,
527
496
  };
528
497
  }
529
498
 
@@ -552,7 +521,7 @@ This alias has the same invocation boundary as \`$${canonicalSkillName}\`: prepa
552
521
  function installAgents({ quiet = false } = {}) {
553
522
  const source = join(skillSource, "agents");
554
523
  const target = join(codexHome(), "agents");
555
- const force = hasFlag("--force") || command === "update" || command === "install";
524
+ const force = hasFlag("--force") || command === "update" || command === "install" || command === "default" || command === "plugin";
556
525
  mkdirSync(target, { recursive: true });
557
526
 
558
527
  const results = [];
@@ -585,7 +554,6 @@ async function installAll() {
585
554
  codex_home: codexHome(),
586
555
  skill: installSkill({ force: true, quiet }),
587
556
  agents: installAgents({ quiet }),
588
- extensions: await extensionDiscoverySummary(),
589
557
  warnings: [],
590
558
  };
591
559
 
@@ -601,6 +569,7 @@ async function installAll() {
601
569
  function doctor() {
602
570
  const skillPath = join(installedSkillRoot(), "SKILL.md");
603
571
  const legacySkillPath = join(legacyInstalledSkillRoot(), "SKILL.md");
572
+ const plugin = installedCodexPlugin();
604
573
  const agentsPath = join(codexHome(), "agents");
605
574
  const installed = existsSync(skillPath);
606
575
  const legacyInstalled = existsSync(legacySkillPath);
@@ -616,12 +585,38 @@ function doctor() {
616
585
  });
617
586
  const goalRuntime = codexGoalRuntimeStatus();
618
587
  const warnings = [];
588
+ const errors = [];
619
589
  if (!goalRuntime.ready) {
620
590
  warnings.push("native Codex /goal runtime is not ready; run `codex login` and `codex features enable goals` before using /goal.");
621
591
  }
592
+ if (!plugin.skill_installed && !installed) {
593
+ errors.push("Codex GoalBuddy plugin is not installed; run `npx goalbuddy --target codex`.");
594
+ }
595
+ if (plugin.skill_installed && !plugin.enabled) {
596
+ errors.push("Codex GoalBuddy plugin cache exists but is not enabled in config.toml; run `npx goalbuddy --target codex`.");
597
+ }
598
+ for (const file of missingAgents) {
599
+ errors.push(`Missing GoalBuddy Codex agent: ${file}; run \`npx goalbuddy --target codex\`.`);
600
+ }
601
+ for (const file of staleAgents) {
602
+ errors.push(`Stale GoalBuddy Codex agent: ${file}; run \`npx goalbuddy update --target codex\`.`);
603
+ }
604
+ if (hasFlag("--goal-ready") && !goalRuntime.ready) {
605
+ errors.push("Native Codex /goal runtime is not ready. GoalBuddy $goal-prep and local boards are separate from OpenAI-gated native /goal.");
606
+ }
622
607
 
623
608
  console.log(JSON.stringify({
624
609
  codex_home: codexHome(),
610
+ codex_install_model: "plugin",
611
+ expected_state: {
612
+ plugin_cache: true,
613
+ bundled_skill: "$goal-prep",
614
+ standalone_personal_skill: false,
615
+ compatibility_skill: false,
616
+ agents: requiredAgentFiles,
617
+ native_goal: "separate OpenAI-gated Codex feature",
618
+ },
619
+ plugin,
625
620
  skill_installed: installed,
626
621
  skill_path: skillPath,
627
622
  compatibility_skill_installed: legacyInstalled,
@@ -631,11 +626,14 @@ function doctor() {
631
626
  stale_agents: staleAgents,
632
627
  goal_runtime: goalRuntime,
633
628
  warnings,
629
+ errors,
634
630
  }, null, 2));
635
631
 
636
- const installOk = installed && missingAgents.length === 0 && staleAgents.length === 0;
632
+ const pluginOk = plugin.skill_installed && plugin.enabled;
633
+ const legacySkillOk = installed;
634
+ const installOk = (pluginOk || legacySkillOk) && missingAgents.length === 0 && staleAgents.length === 0;
637
635
  const goalReadyOk = !hasFlag("--goal-ready") || goalRuntime.ready;
638
- process.exit(installOk && goalReadyOk ? 0 : 1);
636
+ process.exit(installOk && goalReadyOk && errors.length === 0 ? 0 : 1);
639
637
  }
640
638
 
641
639
  function checkUpdate() {
@@ -680,6 +678,10 @@ function updateReport() {
680
678
 
681
679
  function plugin() {
682
680
  const subcommand = positional(1) || "";
681
+ if (wantsHelp()) {
682
+ pluginUsage();
683
+ return;
684
+ }
683
685
  switch (subcommand) {
684
686
  case "install":
685
687
  installPlugin();
@@ -717,22 +719,17 @@ function installPlugin({ quiet = false } = {}) {
717
719
 
718
720
  const pluginManifest = JSON.parse(readFileSync(pluginManifestPath, "utf8"));
719
721
  const pluginCachePath = pluginCacheRoot(pluginManifest.version);
720
- const pluginSkillPath = join(pluginCachePath, "skills", canonicalSkillDirectory);
721
722
  const marketplace = runCodex(["plugin", "marketplace", "add", source]);
722
723
  if (!marketplace.ok) {
723
724
  throw new Error(`Failed to add Codex plugin marketplace: ${firstLine(marketplace.stderr || marketplace.stdout)}`);
724
725
  }
725
726
 
726
- const legacySkillPaths = legacyCodexSkillRoots();
727
- const existingPluginSkillPath = installedPluginSkillRoot();
728
- const preservedExtensions = preserveInstalledExtensions([existingPluginSkillPath, ...legacySkillPaths], { tempRoot: dirname(pluginCachePath) });
729
727
  mkdirSync(dirname(pluginCachePath), { recursive: true });
730
728
  rmSync(pluginCachePath, { recursive: true, force: true });
731
729
  cpSync(pluginSource, pluginCachePath, { recursive: true });
732
- restoreInstalledExtensions(pluginSkillPath, preservedExtensions.tempPath);
733
- cleanupPreservedExtensions([preservedExtensions.tempPath]);
734
730
  const removedLegacySkillPaths = cleanupLegacyCodexSkills();
735
731
  const configPath = enablePluginConfig();
732
+ const agents = installAgents({ quiet: true });
736
733
 
737
734
  const report = {
738
735
  installed: true,
@@ -743,7 +740,7 @@ function installPlugin({ quiet = false } = {}) {
743
740
  marketplace_source: source,
744
741
  cache_path: pluginCachePath,
745
742
  config_path: configPath,
746
- preserved_extensions: preservedExtensions.ids,
743
+ agents,
747
744
  removed_legacy_skill_paths: removedLegacySkillPaths,
748
745
  };
749
746
 
@@ -758,9 +755,7 @@ function installPlugin({ quiet = false } = {}) {
758
755
  console.log(`Marketplace: ${source}`);
759
756
  console.log(`Cache: ${pluginCachePath}`);
760
757
  console.log(`Config: ${configPath}`);
761
- if (report.preserved_extensions.length) {
762
- console.log(`Preserved extensions: ${report.preserved_extensions.join(", ")}`);
763
- }
758
+ console.log(`Agents: ${summarizeStatuses(report.agents)}`);
764
759
  if (report.removed_legacy_skill_paths.length) {
765
760
  console.log(`Removed legacy personal skills: ${report.removed_legacy_skill_paths.join(", ")}`);
766
761
  }
@@ -768,9 +763,8 @@ function installPlugin({ quiet = false } = {}) {
768
763
  console.log("Restart Codex, then use:");
769
764
  console.log(` $${canonicalSkillName}`);
770
765
  console.log("");
771
- console.log("Bundled visual boards:");
766
+ console.log("Goal surface:");
772
767
  console.log(` npx ${canonicalCliName} board docs/goals/<slug>`);
773
- console.log(` npx ${canonicalCliName} extend github-projects`);
774
768
  return report;
775
769
  }
776
770
 
@@ -912,33 +906,6 @@ function firstLine(value) {
912
906
  return (value || "").split(/\r?\n/).find((line) => line.trim())?.trim() || "";
913
907
  }
914
908
 
915
- async function extend() {
916
- if (args.includes("--help") || args.includes("-h")) {
917
- extendUsage();
918
- return;
919
- }
920
-
921
- const subcommand = positional(1) || "";
922
- switch (subcommand) {
923
- case "":
924
- await extendCatalog();
925
- break;
926
- case "install":
927
- await extendInstall();
928
- break;
929
- case "doctor":
930
- extendDoctor();
931
- break;
932
- case "help":
933
- case "--help":
934
- case "-h":
935
- extendUsage();
936
- break;
937
- default:
938
- await extendDetails(subcommand);
939
- }
940
- }
941
-
942
909
  async function board() {
943
910
  const goal = optionValue("--goal") || positional(1);
944
911
  if (!goal) {
@@ -946,7 +913,7 @@ async function board() {
946
913
  process.exit(2);
947
914
  }
948
915
 
949
- const script = await ensureLocalBoardExtension();
916
+ const script = ensureLocalBoardSurface();
950
917
  const scriptArgs = [script, "--goal", goal];
951
918
  for (const option of ["--host", "--port"]) {
952
919
  const value = optionValue(option);
@@ -1004,302 +971,33 @@ async function parallelPlan() {
1004
971
  process.exit(result.status ?? 1);
1005
972
  }
1006
973
 
1007
- async function ensureLocalBoardExtension() {
1008
- const id = "local-goal-board";
1009
- const script = join(extensionTarget(id), "scripts", "local-goal-board.mjs");
1010
- if (existsSync(script)) return script;
1011
-
1012
- const catalog = await loadCatalog();
1013
- const extension = catalog.extensions.find((candidate) => candidate.id === id);
1014
- if (!extension) {
1015
- throw new Error(`Extension ${id} is not available in ${catalog.url}.`);
1016
- }
1017
-
1018
- await installCatalogExtension(catalog, extension);
974
+ function ensureLocalBoardSurface() {
975
+ const script = join(skillSource, "surfaces", "local-goal-board", "scripts", "local-goal-board.mjs");
1019
976
  if (!existsSync(script)) {
1020
- throw new Error(`Extension ${id} installed, but script is missing: ${script}`);
977
+ throw new Error(`Bundled GoalBuddy board surface is missing: ${script}`);
1021
978
  }
1022
979
  return script;
1023
980
  }
1024
981
 
1025
- function extendUsage() {
1026
- console.log(`${canonicalProductName} Extend
1027
-
1028
- Usage:
1029
- ${canonicalCliName} extend [--catalog-url <url-or-path>] [--kind <kind>] [--json]
1030
- ${canonicalCliName} extend <id> [--catalog-url <url-or-path>] [--json]
1031
- ${canonicalCliName} extend install <id> [--catalog-url <url-or-path>] [--dry-run] [--force] [--json]
1032
- ${canonicalCliName} extend install --all [--catalog-url <url-or-path>] [--dry-run] [--force] [--json]
1033
- ${canonicalCliName} extend doctor [<id>] [--codex-home <path>] [--json]
1034
-
1035
- States:
1036
- available Listed in the catalog.
1037
- installed Copied into the local ${canonicalProductName} skill install.
1038
- enabled Allowed by a goal or task. Not implemented by this command yet.
1039
- configured Required local env/provider settings are present.
1040
-
1041
- Catalog:
1042
- Defaults to ${defaultCatalogUrl}
1043
- Override with --catalog-url, GOALBUDDY_EXTEND_CATALOG_URL, or legacy GOAL_MAKER_EXTEND_CATALOG_URL.
1044
- `);
1045
- }
1046
-
1047
- async function extendCatalog() {
1048
- const catalog = await loadCatalog();
1049
- const kind = optionValue("--kind");
1050
- const extensions = catalog.extensions
1051
- .filter((extension) => !kind || extension.kind === kind)
1052
- .map(extensionWithLocalState);
1053
-
1054
- if (hasFlag("--json")) {
1055
- printJson({ catalog_url: catalog.url, extensions });
1056
- return;
1057
- }
1058
-
1059
- console.log("Available extensions");
1060
- if (extensions.length === 0) {
1061
- console.log("");
1062
- console.log(kind ? `No ${kind} extensions found.` : "No extensions found.");
1063
- return;
1064
- }
1065
- console.log("");
1066
- for (const extension of extensions) {
1067
- console.log(extension.name || extension.id);
1068
- if (extension.summary) console.log(` ${extension.summary}`);
1069
- console.log(` Details: npx ${canonicalCliName} extend ${extension.id}`);
1070
- console.log("");
1071
- }
1072
- console.log("Install all:");
1073
- console.log(` npx ${canonicalCliName} extend install --all`);
1074
- }
1075
-
1076
- async function extendDetails(id) {
1077
- const catalog = await loadCatalog();
1078
- const extension = catalog.extensions.find((candidate) => candidate.id === id);
1079
- if (!extension) {
1080
- printExtensionNotFound(id, catalog.extensions);
1081
- process.exit(1);
1082
- }
1083
- validateCatalogExtension(extension);
1084
- const detailed = extensionWithLocalState(extension);
1085
-
1086
- if (hasFlag("--json")) {
1087
- printJson({ catalog_url: catalog.url, extension: detailed });
1088
- return;
1089
- }
1090
-
1091
- console.log(extension.name || extension.summary || "");
1092
- console.log("");
1093
- if (extension.summary && extension.summary !== extension.name) {
1094
- console.log(extension.summary);
1095
- console.log("");
1096
- }
1097
- console.log(`Status: ${detailed.state.installed ? "installed" : "available"}`);
1098
- console.log(`Configured: ${detailed.state.configured ? "yes" : "no"}`);
1099
- console.log(`ID: ${extension.id}`);
1100
- console.log(`Kind: ${extension.kind}`);
1101
- if (extension.version) console.log(`Version: ${extension.version}`);
1102
- console.log(`Activation: ${extension.activation || "unspecified"}`);
1103
- console.log(`Safe by default: ${extension.safe_by_default ? "yes" : "no"}`);
1104
- console.log(`Requires approval: ${extension.requires_approval ? "yes" : "no"}`);
1105
- if (!detailed.state.configured && detailed.state.missing_env.length) {
1106
- console.log(`Missing env: ${detailed.state.missing_env.join(", ")}`);
1107
- }
1108
- printListSection("Use when", extension.use_when);
1109
- printListSection("Outputs", extension.outputs);
1110
- printListSection("Reads", extension.reads);
1111
- printListSection("Writes", extension.writes);
1112
- printListSection("Side effects", extension.side_effects);
1113
- printListSection("Agent instructions", extension.agent_instructions);
1114
- printListSection("Auth env", extension.auth?.env);
1115
- printSupports(extension.supports);
1116
- console.log("");
1117
- console.log("Local use prompt:");
1118
- console.log(` ${extension.local_use_prompt || `Use the ${extension.name || extension.id} extension for docs/goals/<slug>/goal.md and write its ${firstValue(extension.outputs, "artifact")} as Markdown.`}`);
1119
- console.log("");
1120
- console.log("Install:");
1121
- console.log(` npx ${canonicalCliName} extend install ${extension.id}`);
1122
- console.log("");
1123
- console.log("Preview install:");
1124
- console.log(` npx ${canonicalCliName} extend install ${extension.id} --dry-run`);
1125
- }
1126
-
1127
- function printListSection(title, values) {
1128
- if (!Array.isArray(values) || values.length === 0) return;
1129
- console.log("");
1130
- console.log(`${title}:`);
1131
- for (const value of values) console.log(` - ${value}`);
1132
- }
1133
-
1134
- function printSupports(supports) {
1135
- if (!supports || typeof supports !== "object") return;
1136
- const entries = Object.entries(supports);
1137
- if (entries.length === 0) return;
1138
- console.log("");
1139
- console.log("Supports:");
1140
- for (const [key, value] of entries) console.log(` - ${key}: ${value}`);
1141
- }
1142
-
1143
- function firstValue(values, fallback) {
1144
- return Array.isArray(values) && values.length ? values[0] : fallback;
1145
- }
1146
-
1147
- async function extendInstall() {
1148
- const id = positional(2);
1149
- const catalog = await loadCatalog();
1150
- if (hasFlag("--all")) {
1151
- await extendInstallAll(catalog);
1152
- return;
1153
- }
1154
-
1155
- if (!id) throw new Error(`Missing extension id. Usage: ${canonicalCliName} extend install <id>`);
1156
- const extension = catalog.extensions.find((candidate) => candidate.id === id);
1157
- if (!extension) {
1158
- printExtensionNotFound(id, catalog.extensions);
1159
- process.exit(1);
1160
- }
1161
- const result = await installCatalogExtension(catalog, extension);
1162
-
1163
- if (hasFlag("--dry-run")) {
1164
- if (hasFlag("--json")) {
1165
- printJson({ dry_run: true, extension: extensionWithLocalState(extension), target: result.target, files: result.plan });
1166
- } else {
1167
- console.log(`Would install ${extension.id} to ${result.target}`);
1168
- for (const file of result.plan) console.log(` ${file.path}`);
1169
- }
1170
- return;
1171
- }
1172
-
1173
- if (hasFlag("--json")) {
1174
- printJson({ installed: true, extension: extension.id, target: result.target });
1175
- } else {
1176
- console.log(`Installed ${extension.id} to ${result.target}`);
1177
- }
1178
- }
1179
-
1180
- async function extendInstallAll(catalog) {
1181
- const results = [];
1182
- for (const extension of catalog.extensions) {
1183
- if (existsSync(extensionTarget(extension.id)) && !hasFlag("--force")) {
1184
- validateCatalogExtension(extension);
1185
- results.push({ extension, target: extensionTarget(extension.id), plan: installPlan(catalog, extension, extensionTarget(extension.id)), skipped: true });
1186
- continue;
1187
- }
1188
- results.push(await installCatalogExtension(catalog, extension));
1189
- }
1190
-
1191
- if (hasFlag("--dry-run")) {
1192
- if (hasFlag("--json")) {
1193
- printJson({
1194
- dry_run: true,
1195
- extensions: results.map(({ extension, target, plan }) => ({
1196
- extension: extensionWithLocalState(extension),
1197
- target,
1198
- files: plan,
1199
- })),
1200
- });
1201
- } else {
1202
- console.log(`Would install ${results.length} extensions`);
1203
- for (const { extension, target, plan } of results) {
1204
- console.log(`${extension.id} -> ${target}`);
1205
- for (const file of plan) console.log(` ${file.path}`);
1206
- }
1207
- }
1208
- return;
1209
- }
1210
-
1211
- if (hasFlag("--json")) {
1212
- printJson({
1213
- installed: true,
1214
- count: results.length,
1215
- extensions: results.map(({ extension, target, skipped }) => ({ id: extension.id, target, skipped: Boolean(skipped) })),
1216
- });
1217
- } else {
1218
- const installedCount = results.filter((result) => !result.skipped).length;
1219
- const skippedCount = results.length - installedCount;
1220
- console.log(`Installed ${installedCount} extensions${skippedCount ? `, skipped ${skippedCount} already installed` : ""}`);
1221
- for (const { extension, target, skipped } of results) console.log(` ${extension.id} -> ${target}${skipped ? " (already installed)" : ""}`);
1222
- }
1223
- }
1224
-
1225
- async function installCatalogExtension(catalog, extension) {
1226
- validateCatalogExtension(extension);
1227
- const target = extensionTarget(extension.id);
1228
- const plan = installPlan(catalog, extension, target);
1229
-
1230
- if (hasFlag("--dry-run")) return { extension, target, plan };
1231
-
1232
- assertSkillInstalledForExtensionInstall();
1233
- if (existsSync(target) && !hasFlag("--force")) {
1234
- throw new Error(`Extension already installed: ${target}. Use --force to overwrite.`);
1235
- }
1236
-
1237
- const temp = `${target}.tmp-${process.pid}-${Date.now()}`;
1238
- rmSync(temp, { recursive: true, force: true });
1239
- mkdirSync(temp, { recursive: true });
1240
-
1241
- try {
1242
- for (const file of plan) {
1243
- const content = await readResource(file.url);
1244
- const actualSha = sha256(content);
1245
- if (actualSha !== file.sha256) {
1246
- throw new Error(`Checksum mismatch for ${file.path}: expected ${file.sha256}, got ${actualSha}`);
1247
- }
1248
- const destination = join(temp, file.path);
1249
- mkdirSync(dirname(destination), { recursive: true });
1250
- writeFileSync(destination, content);
1251
- }
1252
-
1253
- writeFileSync(join(temp, ".installed.json"), `${JSON.stringify({
1254
- id: extension.id,
1255
- version: extension.version || "",
1256
- kind: extension.kind,
1257
- catalog_url: catalog.url,
1258
- installed_at: new Date().toISOString(),
1259
- manifest: publicExtension(extension),
1260
- files: plan.map(({ path, sha256: digest }) => ({ path, sha256: digest })),
1261
- }, null, 2)}\n`);
1262
-
1263
- rmSync(target, { recursive: true, force: true });
1264
- mkdirSync(dirname(target), { recursive: true });
1265
- renameSync(temp, target);
1266
- } catch (error) {
1267
- rmSync(temp, { recursive: true, force: true });
1268
- throw error;
1269
- }
1270
-
1271
- return { extension, target, plan };
1272
- }
1273
-
1274
- function extendDoctor() {
1275
- const id = positional(2);
1276
- const targets = installedExtensions().filter((extension) => !id || extension.id === id);
1277
- if (id && targets.length === 0) throw new Error(`Extension is not installed: ${id}`);
1278
-
1279
- const reports = targets.map(doctorInstalledExtension);
1280
- const ok = reports.every((report) => report.ok);
1281
-
1282
- if (hasFlag("--json")) {
1283
- printJson({ ok, extensions: reports });
1284
- } else if (reports.length === 0) {
1285
- console.log("No extensions installed.");
1286
- } else {
1287
- for (const report of reports) {
1288
- console.log(`${report.ok ? "ok" : "not ok"}\t${report.id}`);
1289
- for (const issue of report.issues) console.log(` - ${issue}`);
1290
- }
1291
- }
1292
-
1293
- process.exit(ok ? 0 : 1);
1294
- }
1295
-
1296
982
  function installedSkillRoot() {
1297
983
  return join(codexHome(), "skills", canonicalSkillDirectory);
1298
984
  }
1299
985
 
1300
- function installedPluginSkillRoot() {
986
+ function installedCodexPlugin() {
1301
987
  const root = join(codexHome(), "plugins", "cache", pluginName, pluginName);
1302
- if (!existsSync(root)) return "";
988
+ const configPath = join(codexHome(), "config.toml");
989
+ const base = {
990
+ installed: false,
991
+ enabled: pluginConfigEnabled(configPath),
992
+ name: `${pluginName}@${pluginName}`,
993
+ version: "",
994
+ cache_path: "",
995
+ manifest_path: "",
996
+ skill_installed: false,
997
+ skill_path: "",
998
+ config_path: configPath,
999
+ };
1000
+ if (!existsSync(root)) return base;
1303
1001
  const versions = readdirSync(root, { withFileTypes: true })
1304
1002
  .filter((entry) => entry.isDirectory())
1305
1003
  .map((entry) => entry.name)
@@ -1307,123 +1005,41 @@ function installedPluginSkillRoot() {
1307
1005
  .sort(compareVersions)
1308
1006
  .reverse();
1309
1007
  for (const version of versions) {
1310
- const skillPath = join(root, version, "skills", canonicalSkillDirectory);
1311
- if (existsSync(join(skillPath, "SKILL.md"))) return skillPath;
1312
- }
1313
- return "";
1314
- }
1315
-
1316
- function activeSkillRoot() {
1317
- if (existsSync(join(installedSkillRoot(), "SKILL.md"))) return installedSkillRoot();
1318
- const pluginSkillRoot = installedPluginSkillRoot();
1319
- if (pluginSkillRoot) return pluginSkillRoot;
1320
- return installedSkillRoot();
1321
- }
1322
-
1323
- function legacyInstalledSkillRoot() {
1324
- return join(codexHome(), "skills", legacySkillName);
1325
- }
1326
-
1327
- function extendRoot() {
1328
- return join(activeSkillRoot(), "extend");
1329
- }
1330
-
1331
- function extensionTarget(id) {
1332
- return join(extendRoot(), id);
1333
- }
1334
-
1335
- function catalogUrl() {
1336
- return optionValue("--catalog-url")
1337
- || optionValue("--catalog")
1338
- || process.env.GOALBUDDY_EXTEND_CATALOG_URL
1339
- || process.env.GOAL_MAKER_EXTEND_CATALOG_URL
1340
- || defaultCatalogUrl;
1341
- }
1342
-
1343
- async function loadCatalog() {
1344
- const url = catalogUrl();
1345
- const text = await readResource(url);
1346
- const catalog = JSON.parse(text);
1347
- if (!Array.isArray(catalog.extensions)) {
1348
- throw new Error("Extension catalog must contain an extensions array.");
1349
- }
1350
- return { ...catalog, url };
1351
- }
1352
-
1353
- function printExtensionNotFound(id, extensions) {
1354
- console.error(`Extension not found: ${id}`);
1355
- if (extensions.length) {
1356
- console.error("");
1357
- console.error("Available extensions:");
1358
- for (const extension of extensions) {
1359
- console.error(` ${extension.id}`);
1008
+ const cachePath = join(root, version);
1009
+ const skillPath = join(cachePath, "skills", canonicalSkillDirectory);
1010
+ const manifestPath = join(cachePath, ".codex-plugin", "plugin.json");
1011
+ if (existsSync(join(skillPath, "SKILL.md"))) {
1012
+ return {
1013
+ ...base,
1014
+ installed: true,
1015
+ version,
1016
+ cache_path: cachePath,
1017
+ manifest_path: manifestPath,
1018
+ skill_installed: true,
1019
+ skill_path: skillPath,
1020
+ };
1360
1021
  }
1361
1022
  }
1362
- console.error("");
1363
- console.error("Try:");
1364
- console.error(` npx ${canonicalCliName} extend`);
1365
- }
1366
-
1367
- function validateCatalogExtension(extension) {
1368
- if (!extension.id || !/^[a-z0-9][a-z0-9-]*$/.test(extension.id)) {
1369
- throw new Error(`Invalid extension id: ${extension.id || "<missing>"}`);
1370
- }
1371
- if (!extension.kind) throw new Error(`Extension ${extension.id} missing kind.`);
1372
- if (!Array.isArray(extension.files) || extension.files.length === 0) {
1373
- throw new Error(`Extension ${extension.id} must list files.`);
1374
- }
1375
- for (const file of extension.files) {
1376
- validateCatalogFile(extension, file);
1377
- }
1023
+ return base;
1378
1024
  }
1379
1025
 
1380
- function validateCatalogFile(extension, file) {
1381
- if (!file.path) throw new Error(`Extension ${extension.id} has a file without path.`);
1382
- if (!file.url) throw new Error(`Extension ${extension.id} file ${file.path} missing url.`);
1383
- if (!/^[a-f0-9]{64}$/i.test(file.sha256 || "")) {
1384
- throw new Error(`Extension ${extension.id} file ${file.path} must include sha256.`);
1385
- }
1386
- safeRelativePath(file.path);
1387
- }
1388
-
1389
- function installPlan(catalog, extension, target) {
1390
- return extension.files.map((file) => ({
1391
- path: safeRelativePath(file.path),
1392
- url: resolveResourceUrl(catalog.url, file.url),
1393
- sha256: file.sha256.toLowerCase(),
1394
- target: join(target, safeRelativePath(file.path)),
1395
- }));
1396
- }
1397
-
1398
- function safeRelativePath(path) {
1399
- const normalized = normalize(path).replaceAll("\\", "/");
1400
- if (!normalized || normalized.startsWith("../") || normalized === ".." || normalized.startsWith("/") || /^[A-Za-z]:/.test(normalized)) {
1401
- throw new Error(`Unsafe extension file path: ${path}`);
1026
+ function pluginConfigEnabled(configPath) {
1027
+ if (!existsSync(configPath)) return false;
1028
+ const lines = readFileSync(configPath, "utf8").split(/\r?\n/);
1029
+ const header = `[plugins."${pluginName}@${pluginName}"]`;
1030
+ const start = lines.findIndex((line) => line.trim() === header);
1031
+ if (start === -1) return false;
1032
+ for (let index = start + 1; index < lines.length; index += 1) {
1033
+ const line = lines[index].trim();
1034
+ if (line.startsWith("[")) break;
1035
+ if (/^enabled\s*=\s*true\b/.test(line)) return true;
1036
+ if (/^enabled\s*=/.test(line)) return false;
1402
1037
  }
1403
- return normalized;
1038
+ return false;
1404
1039
  }
1405
1040
 
1406
- function resolveResourceUrl(base, value) {
1407
- if (/^https?:\/\//.test(value) || value.startsWith("file://") || value.startsWith("/")) return value;
1408
- if (/^https?:\/\//.test(base) || base.startsWith("file://")) {
1409
- return new URL(value, base).href;
1410
- }
1411
- return resolve(dirname(resolve(base)), value);
1412
- }
1413
-
1414
- async function readResource(location) {
1415
- if (/^https?:\/\//.test(location)) {
1416
- if (!globalThis.fetch) throw new Error("This Node runtime does not provide fetch.");
1417
- const response = await globalThis.fetch(location, {
1418
- headers: {
1419
- "accept-encoding": "identity",
1420
- },
1421
- });
1422
- if (!response.ok) throw new Error(`Failed to fetch ${location}: HTTP ${response.status}`);
1423
- return Buffer.from(await response.arrayBuffer());
1424
- }
1425
- const path = location.startsWith("file://") ? fileURLToPath(location) : resolve(location);
1426
- return readFileSync(path);
1041
+ function legacyInstalledSkillRoot() {
1042
+ return join(codexHome(), "skills", legacySkillName);
1427
1043
  }
1428
1044
 
1429
1045
  function sha256(content) {
@@ -1458,50 +1074,8 @@ function listFiles(root, { exclude = new Set(), prefix = "" } = {}) {
1458
1074
  return files;
1459
1075
  }
1460
1076
 
1461
- function preserveInstalledExtensions(targets, { tempRoot = "" } = {}) {
1462
- const ids = [];
1463
- const firstTarget = targets.find(Boolean) || codexHome();
1464
- const tempPath = join(tempRoot || dirname(dirname(firstTarget)), `.goalbuddy-preserved-extend-${process.pid}-${Date.now()}`);
1465
- let hasExtensions = false;
1466
- for (const target of targets) {
1467
- if (!target) continue;
1468
- const source = join(target, "extend");
1469
- if (!existsSync(source)) continue;
1470
- for (const entry of readdirSync(source, { withFileTypes: true })) {
1471
- if (bundledCoreExtensionIds.has(entry.name)) continue;
1472
- mkdirSync(tempPath, { recursive: true });
1473
- const from = join(source, entry.name);
1474
- const to = join(tempPath, entry.name);
1475
- cpSync(from, to, { recursive: true, force: true });
1476
- if (entry.isDirectory()) ids.push(entry.name);
1477
- hasExtensions = true;
1478
- }
1479
- rmSync(source, { recursive: true, force: true });
1480
- }
1481
- return { tempPath: hasExtensions ? tempPath : "", ids: uniqueSorted(ids) };
1482
- }
1483
-
1484
- function restoreInstalledExtensions(target, tempPath) {
1485
- if (!tempPath) return;
1486
- const destinationRoot = join(target, "extend");
1487
- mkdirSync(destinationRoot, { recursive: true });
1488
- for (const entry of readdirSync(tempPath, { withFileTypes: true })) {
1489
- cpSync(join(tempPath, entry.name), join(destinationRoot, entry.name), { recursive: true, force: true });
1490
- }
1491
- }
1492
-
1493
- function cleanupPreservedExtensions(paths) {
1494
- for (const path of uniqueSorted(paths.filter(Boolean))) {
1495
- rmSync(path, { recursive: true, force: true });
1496
- }
1497
- }
1498
-
1499
- function uniqueSorted(values) {
1500
- return [...new Set(values)].sort();
1501
- }
1502
-
1503
1077
  function installFingerprintExcludes() {
1504
- return new Set(["extend", ".goalbuddy-install.json", ".goal-maker-install.json"]);
1078
+ return new Set([".goalbuddy-install.json", ".goal-maker-install.json"]);
1505
1079
  }
1506
1080
 
1507
1081
  function installMetadataPath(target) {
@@ -1533,53 +1107,6 @@ function writeInstallMetadata(target, previousMetadata) {
1533
1107
  }, null, 2)}\n`);
1534
1108
  }
1535
1109
 
1536
- async function extensionDiscoverySummary() {
1537
- try {
1538
- const catalog = await loadCatalog();
1539
- const extensions = catalog.extensions.map(extensionWithLocalState);
1540
- return {
1541
- catalog_url: catalog.url,
1542
- catalog_version: catalog.version || null,
1543
- available_count: extensions.length,
1544
- installed_count: extensions.filter((extension) => extension.state.installed).length,
1545
- available: extensions.map((extension) => ({
1546
- id: extension.id,
1547
- name: extension.name,
1548
- kind: extension.kind,
1549
- version: extension.version,
1550
- summary: extension.summary,
1551
- activation: extension.activation,
1552
- safe_by_default: extension.safe_by_default,
1553
- installed: extension.state.installed,
1554
- configured: extension.state.configured,
1555
- use_when: extension.use_when,
1556
- next_command: `${canonicalCliName} extend ${extension.id}`,
1557
- })),
1558
- recommended: extensions
1559
- .filter((extension) => extension.safe_by_default && !extension.state.installed)
1560
- .map((extension) => ({
1561
- id: extension.id,
1562
- name: extension.name,
1563
- kind: extension.kind,
1564
- activation: extension.activation,
1565
- summary: extension.summary,
1566
- use_when: extension.use_when.slice(0, 1),
1567
- next_command: `${canonicalCliName} extend ${extension.id}`,
1568
- })),
1569
- };
1570
- } catch (error) {
1571
- return {
1572
- catalog_url: catalogUrl(),
1573
- catalog_version: null,
1574
- available_count: 0,
1575
- installed_count: 0,
1576
- available: [],
1577
- recommended: [],
1578
- error: error.message,
1579
- };
1580
- }
1581
- }
1582
-
1583
1110
  function printInstallReport(report) {
1584
1111
  const verb = report.command === "update" ? "Updated" : "Installed";
1585
1112
  const previous = report.package.previous_version && report.package.previous_version !== report.package.current_version
@@ -1592,31 +1119,11 @@ function printInstallReport(report) {
1592
1119
  console.log(`Compatibility skill: ${report.skill.compatibility_path}`);
1593
1120
  const agentSummary = summarizeStatuses(report.agents);
1594
1121
  console.log(`Agents: ${agentSummary}`);
1595
- if (report.skill.preserved_extensions.length) {
1596
- console.log(`Preserved extensions: ${report.skill.preserved_extensions.join(", ")}`);
1597
- }
1598
-
1599
- if (report.extensions.error) {
1600
- console.log("");
1601
- console.log(`Extensions: unavailable (${report.extensions.error})`);
1602
- } else {
1603
- console.log("");
1604
- console.log(`Extensions: ${report.extensions.available_count} available from ${report.extensions.catalog_url}`);
1605
- if (report.extensions.recommended.length) {
1606
- console.log("");
1607
- console.log("Recommended:");
1608
- for (const extension of report.extensions.recommended.slice(0, 3)) {
1609
- console.log(` ${extension.name || extension.id}`);
1610
- if (extension.summary) console.log(` ${extension.summary}`);
1611
- console.log(` Details: npx ${extension.next_command}`);
1612
- }
1613
- }
1614
- }
1615
1122
 
1616
1123
  console.log("");
1617
1124
  console.log("Next:");
1618
1125
  console.log(` $${canonicalSkillName}`);
1619
- console.log(` ${canonicalCliName} extend`);
1126
+ console.log(` ${canonicalCliName} board docs/goals/<slug>`);
1620
1127
  console.log(` ${legacyCliName} remains a temporary compatibility alias.`);
1621
1128
  }
1622
1129
 
@@ -1630,9 +1137,6 @@ function printEverywhereInstallReport(report) {
1630
1137
  console.log(`Codex: not completed (${report.codex.error})`);
1631
1138
  } else if (report.codex) {
1632
1139
  console.log(`Codex: plugin ${report.codex.version} enabled at ${report.codex.cache_path}`);
1633
- if (report.codex.preserved_extensions?.length) {
1634
- console.log(`Codex preserved extensions: ${report.codex.preserved_extensions.join(", ")}`);
1635
- }
1636
1140
  }
1637
1141
 
1638
1142
  if (report.claude?.ok === false) {
@@ -1667,12 +1171,6 @@ function summarizeStatuses(items) {
1667
1171
  .join(", ");
1668
1172
  }
1669
1173
 
1670
- function assertSkillInstalledForExtensionInstall() {
1671
- if (!existsSync(join(activeSkillRoot(), "SKILL.md"))) {
1672
- throw new Error(`${canonicalProductName} skill is not installed. Run: npx ${canonicalCliName}`);
1673
- }
1674
- }
1675
-
1676
1174
  function latestPublishedVersion() {
1677
1175
  if (process.env.GOALBUDDY_TEST_NPM_LATEST_VERSION) {
1678
1176
  return normalizeVersion(process.env.GOALBUDDY_TEST_NPM_LATEST_VERSION);
@@ -1718,106 +1216,6 @@ function compareVersions(left, right) {
1718
1216
  return left.localeCompare(right);
1719
1217
  }
1720
1218
 
1721
- function installedExtensions() {
1722
- const root = extendRoot();
1723
- if (!existsSync(root)) return [];
1724
- return readdirSync(root, { withFileTypes: true })
1725
- .filter((entry) => entry.isDirectory())
1726
- .map((entry) => readInstalledExtension(join(root, entry.name)))
1727
- .filter(Boolean);
1728
- }
1729
-
1730
- function readInstalledExtension(path) {
1731
- const installedPath = join(path, ".installed.json");
1732
- if (!existsSync(installedPath)) {
1733
- return { id: basename(path), path, issues: ["missing .installed.json"] };
1734
- }
1735
- const data = JSON.parse(readFileSync(installedPath, "utf8"));
1736
- return {
1737
- ...data,
1738
- path,
1739
- };
1740
- }
1741
-
1742
- function doctorInstalledExtension(extension) {
1743
- const issues = [];
1744
- for (const file of extension.files || []) {
1745
- const path = join(extension.path, safeRelativePath(file.path));
1746
- if (!existsSync(path)) {
1747
- issues.push(`missing file: ${file.path}`);
1748
- continue;
1749
- }
1750
- const actualSha = sha256(readFileSync(path));
1751
- if (actualSha !== file.sha256) {
1752
- issues.push(`checksum mismatch: ${file.path}`);
1753
- }
1754
- }
1755
- for (const envName of extension.manifest?.auth?.env || []) {
1756
- if (!process.env[envName]) issues.push(`missing env: ${envName}`);
1757
- }
1758
-
1759
- return {
1760
- id: extension.id,
1761
- version: extension.version || "",
1762
- kind: extension.kind || "",
1763
- path: extension.path,
1764
- ok: issues.length === 0,
1765
- issues,
1766
- };
1767
- }
1768
-
1769
- function publicExtension(extension) {
1770
- return {
1771
- id: extension.id,
1772
- name: extension.name || "",
1773
- kind: extension.kind || "",
1774
- version: extension.version || "",
1775
- summary: extension.summary || "",
1776
- description: extension.description || "",
1777
- local_use_prompt: extension.local_use_prompt || "",
1778
- source: extension.source || "",
1779
- docs: extension.docs || "",
1780
- use_when: extension.use_when || [],
1781
- activation: extension.activation || "",
1782
- outputs: extension.outputs || [],
1783
- requires_approval: extension.requires_approval || false,
1784
- safe_by_default: extension.safe_by_default || false,
1785
- applies_to: extension.applies_to || {},
1786
- reads: extension.reads || [],
1787
- writes: extension.writes || [],
1788
- side_effects: extension.side_effects || [],
1789
- agent_instructions: extension.agent_instructions || [],
1790
- auth: extension.auth || { env: [] },
1791
- supports: extension.supports || {},
1792
- source_of_truth: extension.source_of_truth || "local",
1793
- files: (extension.files || []).map((file) => ({
1794
- path: file.path,
1795
- sha256: file.sha256,
1796
- })),
1797
- };
1798
- }
1799
-
1800
- function extensionWithLocalState(extension) {
1801
- return {
1802
- ...publicExtension(extension),
1803
- state: {
1804
- available: true,
1805
- installed: existsSync(extensionTarget(extension.id)),
1806
- enabled: false,
1807
- configured: configuredFor(extension),
1808
- missing_env: missingEnv(extension),
1809
- },
1810
- };
1811
- }
1812
-
1813
- function configuredFor(extension) {
1814
- return missingEnv(extension).length === 0;
1815
- }
1816
-
1817
- function missingEnv(extension) {
1818
- return (extension.auth?.env || []).filter((envName) => !process.env[envName]);
1819
- }
1820
-
1821
1219
  function printJson(value) {
1822
1220
  console.log(JSON.stringify(value, null, 2));
1823
1221
  }