goalbuddy 0.3.6 → 0.3.8

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 (81) hide show
  1. package/CHANGELOG.md +61 -0
  2. package/CONTRIBUTING.md +2 -2
  3. package/README.md +27 -10
  4. package/{RELEASE-0.3.5.md → docs/releases/0.3.5.md} +4 -4
  5. package/docs/releases/0.3.7.md +129 -0
  6. package/docs/releases/0.3.8.md +40 -0
  7. package/docs/releases/README.md +83 -0
  8. package/goalbuddy/SKILL.md +21 -10
  9. package/goalbuddy/scripts/check-goal-state.mjs +53 -0
  10. package/goalbuddy/scripts/render-task-prompt.mjs +39 -4
  11. package/{plugins/goalbuddy/skills/goalbuddy/extend → goalbuddy/surfaces}/local-goal-board/README.md +7 -9
  12. package/goalbuddy/{extend → surfaces}/local-goal-board/examples/sample-goal/state.yaml +5 -5
  13. package/{plugins/goalbuddy/skills/goalbuddy/extend → goalbuddy/surfaces}/local-goal-board/examples/subgoal-parent/state.yaml +3 -3
  14. package/goalbuddy/{extend → surfaces}/local-goal-board/examples/subgoal-parent/subgoals/T004-board-view/state.yaml +3 -3
  15. package/{plugins/goalbuddy/skills/goalbuddy/extend → goalbuddy/surfaces}/local-goal-board/scripts/lib/goal-board.mjs +17 -13
  16. package/goalbuddy/{extend → surfaces}/local-goal-board/scripts/local-goal-board.mjs +27 -6
  17. package/{plugins/goalbuddy/skills/goalbuddy/extend → goalbuddy/surfaces}/local-goal-board/test/local-goal-board.test.mjs +63 -12
  18. package/goalbuddy/templates/goal.md +9 -0
  19. package/goalbuddy/templates/state.yaml +7 -6
  20. package/internal/assets/goalbuddy-v0.3.7-release.png +0 -0
  21. package/internal/cli/goal-maker.mjs +177 -717
  22. package/package.json +7 -8
  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/skills/goalbuddy/SKILL.md +21 -10
  27. package/plugins/goalbuddy/skills/goalbuddy/scripts/check-goal-state.mjs +53 -0
  28. package/plugins/goalbuddy/skills/goalbuddy/scripts/render-task-prompt.mjs +39 -4
  29. package/{goalbuddy/extend → plugins/goalbuddy/skills/goalbuddy/surfaces}/local-goal-board/README.md +7 -9
  30. package/plugins/goalbuddy/skills/goalbuddy/{extend → surfaces}/local-goal-board/examples/sample-goal/state.yaml +5 -5
  31. package/{goalbuddy/extend → plugins/goalbuddy/skills/goalbuddy/surfaces}/local-goal-board/examples/subgoal-parent/state.yaml +3 -3
  32. package/plugins/goalbuddy/skills/goalbuddy/{extend → surfaces}/local-goal-board/examples/subgoal-parent/subgoals/T004-board-view/state.yaml +3 -3
  33. package/{goalbuddy/extend → plugins/goalbuddy/skills/goalbuddy/surfaces}/local-goal-board/scripts/lib/goal-board.mjs +2 -2
  34. package/plugins/goalbuddy/skills/goalbuddy/{extend → surfaces}/local-goal-board/scripts/local-goal-board.mjs +27 -6
  35. package/{goalbuddy/extend → plugins/goalbuddy/skills/goalbuddy/surfaces}/local-goal-board/test/local-goal-board.test.mjs +35 -8
  36. package/plugins/goalbuddy/skills/goalbuddy/templates/goal.md +9 -0
  37. package/plugins/goalbuddy/skills/goalbuddy/templates/state.yaml +7 -6
  38. package/examples/extend-catalog-workflow/goal.md +0 -53
  39. package/examples/extend-catalog-workflow/notes/T001-extension-model-map.md +0 -47
  40. package/examples/extend-catalog-workflow/notes/T002-architecture-decision.md +0 -48
  41. package/examples/extend-catalog-workflow/notes/T003-implementation-summary.md +0 -43
  42. package/examples/extend-catalog-workflow/notes/T004-root-extend-folder.md +0 -24
  43. package/examples/extend-catalog-workflow/notes/T005-layout-cleanup.md +0 -46
  44. package/examples/extend-catalog-workflow/notes/T006-catalog-location.md +0 -50
  45. package/examples/extend-catalog-workflow/notes/T999-completion-audit.md +0 -36
  46. package/examples/extend-catalog-workflow/state.yaml +0 -327
  47. package/examples/github-pr-workflow-extension/pr-handoff.md +0 -46
  48. package/examples/improve-goal-maker/goal.md +0 -51
  49. package/examples/improve-goal-maker/notes/T001-repo-map.md +0 -59
  50. package/examples/improve-goal-maker/notes/T002-risk-map.md +0 -37
  51. package/examples/improve-goal-maker/state.yaml +0 -224
  52. package/goalbuddy/extend/github-projects/README.md +0 -105
  53. package/goalbuddy/extend/github-projects/examples/goal-board-sync/state.yaml +0 -63
  54. package/goalbuddy/extend/github-projects/extension.yaml +0 -43
  55. package/goalbuddy/extend/github-projects/scripts/lib/github-projects.mjs +0 -728
  56. package/goalbuddy/extend/github-projects/scripts/lib/goal-state.mjs +0 -362
  57. package/goalbuddy/extend/github-projects/scripts/sync-github-project.mjs +0 -193
  58. package/goalbuddy/extend/github-projects/test/github-projects.test.mjs +0 -267
  59. package/goalbuddy/extend/local-goal-board/extension.yaml +0 -39
  60. package/internal/assets/extend-release.png +0 -0
  61. package/internal/assets/extend-release.svg +0 -83
  62. package/plugins/goalbuddy/skills/goalbuddy/extend/github-projects/README.md +0 -105
  63. package/plugins/goalbuddy/skills/goalbuddy/extend/github-projects/examples/goal-board-sync/state.yaml +0 -63
  64. package/plugins/goalbuddy/skills/goalbuddy/extend/github-projects/extension.yaml +0 -43
  65. package/plugins/goalbuddy/skills/goalbuddy/extend/github-projects/scripts/lib/github-projects.mjs +0 -728
  66. package/plugins/goalbuddy/skills/goalbuddy/extend/github-projects/scripts/lib/goal-state.mjs +0 -362
  67. package/plugins/goalbuddy/skills/goalbuddy/extend/github-projects/scripts/sync-github-project.mjs +0 -193
  68. package/plugins/goalbuddy/skills/goalbuddy/extend/github-projects/test/github-projects.test.mjs +0 -267
  69. package/plugins/goalbuddy/skills/goalbuddy/extend/local-goal-board/extension.yaml +0 -39
  70. /package/goalbuddy/{extend → surfaces}/local-goal-board/assets/goalbuddy-mark.png +0 -0
  71. /package/goalbuddy/{extend → surfaces}/local-goal-board/examples/sample-goal/notes/T001-scout.md +0 -0
  72. /package/goalbuddy/{extend → surfaces}/local-goal-board/examples/subgoal-parent/goal.md +0 -0
  73. /package/goalbuddy/{extend → surfaces}/local-goal-board/examples/subgoal-parent/notes/.gitkeep +0 -0
  74. /package/goalbuddy/{extend → surfaces}/local-goal-board/examples/subgoal-parent/subgoals/T004-board-view/goal.md +0 -0
  75. /package/goalbuddy/{extend → surfaces}/local-goal-board/examples/subgoal-parent/subgoals/T004-board-view/notes/.gitkeep +0 -0
  76. /package/plugins/goalbuddy/skills/goalbuddy/{extend → surfaces}/local-goal-board/assets/goalbuddy-mark.png +0 -0
  77. /package/plugins/goalbuddy/skills/goalbuddy/{extend → surfaces}/local-goal-board/examples/sample-goal/notes/T001-scout.md +0 -0
  78. /package/plugins/goalbuddy/skills/goalbuddy/{extend → surfaces}/local-goal-board/examples/subgoal-parent/goal.md +0 -0
  79. /package/plugins/goalbuddy/skills/goalbuddy/{extend → surfaces}/local-goal-board/examples/subgoal-parent/notes/.gitkeep +0 -0
  80. /package/plugins/goalbuddy/skills/goalbuddy/{extend → surfaces}/local-goal-board/examples/subgoal-parent/subgoals/T004-board-view/goal.md +0 -0
  81. /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,21 +38,18 @@ 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",
55
49
  "--task",
56
50
  "--board",
57
51
  ]);
52
+ const pathOptions = new Set(["--board", "--goal"]);
58
53
 
59
54
  const args = process.argv.slice(2);
60
55
  const command = args[0] === "--help" || args[0] === "-h"
@@ -117,6 +112,17 @@ async function main() {
117
112
  doctorClaude();
118
113
  }
119
114
  break;
115
+ case "reset":
116
+ if (wantsHelp()) {
117
+ usage();
118
+ break;
119
+ }
120
+ if (targetMode() !== "codex") {
121
+ console.error("Reset currently supports --target codex only.");
122
+ process.exit(2);
123
+ }
124
+ resetCodex();
125
+ break;
120
126
  case "check-update":
121
127
  case "update-check":
122
128
  checkUpdate();
@@ -128,9 +134,6 @@ async function main() {
128
134
  }
129
135
  plugin();
130
136
  break;
131
- case "extend":
132
- await extend();
133
- break;
134
137
  case "board":
135
138
  await board();
136
139
  break;
@@ -202,6 +205,35 @@ function positionalArgs() {
202
205
  return values;
203
206
  }
204
207
 
208
+ /**
209
+ * Resolve goal-related paths in raw args to absolute paths.
210
+ * Child processes spawned with cwd=packageRoot cannot resolve
211
+ * relative goal paths from the user's working directory.
212
+ */
213
+ function resolveChildGoalArgs(rawArgs) {
214
+ const out = [];
215
+ for (let index = 0; index < rawArgs.length; index += 1) {
216
+ const arg = rawArgs[index];
217
+ const joinedMatch = [...pathOptions].find((opt) => arg.startsWith(opt + "="));
218
+ if (joinedMatch) {
219
+ const value = arg.slice(joinedMatch.length + 1);
220
+ out.push(`${joinedMatch}=${value ? resolve(value) : value}`);
221
+ } else if (pathOptions.has(arg)) {
222
+ out.push(arg);
223
+ const value = rawArgs[++index] || "";
224
+ out.push(value ? resolve(value) : value);
225
+ } else if (optionsWithValues.has(arg)) {
226
+ out.push(arg);
227
+ out.push(rawArgs[++index] || "");
228
+ } else if (!arg.startsWith("-")) {
229
+ out.push(resolve(arg));
230
+ } else {
231
+ out.push(arg);
232
+ }
233
+ }
234
+ return out;
235
+ }
236
+
205
237
  function usage() {
206
238
  console.log(`${canonicalProductName} for Claude Code and Codex
207
239
 
@@ -212,13 +244,9 @@ Usage:
212
244
  ${canonicalCliName} update [--target claude|codex] [--claude-home <path>] [--codex-home <path>] [--json]
213
245
  ${canonicalCliName} agents [--target claude|codex] [--claude-home <path>] [--codex-home <path>] [--force]
214
246
  ${canonicalCliName} doctor [--target claude|codex] [--claude-home <path>] [--codex-home <path>] [--goal-ready]
247
+ ${canonicalCliName} reset --target codex [--codex-home <path>] [--json]
215
248
  ${canonicalCliName} check-update [--json]
216
- ${canonicalCliName} extend [--catalog-url <url-or-path>] [--kind <kind>] [--json]
217
- ${canonicalCliName} extend <id> [--catalog-url <url-or-path>] [--json]
218
- ${canonicalCliName} extend install <id> [--catalog-url <url-or-path>] [--dry-run] [--force] [--json]
219
- ${canonicalCliName} extend install --all [--catalog-url <url-or-path>] [--dry-run] [--force] [--json]
220
- ${canonicalCliName} extend doctor [<id>] [--codex-home <path>] [--json]
221
- ${canonicalCliName} board <docs/goals/slug> [--catalog-url <url-or-path>] [--host <host>] [--port <port>] [--once] [--json]
249
+ ${canonicalCliName} board <docs/goals/slug> [--host <host>] [--port <port>] [--once] [--json]
222
250
  ${canonicalCliName} prompt <docs/goals/slug> [--task T###] [--board <path/to/state.yaml>] [--json]
223
251
  ${canonicalCliName} parallel-plan <docs/goals/slug> [--json]
224
252
 
@@ -235,8 +263,6 @@ Compatibility:
235
263
  Environment:
236
264
  CODEX_HOME Overrides the default ~/.codex target.
237
265
  CLAUDE_HOME Overrides the default ~/.claude target (and selects Claude Code unless --target codex is set).
238
- GOALBUDDY_EXTEND_CATALOG_URL Overrides the default GitHub-hosted extension catalog.
239
- GOAL_MAKER_EXTEND_CATALOG_URL Legacy fallback for the extension catalog.
240
266
  `);
241
267
  }
242
268
 
@@ -289,16 +315,11 @@ function installClaudeSkill({ quiet = false } = {}) {
289
315
 
290
316
  const previousMetadata = readInstallMetadata(target);
291
317
  const previousFingerprint = existsSync(target) ? directoryFingerprint(target, { exclude: installFingerprintExcludes() }) : "";
292
- const preservedExtensions = preserveInstalledExtensions([target], { tempRoot: claudeHome() });
293
- const extensionTempPath = preservedExtensions.tempPath;
294
- const preservedExtensionIds = preservedExtensions.ids;
295
318
 
296
319
  mkdirSync(dirname(target), { recursive: true });
297
320
  rmSync(target, { recursive: true, force: true });
298
321
  cpSync(skillSource, target, { recursive: true });
299
- restoreInstalledExtensions(target, extensionTempPath);
300
322
  writeInstallMetadata(target, previousMetadata);
301
- cleanupPreservedExtensions([extensionTempPath]);
302
323
 
303
324
  const currentFingerprint = directoryFingerprint(target, { exclude: installFingerprintExcludes() });
304
325
  const status = previousFingerprint
@@ -311,7 +332,6 @@ function installClaudeSkill({ quiet = false } = {}) {
311
332
  path: target,
312
333
  previous_version: previousMetadata?.package_version || "",
313
334
  current_version: packageInfo.version,
314
- preserved_extensions: preservedExtensionIds,
315
335
  };
316
336
  }
317
337
 
@@ -362,7 +382,6 @@ async function buildClaudeInstallReport() {
362
382
  skill: installClaudeSkill({ quiet }),
363
383
  agents: installClaudeAgents({ quiet }),
364
384
  legacy_commands_cleanup: cleanupLegacyClaudeCommands({ quiet }),
365
- extensions: await extensionDiscoverySummary(),
366
385
  warnings: [],
367
386
  };
368
387
 
@@ -463,27 +482,6 @@ function printClaudeInstallReport(report) {
463
482
  if (report.legacy_commands_cleanup?.removed) {
464
483
  console.log(`Removed legacy command: ${report.legacy_commands_cleanup.path}`);
465
484
  }
466
- if (report.skill.preserved_extensions.length) {
467
- console.log(`Preserved extensions: ${report.skill.preserved_extensions.join(", ")}`);
468
- }
469
-
470
- if (report.extensions?.error) {
471
- console.log("");
472
- console.log(`Extensions: unavailable (${report.extensions.error})`);
473
- } else if (report.extensions) {
474
- console.log("");
475
- console.log(`Extensions: ${report.extensions.available_count} available from ${report.extensions.catalog_url}`);
476
- if (report.extensions.recommended?.length) {
477
- console.log("");
478
- console.log("Recommended:");
479
- for (const extension of report.extensions.recommended.slice(0, 3)) {
480
- console.log(` ${extension.name || extension.id}`);
481
- if (extension.summary) console.log(` ${extension.summary}`);
482
- console.log(` Details: npx ${extension.next_command}`);
483
- }
484
- }
485
- }
486
-
487
485
  console.log("");
488
486
  console.log("Next:");
489
487
  console.log(` Restart Claude Code, then run: /goal-prep`);
@@ -503,9 +501,6 @@ function installSkill({ force = true, quiet = false } = {}) {
503
501
 
504
502
  const previousMetadata = readInstallMetadata(target) || readInstallMetadata(legacyTarget);
505
503
  const previousFingerprint = existsSync(target) ? directoryFingerprint(target, { exclude: installFingerprintExcludes() }) : "";
506
- const preservedExtensions = preserveInstalledExtensions([target, legacyTarget], { tempRoot: codexHome() });
507
- const extensionTempPath = preservedExtensions.tempPath;
508
- const preservedExtensionIds = preservedExtensions.ids;
509
504
 
510
505
  mkdirSync(dirname(target), { recursive: true });
511
506
  if (existsSync(target)) {
@@ -520,16 +515,13 @@ function installSkill({ force = true, quiet = false } = {}) {
520
515
  cpSync(skillSource, target, {
521
516
  recursive: true,
522
517
  });
523
- restoreInstalledExtensions(target, extensionTempPath);
524
518
  writeInstallMetadata(target, previousMetadata);
525
519
 
526
520
  mkdirSync(dirname(legacyTarget), { recursive: true });
527
521
  rmSync(legacyTarget, { recursive: true, force: true });
528
522
  mkdirSync(legacyTarget, { recursive: true });
529
523
  writeFileSync(join(legacyTarget, "SKILL.md"), compatibilitySkillBody());
530
- restoreInstalledExtensions(legacyTarget, extensionTempPath);
531
524
  writeInstallMetadata(legacyTarget, previousMetadata);
532
- cleanupPreservedExtensions([extensionTempPath]);
533
525
 
534
526
  const currentFingerprint = directoryFingerprint(target, { exclude: installFingerprintExcludes() });
535
527
  const status = previousFingerprint
@@ -543,7 +535,6 @@ function installSkill({ force = true, quiet = false } = {}) {
543
535
  compatibility_path: legacyTarget,
544
536
  previous_version: previousMetadata?.package_version || "",
545
537
  current_version: packageInfo.version,
546
- preserved_extensions: preservedExtensionIds,
547
538
  };
548
539
  }
549
540
 
@@ -605,7 +596,6 @@ async function installAll() {
605
596
  codex_home: codexHome(),
606
597
  skill: installSkill({ force: true, quiet }),
607
598
  agents: installAgents({ quiet }),
608
- extensions: await extensionDiscoverySummary(),
609
599
  warnings: [],
610
600
  };
611
601
 
@@ -628,20 +618,36 @@ function doctor() {
628
618
  const agents = existsSync(agentsPath)
629
619
  ? readdirSync(agentsPath).filter((file) => file.startsWith("goal_") && file.endsWith(".toml"))
630
620
  : [];
631
- const missingAgents = requiredAgentFiles.filter((file) => !agents.includes(file));
621
+ const installSurfacePresent = plugin.skill_installed || installed || legacyInstalled;
622
+ const residualAgents = installSurfacePresent ? [] : agents.filter((file) => requiredAgentFiles.includes(file));
623
+ const missingAgents = installSurfacePresent || residualAgents.length > 0
624
+ ? requiredAgentFiles.filter((file) => !agents.includes(file))
625
+ : [];
632
626
  const staleAgents = requiredAgentFiles.filter((file) => {
633
627
  const installedAgent = join(agentsPath, file);
634
628
  const bundledAgent = join(skillSource, "agents", file);
635
629
  if (!existsSync(installedAgent) || !existsSync(bundledAgent)) return false;
636
630
  return sha256(readFileSync(installedAgent)) !== sha256(readFileSync(bundledAgent));
637
631
  });
632
+ const runtimeState = codexInstallState({
633
+ plugin,
634
+ installed,
635
+ legacyInstalled,
636
+ residualAgents,
637
+ missingAgents,
638
+ staleAgents,
639
+ });
638
640
  const goalRuntime = codexGoalRuntimeStatus();
639
641
  const warnings = [];
640
642
  const errors = [];
641
643
  if (!goalRuntime.ready) {
642
644
  warnings.push("native Codex /goal runtime is not ready; run `codex login` and `codex features enable goals` before using /goal.");
643
645
  }
644
- if (!plugin.skill_installed && !installed) {
646
+ if (runtimeState === "fully-removed") {
647
+ errors.push("Codex GoalBuddy is fully removed; run `npx goalbuddy --target codex` to install.");
648
+ } else if (runtimeState === "residual-agents-only") {
649
+ errors.push(`Residual GoalBuddy Codex agents remain without plugin cache/config: ${residualAgents.join(", ")}; run a GoalBuddy reset/cleanup before treating it as removed.`);
650
+ } else if (!plugin.skill_installed && !installed) {
645
651
  errors.push("Codex GoalBuddy plugin is not installed; run `npx goalbuddy --target codex`.");
646
652
  }
647
653
  if (plugin.skill_installed && !plugin.enabled) {
@@ -673,7 +679,9 @@ function doctor() {
673
679
  skill_path: skillPath,
674
680
  compatibility_skill_installed: legacyInstalled,
675
681
  compatibility_skill_path: legacySkillPath,
682
+ runtime_state: runtimeState,
676
683
  installed_agents: agents,
684
+ residual_agents: residualAgents,
677
685
  missing_agents: missingAgents,
678
686
  stale_agents: staleAgents,
679
687
  goal_runtime: goalRuntime,
@@ -688,6 +696,20 @@ function doctor() {
688
696
  process.exit(installOk && goalReadyOk && errors.length === 0 ? 0 : 1);
689
697
  }
690
698
 
699
+ function codexInstallState({ plugin, installed, legacyInstalled, residualAgents, missingAgents, staleAgents }) {
700
+ if (residualAgents.length > 0 && !plugin.skill_installed && !installed && !legacyInstalled) {
701
+ return "residual-agents-only";
702
+ }
703
+ if (!plugin.skill_installed && !installed && !legacyInstalled) {
704
+ return "fully-removed";
705
+ }
706
+ if (staleAgents.length > 0) return "stale-agents";
707
+ if (missingAgents.length > 0) return "incomplete";
708
+ if (plugin.skill_installed && !plugin.enabled) return "disabled";
709
+ if ((plugin.skill_installed && plugin.enabled) || installed) return "installed";
710
+ return "incomplete";
711
+ }
712
+
691
713
  function checkUpdate() {
692
714
  const report = updateReport();
693
715
 
@@ -771,20 +793,14 @@ function installPlugin({ quiet = false } = {}) {
771
793
 
772
794
  const pluginManifest = JSON.parse(readFileSync(pluginManifestPath, "utf8"));
773
795
  const pluginCachePath = pluginCacheRoot(pluginManifest.version);
774
- const pluginSkillPath = join(pluginCachePath, "skills", canonicalSkillDirectory);
775
796
  const marketplace = runCodex(["plugin", "marketplace", "add", source]);
776
797
  if (!marketplace.ok) {
777
798
  throw new Error(`Failed to add Codex plugin marketplace: ${firstLine(marketplace.stderr || marketplace.stdout)}`);
778
799
  }
779
800
 
780
- const legacySkillPaths = legacyCodexSkillRoots();
781
- const existingPluginSkillPath = installedPluginSkillRoot();
782
- const preservedExtensions = preserveInstalledExtensions([existingPluginSkillPath, ...legacySkillPaths], { tempRoot: dirname(pluginCachePath) });
783
801
  mkdirSync(dirname(pluginCachePath), { recursive: true });
784
802
  rmSync(pluginCachePath, { recursive: true, force: true });
785
803
  cpSync(pluginSource, pluginCachePath, { recursive: true });
786
- restoreInstalledExtensions(pluginSkillPath, preservedExtensions.tempPath);
787
- cleanupPreservedExtensions([preservedExtensions.tempPath]);
788
804
  const removedLegacySkillPaths = cleanupLegacyCodexSkills();
789
805
  const configPath = enablePluginConfig();
790
806
  const agents = installAgents({ quiet: true });
@@ -799,7 +815,6 @@ function installPlugin({ quiet = false } = {}) {
799
815
  cache_path: pluginCachePath,
800
816
  config_path: configPath,
801
817
  agents,
802
- preserved_extensions: preservedExtensions.ids,
803
818
  removed_legacy_skill_paths: removedLegacySkillPaths,
804
819
  };
805
820
 
@@ -815,9 +830,6 @@ function installPlugin({ quiet = false } = {}) {
815
830
  console.log(`Cache: ${pluginCachePath}`);
816
831
  console.log(`Config: ${configPath}`);
817
832
  console.log(`Agents: ${summarizeStatuses(report.agents)}`);
818
- if (report.preserved_extensions.length) {
819
- console.log(`Preserved extensions: ${report.preserved_extensions.join(", ")}`);
820
- }
821
833
  if (report.removed_legacy_skill_paths.length) {
822
834
  console.log(`Removed legacy personal skills: ${report.removed_legacy_skill_paths.join(", ")}`);
823
835
  }
@@ -825,9 +837,8 @@ function installPlugin({ quiet = false } = {}) {
825
837
  console.log("Restart Codex, then use:");
826
838
  console.log(` $${canonicalSkillName}`);
827
839
  console.log("");
828
- console.log("Bundled visual boards:");
840
+ console.log("Goal surface:");
829
841
  console.log(` npx ${canonicalCliName} board docs/goals/<slug>`);
830
- console.log(` npx ${canonicalCliName} extend github-projects`);
831
842
  return report;
832
843
  }
833
844
 
@@ -845,8 +856,95 @@ function cleanupLegacyCodexSkills() {
845
856
  return removed;
846
857
  }
847
858
 
859
+ function resetCodex() {
860
+ const configPath = join(codexHome(), "config.toml");
861
+ const removedConfigSections = [];
862
+ if (existsSync(configPath)) {
863
+ const existing = readFileSync(configPath, "utf8");
864
+ let updated = existing;
865
+ for (const header of [`[plugins."${pluginName}@${pluginName}"]`, `[marketplaces.${pluginName}]`]) {
866
+ const next = removeTomlTable(updated, header);
867
+ if (next !== updated) {
868
+ removedConfigSections.push(header);
869
+ updated = next;
870
+ }
871
+ }
872
+ if (updated !== existing) writeFileSync(configPath, updated);
873
+ }
874
+
875
+ const removedPluginCachePaths = [];
876
+ const cacheRoot = pluginCacheOwnerRoot();
877
+ if (existsSync(cacheRoot)) {
878
+ rmSync(cacheRoot, { recursive: true, force: true });
879
+ removedPluginCachePaths.push(cacheRoot);
880
+ }
881
+
882
+ const removedAgents = [];
883
+ const agentsRoot = join(codexHome(), "agents");
884
+ for (const file of requiredAgentFiles) {
885
+ const path = join(agentsRoot, file);
886
+ if (!existsSync(path)) continue;
887
+ rmSync(path, { recursive: true, force: true });
888
+ removedAgents.push(path);
889
+ }
890
+
891
+ const removedLegacySkillPaths = cleanupLegacyCodexSkills();
892
+ const report = {
893
+ reset: true,
894
+ target: "codex",
895
+ codex_home: codexHome(),
896
+ config_path: configPath,
897
+ removed_config_sections: removedConfigSections,
898
+ removed_plugin_cache_paths: removedPluginCachePaths,
899
+ removed_agents: removedAgents,
900
+ removed_legacy_skill_paths: removedLegacySkillPaths,
901
+ };
902
+
903
+ if (hasFlag("--json")) {
904
+ printJson(report);
905
+ return report;
906
+ }
907
+
908
+ console.log(`Reset ${canonicalProductName} Codex-owned runtime files`);
909
+ console.log(`Config sections: ${removedConfigSections.length ? removedConfigSections.join(", ") : "none"}`);
910
+ console.log(`Plugin cache: ${removedPluginCachePaths.length ? removedPluginCachePaths.join(", ") : "none"}`);
911
+ console.log(`Agents: ${removedAgents.length ? removedAgents.join(", ") : "none"}`);
912
+ console.log(`Legacy personal skills: ${removedLegacySkillPaths.length ? removedLegacySkillPaths.join(", ") : "none"}`);
913
+ return report;
914
+ }
915
+
916
+ function removeTomlTable(text, header) {
917
+ const normalized = text.endsWith("\n") || text.length === 0 ? text : `${text}\n`;
918
+ const lines = normalized.split("\n");
919
+ const output = [];
920
+ let skipping = false;
921
+ let removed = false;
922
+ const descendantPrefix = `${header.slice(0, -1)}.`;
923
+
924
+ for (const line of lines) {
925
+ const trimmed = line.trim();
926
+ if (trimmed === header || trimmed.startsWith(descendantPrefix)) {
927
+ skipping = true;
928
+ removed = true;
929
+ continue;
930
+ }
931
+ if (skipping && /^\s*\[/.test(line)) {
932
+ skipping = trimmed.startsWith(descendantPrefix);
933
+ if (skipping) continue;
934
+ }
935
+ if (!skipping) output.push(line);
936
+ }
937
+
938
+ if (!removed) return text;
939
+ return output.join("\n").replace(/\n{3,}/g, "\n\n").replace(/\n*$/, "\n");
940
+ }
941
+
942
+ function pluginCacheOwnerRoot() {
943
+ return join(codexHome(), "plugins", "cache", pluginName);
944
+ }
945
+
848
946
  function pluginCacheRoot(version) {
849
- return join(codexHome(), "plugins", "cache", pluginName, pluginName, version);
947
+ return join(pluginCacheOwnerRoot(), pluginName, version);
850
948
  }
851
949
 
852
950
  function enablePluginConfig() {
@@ -969,33 +1067,6 @@ function firstLine(value) {
969
1067
  return (value || "").split(/\r?\n/).find((line) => line.trim())?.trim() || "";
970
1068
  }
971
1069
 
972
- async function extend() {
973
- if (args.includes("--help") || args.includes("-h")) {
974
- extendUsage();
975
- return;
976
- }
977
-
978
- const subcommand = positional(1) || "";
979
- switch (subcommand) {
980
- case "":
981
- await extendCatalog();
982
- break;
983
- case "install":
984
- await extendInstall();
985
- break;
986
- case "doctor":
987
- extendDoctor();
988
- break;
989
- case "help":
990
- case "--help":
991
- case "-h":
992
- extendUsage();
993
- break;
994
- default:
995
- await extendDetails(subcommand);
996
- }
997
- }
998
-
999
1070
  async function board() {
1000
1071
  const goal = optionValue("--goal") || positional(1);
1001
1072
  if (!goal) {
@@ -1003,8 +1074,9 @@ async function board() {
1003
1074
  process.exit(2);
1004
1075
  }
1005
1076
 
1006
- const script = await ensureLocalBoardExtension();
1007
- const scriptArgs = [script, "--goal", goal];
1077
+ const absoluteGoal = resolve(goal);
1078
+ const script = ensureLocalBoardSurface();
1079
+ const scriptArgs = [script, "--goal", absoluteGoal];
1008
1080
  for (const option of ["--host", "--port"]) {
1009
1081
  const value = optionValue(option);
1010
1082
  if (value) scriptArgs.push(option, value);
@@ -1035,7 +1107,7 @@ async function prompt() {
1035
1107
  }
1036
1108
 
1037
1109
  const script = join(skillSource, "scripts", "render-task-prompt.mjs");
1038
- const scriptArgs = [script, ...args.slice(1)];
1110
+ const scriptArgs = [script, ...resolveChildGoalArgs(args.slice(1))];
1039
1111
  const result = spawnSync(process.execPath, scriptArgs, {
1040
1112
  cwd: packageRoot,
1041
1113
  encoding: "utf8",
@@ -1049,7 +1121,7 @@ async function prompt() {
1049
1121
 
1050
1122
  async function parallelPlan() {
1051
1123
  const script = join(skillSource, "scripts", "parallel-plan.mjs");
1052
- const scriptArgs = [script, ...args.slice(1).filter((arg) => arg !== "--parallel-plan")];
1124
+ const scriptArgs = [script, ...resolveChildGoalArgs(args.slice(1).filter((arg) => arg !== "--parallel-plan"))];
1053
1125
  const result = spawnSync(process.execPath, scriptArgs, {
1054
1126
  cwd: packageRoot,
1055
1127
  encoding: "utf8",
@@ -1061,303 +1133,18 @@ async function parallelPlan() {
1061
1133
  process.exit(result.status ?? 1);
1062
1134
  }
1063
1135
 
1064
- async function ensureLocalBoardExtension() {
1065
- const id = "local-goal-board";
1066
- const script = join(extensionTarget(id), "scripts", "local-goal-board.mjs");
1067
- if (existsSync(script)) return script;
1068
-
1069
- const catalog = await loadCatalog();
1070
- const extension = catalog.extensions.find((candidate) => candidate.id === id);
1071
- if (!extension) {
1072
- throw new Error(`Extension ${id} is not available in ${catalog.url}.`);
1073
- }
1074
-
1075
- await installCatalogExtension(catalog, extension);
1136
+ function ensureLocalBoardSurface() {
1137
+ const script = join(skillSource, "surfaces", "local-goal-board", "scripts", "local-goal-board.mjs");
1076
1138
  if (!existsSync(script)) {
1077
- throw new Error(`Extension ${id} installed, but script is missing: ${script}`);
1139
+ throw new Error(`Bundled GoalBuddy board surface is missing: ${script}`);
1078
1140
  }
1079
1141
  return script;
1080
1142
  }
1081
1143
 
1082
- function extendUsage() {
1083
- console.log(`${canonicalProductName} Extend
1084
-
1085
- Usage:
1086
- ${canonicalCliName} extend [--catalog-url <url-or-path>] [--kind <kind>] [--json]
1087
- ${canonicalCliName} extend <id> [--catalog-url <url-or-path>] [--json]
1088
- ${canonicalCliName} extend install <id> [--catalog-url <url-or-path>] [--dry-run] [--force] [--json]
1089
- ${canonicalCliName} extend install --all [--catalog-url <url-or-path>] [--dry-run] [--force] [--json]
1090
- ${canonicalCliName} extend doctor [<id>] [--codex-home <path>] [--json]
1091
-
1092
- States:
1093
- available Listed in the catalog.
1094
- installed Copied into the local ${canonicalProductName} skill install.
1095
- enabled Allowed by a goal or task. Not implemented by this command yet.
1096
- configured Required local env/provider settings are present.
1097
-
1098
- Catalog:
1099
- Defaults to ${defaultCatalogUrl}
1100
- Override with --catalog-url, GOALBUDDY_EXTEND_CATALOG_URL, or legacy GOAL_MAKER_EXTEND_CATALOG_URL.
1101
- `);
1102
- }
1103
-
1104
- async function extendCatalog() {
1105
- const catalog = await loadCatalog();
1106
- const kind = optionValue("--kind");
1107
- const extensions = catalog.extensions
1108
- .filter((extension) => !kind || extension.kind === kind)
1109
- .map(extensionWithLocalState);
1110
-
1111
- if (hasFlag("--json")) {
1112
- printJson({ catalog_url: catalog.url, extensions });
1113
- return;
1114
- }
1115
-
1116
- console.log("Available extensions");
1117
- if (extensions.length === 0) {
1118
- console.log("");
1119
- console.log(kind ? `No ${kind} extensions found.` : "No extensions found.");
1120
- return;
1121
- }
1122
- console.log("");
1123
- for (const extension of extensions) {
1124
- console.log(extension.name || extension.id);
1125
- if (extension.summary) console.log(` ${extension.summary}`);
1126
- console.log(` Details: npx ${canonicalCliName} extend ${extension.id}`);
1127
- console.log("");
1128
- }
1129
- console.log("Install all:");
1130
- console.log(` npx ${canonicalCliName} extend install --all`);
1131
- }
1132
-
1133
- async function extendDetails(id) {
1134
- const catalog = await loadCatalog();
1135
- const extension = catalog.extensions.find((candidate) => candidate.id === id);
1136
- if (!extension) {
1137
- printExtensionNotFound(id, catalog.extensions);
1138
- process.exit(1);
1139
- }
1140
- validateCatalogExtension(extension);
1141
- const detailed = extensionWithLocalState(extension);
1142
-
1143
- if (hasFlag("--json")) {
1144
- printJson({ catalog_url: catalog.url, extension: detailed });
1145
- return;
1146
- }
1147
-
1148
- console.log(extension.name || extension.summary || "");
1149
- console.log("");
1150
- if (extension.summary && extension.summary !== extension.name) {
1151
- console.log(extension.summary);
1152
- console.log("");
1153
- }
1154
- console.log(`Status: ${detailed.state.installed ? "installed" : "available"}`);
1155
- console.log(`Configured: ${detailed.state.configured ? "yes" : "no"}`);
1156
- console.log(`ID: ${extension.id}`);
1157
- console.log(`Kind: ${extension.kind}`);
1158
- if (extension.version) console.log(`Version: ${extension.version}`);
1159
- console.log(`Activation: ${extension.activation || "unspecified"}`);
1160
- console.log(`Safe by default: ${extension.safe_by_default ? "yes" : "no"}`);
1161
- console.log(`Requires approval: ${extension.requires_approval ? "yes" : "no"}`);
1162
- if (!detailed.state.configured && detailed.state.missing_env.length) {
1163
- console.log(`Missing env: ${detailed.state.missing_env.join(", ")}`);
1164
- }
1165
- printListSection("Use when", extension.use_when);
1166
- printListSection("Outputs", extension.outputs);
1167
- printListSection("Reads", extension.reads);
1168
- printListSection("Writes", extension.writes);
1169
- printListSection("Side effects", extension.side_effects);
1170
- printListSection("Agent instructions", extension.agent_instructions);
1171
- printListSection("Auth env", extension.auth?.env);
1172
- printSupports(extension.supports);
1173
- console.log("");
1174
- console.log("Local use prompt:");
1175
- 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.`}`);
1176
- console.log("");
1177
- console.log("Install:");
1178
- console.log(` npx ${canonicalCliName} extend install ${extension.id}`);
1179
- console.log("");
1180
- console.log("Preview install:");
1181
- console.log(` npx ${canonicalCliName} extend install ${extension.id} --dry-run`);
1182
- }
1183
-
1184
- function printListSection(title, values) {
1185
- if (!Array.isArray(values) || values.length === 0) return;
1186
- console.log("");
1187
- console.log(`${title}:`);
1188
- for (const value of values) console.log(` - ${value}`);
1189
- }
1190
-
1191
- function printSupports(supports) {
1192
- if (!supports || typeof supports !== "object") return;
1193
- const entries = Object.entries(supports);
1194
- if (entries.length === 0) return;
1195
- console.log("");
1196
- console.log("Supports:");
1197
- for (const [key, value] of entries) console.log(` - ${key}: ${value}`);
1198
- }
1199
-
1200
- function firstValue(values, fallback) {
1201
- return Array.isArray(values) && values.length ? values[0] : fallback;
1202
- }
1203
-
1204
- async function extendInstall() {
1205
- const id = positional(2);
1206
- const catalog = await loadCatalog();
1207
- if (hasFlag("--all")) {
1208
- await extendInstallAll(catalog);
1209
- return;
1210
- }
1211
-
1212
- if (!id) throw new Error(`Missing extension id. Usage: ${canonicalCliName} extend install <id>`);
1213
- const extension = catalog.extensions.find((candidate) => candidate.id === id);
1214
- if (!extension) {
1215
- printExtensionNotFound(id, catalog.extensions);
1216
- process.exit(1);
1217
- }
1218
- const result = await installCatalogExtension(catalog, extension);
1219
-
1220
- if (hasFlag("--dry-run")) {
1221
- if (hasFlag("--json")) {
1222
- printJson({ dry_run: true, extension: extensionWithLocalState(extension), target: result.target, files: result.plan });
1223
- } else {
1224
- console.log(`Would install ${extension.id} to ${result.target}`);
1225
- for (const file of result.plan) console.log(` ${file.path}`);
1226
- }
1227
- return;
1228
- }
1229
-
1230
- if (hasFlag("--json")) {
1231
- printJson({ installed: true, extension: extension.id, target: result.target });
1232
- } else {
1233
- console.log(`Installed ${extension.id} to ${result.target}`);
1234
- }
1235
- }
1236
-
1237
- async function extendInstallAll(catalog) {
1238
- const results = [];
1239
- for (const extension of catalog.extensions) {
1240
- if (existsSync(extensionTarget(extension.id)) && !hasFlag("--force")) {
1241
- validateCatalogExtension(extension);
1242
- results.push({ extension, target: extensionTarget(extension.id), plan: installPlan(catalog, extension, extensionTarget(extension.id)), skipped: true });
1243
- continue;
1244
- }
1245
- results.push(await installCatalogExtension(catalog, extension));
1246
- }
1247
-
1248
- if (hasFlag("--dry-run")) {
1249
- if (hasFlag("--json")) {
1250
- printJson({
1251
- dry_run: true,
1252
- extensions: results.map(({ extension, target, plan }) => ({
1253
- extension: extensionWithLocalState(extension),
1254
- target,
1255
- files: plan,
1256
- })),
1257
- });
1258
- } else {
1259
- console.log(`Would install ${results.length} extensions`);
1260
- for (const { extension, target, plan } of results) {
1261
- console.log(`${extension.id} -> ${target}`);
1262
- for (const file of plan) console.log(` ${file.path}`);
1263
- }
1264
- }
1265
- return;
1266
- }
1267
-
1268
- if (hasFlag("--json")) {
1269
- printJson({
1270
- installed: true,
1271
- count: results.length,
1272
- extensions: results.map(({ extension, target, skipped }) => ({ id: extension.id, target, skipped: Boolean(skipped) })),
1273
- });
1274
- } else {
1275
- const installedCount = results.filter((result) => !result.skipped).length;
1276
- const skippedCount = results.length - installedCount;
1277
- console.log(`Installed ${installedCount} extensions${skippedCount ? `, skipped ${skippedCount} already installed` : ""}`);
1278
- for (const { extension, target, skipped } of results) console.log(` ${extension.id} -> ${target}${skipped ? " (already installed)" : ""}`);
1279
- }
1280
- }
1281
-
1282
- async function installCatalogExtension(catalog, extension) {
1283
- validateCatalogExtension(extension);
1284
- const target = extensionTarget(extension.id);
1285
- const plan = installPlan(catalog, extension, target);
1286
-
1287
- if (hasFlag("--dry-run")) return { extension, target, plan };
1288
-
1289
- assertSkillInstalledForExtensionInstall();
1290
- if (existsSync(target) && !hasFlag("--force")) {
1291
- throw new Error(`Extension already installed: ${target}. Use --force to overwrite.`);
1292
- }
1293
-
1294
- const temp = `${target}.tmp-${process.pid}-${Date.now()}`;
1295
- rmSync(temp, { recursive: true, force: true });
1296
- mkdirSync(temp, { recursive: true });
1297
-
1298
- try {
1299
- for (const file of plan) {
1300
- const content = await readResource(file.url);
1301
- const actualSha = sha256(content);
1302
- if (actualSha !== file.sha256) {
1303
- throw new Error(`Checksum mismatch for ${file.path}: expected ${file.sha256}, got ${actualSha}`);
1304
- }
1305
- const destination = join(temp, file.path);
1306
- mkdirSync(dirname(destination), { recursive: true });
1307
- writeFileSync(destination, content);
1308
- }
1309
-
1310
- writeFileSync(join(temp, ".installed.json"), `${JSON.stringify({
1311
- id: extension.id,
1312
- version: extension.version || "",
1313
- kind: extension.kind,
1314
- catalog_url: catalog.url,
1315
- installed_at: new Date().toISOString(),
1316
- manifest: publicExtension(extension),
1317
- files: plan.map(({ path, sha256: digest }) => ({ path, sha256: digest })),
1318
- }, null, 2)}\n`);
1319
-
1320
- rmSync(target, { recursive: true, force: true });
1321
- mkdirSync(dirname(target), { recursive: true });
1322
- renameSync(temp, target);
1323
- } catch (error) {
1324
- rmSync(temp, { recursive: true, force: true });
1325
- throw error;
1326
- }
1327
-
1328
- return { extension, target, plan };
1329
- }
1330
-
1331
- function extendDoctor() {
1332
- const id = positional(2);
1333
- const targets = installedExtensions().filter((extension) => !id || extension.id === id);
1334
- if (id && targets.length === 0) throw new Error(`Extension is not installed: ${id}`);
1335
-
1336
- const reports = targets.map(doctorInstalledExtension);
1337
- const ok = reports.every((report) => report.ok);
1338
-
1339
- if (hasFlag("--json")) {
1340
- printJson({ ok, extensions: reports });
1341
- } else if (reports.length === 0) {
1342
- console.log("No extensions installed.");
1343
- } else {
1344
- for (const report of reports) {
1345
- console.log(`${report.ok ? "ok" : "not ok"}\t${report.id}`);
1346
- for (const issue of report.issues) console.log(` - ${issue}`);
1347
- }
1348
- }
1349
-
1350
- process.exit(ok ? 0 : 1);
1351
- }
1352
-
1353
1144
  function installedSkillRoot() {
1354
1145
  return join(codexHome(), "skills", canonicalSkillDirectory);
1355
1146
  }
1356
1147
 
1357
- function installedPluginSkillRoot() {
1358
- return installedCodexPlugin().skill_path;
1359
- }
1360
-
1361
1148
  function installedCodexPlugin() {
1362
1149
  const root = join(codexHome(), "plugins", "cache", pluginName, pluginName);
1363
1150
  const configPath = join(codexHome(), "config.toml");
@@ -1413,119 +1200,10 @@ function pluginConfigEnabled(configPath) {
1413
1200
  return false;
1414
1201
  }
1415
1202
 
1416
- function activeSkillRoot() {
1417
- if (existsSync(join(installedSkillRoot(), "SKILL.md"))) return installedSkillRoot();
1418
- const pluginSkillRoot = installedPluginSkillRoot();
1419
- if (pluginSkillRoot) return pluginSkillRoot;
1420
- return installedSkillRoot();
1421
- }
1422
-
1423
1203
  function legacyInstalledSkillRoot() {
1424
1204
  return join(codexHome(), "skills", legacySkillName);
1425
1205
  }
1426
1206
 
1427
- function extendRoot() {
1428
- return join(activeSkillRoot(), "extend");
1429
- }
1430
-
1431
- function extensionTarget(id) {
1432
- return join(extendRoot(), id);
1433
- }
1434
-
1435
- function catalogUrl() {
1436
- return optionValue("--catalog-url")
1437
- || optionValue("--catalog")
1438
- || process.env.GOALBUDDY_EXTEND_CATALOG_URL
1439
- || process.env.GOAL_MAKER_EXTEND_CATALOG_URL
1440
- || defaultCatalogUrl;
1441
- }
1442
-
1443
- async function loadCatalog() {
1444
- const url = catalogUrl();
1445
- const text = await readResource(url);
1446
- const catalog = JSON.parse(text);
1447
- if (!Array.isArray(catalog.extensions)) {
1448
- throw new Error("Extension catalog must contain an extensions array.");
1449
- }
1450
- return { ...catalog, url };
1451
- }
1452
-
1453
- function printExtensionNotFound(id, extensions) {
1454
- console.error(`Extension not found: ${id}`);
1455
- if (extensions.length) {
1456
- console.error("");
1457
- console.error("Available extensions:");
1458
- for (const extension of extensions) {
1459
- console.error(` ${extension.id}`);
1460
- }
1461
- }
1462
- console.error("");
1463
- console.error("Try:");
1464
- console.error(` npx ${canonicalCliName} extend`);
1465
- }
1466
-
1467
- function validateCatalogExtension(extension) {
1468
- if (!extension.id || !/^[a-z0-9][a-z0-9-]*$/.test(extension.id)) {
1469
- throw new Error(`Invalid extension id: ${extension.id || "<missing>"}`);
1470
- }
1471
- if (!extension.kind) throw new Error(`Extension ${extension.id} missing kind.`);
1472
- if (!Array.isArray(extension.files) || extension.files.length === 0) {
1473
- throw new Error(`Extension ${extension.id} must list files.`);
1474
- }
1475
- for (const file of extension.files) {
1476
- validateCatalogFile(extension, file);
1477
- }
1478
- }
1479
-
1480
- function validateCatalogFile(extension, file) {
1481
- if (!file.path) throw new Error(`Extension ${extension.id} has a file without path.`);
1482
- if (!file.url) throw new Error(`Extension ${extension.id} file ${file.path} missing url.`);
1483
- if (!/^[a-f0-9]{64}$/i.test(file.sha256 || "")) {
1484
- throw new Error(`Extension ${extension.id} file ${file.path} must include sha256.`);
1485
- }
1486
- safeRelativePath(file.path);
1487
- }
1488
-
1489
- function installPlan(catalog, extension, target) {
1490
- return extension.files.map((file) => ({
1491
- path: safeRelativePath(file.path),
1492
- url: resolveResourceUrl(catalog.url, file.url),
1493
- sha256: file.sha256.toLowerCase(),
1494
- target: join(target, safeRelativePath(file.path)),
1495
- }));
1496
- }
1497
-
1498
- function safeRelativePath(path) {
1499
- const normalized = normalize(path).replaceAll("\\", "/");
1500
- if (!normalized || normalized.startsWith("../") || normalized === ".." || normalized.startsWith("/") || /^[A-Za-z]:/.test(normalized)) {
1501
- throw new Error(`Unsafe extension file path: ${path}`);
1502
- }
1503
- return normalized;
1504
- }
1505
-
1506
- function resolveResourceUrl(base, value) {
1507
- if (/^https?:\/\//.test(value) || value.startsWith("file://") || value.startsWith("/")) return value;
1508
- if (/^https?:\/\//.test(base) || base.startsWith("file://")) {
1509
- return new URL(value, base).href;
1510
- }
1511
- return resolve(dirname(resolve(base)), value);
1512
- }
1513
-
1514
- async function readResource(location) {
1515
- if (/^https?:\/\//.test(location)) {
1516
- if (!globalThis.fetch) throw new Error("This Node runtime does not provide fetch.");
1517
- const response = await globalThis.fetch(location, {
1518
- headers: {
1519
- "accept-encoding": "identity",
1520
- },
1521
- });
1522
- if (!response.ok) throw new Error(`Failed to fetch ${location}: HTTP ${response.status}`);
1523
- return Buffer.from(await response.arrayBuffer());
1524
- }
1525
- const path = location.startsWith("file://") ? fileURLToPath(location) : resolve(location);
1526
- return readFileSync(path);
1527
- }
1528
-
1529
1207
  function sha256(content) {
1530
1208
  return createHash("sha256").update(content).digest("hex");
1531
1209
  }
@@ -1558,50 +1236,8 @@ function listFiles(root, { exclude = new Set(), prefix = "" } = {}) {
1558
1236
  return files;
1559
1237
  }
1560
1238
 
1561
- function preserveInstalledExtensions(targets, { tempRoot = "" } = {}) {
1562
- const ids = [];
1563
- const firstTarget = targets.find(Boolean) || codexHome();
1564
- const tempPath = join(tempRoot || dirname(dirname(firstTarget)), `.goalbuddy-preserved-extend-${process.pid}-${Date.now()}`);
1565
- let hasExtensions = false;
1566
- for (const target of targets) {
1567
- if (!target) continue;
1568
- const source = join(target, "extend");
1569
- if (!existsSync(source)) continue;
1570
- for (const entry of readdirSync(source, { withFileTypes: true })) {
1571
- if (bundledCoreExtensionIds.has(entry.name)) continue;
1572
- mkdirSync(tempPath, { recursive: true });
1573
- const from = join(source, entry.name);
1574
- const to = join(tempPath, entry.name);
1575
- cpSync(from, to, { recursive: true, force: true });
1576
- if (entry.isDirectory()) ids.push(entry.name);
1577
- hasExtensions = true;
1578
- }
1579
- rmSync(source, { recursive: true, force: true });
1580
- }
1581
- return { tempPath: hasExtensions ? tempPath : "", ids: uniqueSorted(ids) };
1582
- }
1583
-
1584
- function restoreInstalledExtensions(target, tempPath) {
1585
- if (!tempPath) return;
1586
- const destinationRoot = join(target, "extend");
1587
- mkdirSync(destinationRoot, { recursive: true });
1588
- for (const entry of readdirSync(tempPath, { withFileTypes: true })) {
1589
- cpSync(join(tempPath, entry.name), join(destinationRoot, entry.name), { recursive: true, force: true });
1590
- }
1591
- }
1592
-
1593
- function cleanupPreservedExtensions(paths) {
1594
- for (const path of uniqueSorted(paths.filter(Boolean))) {
1595
- rmSync(path, { recursive: true, force: true });
1596
- }
1597
- }
1598
-
1599
- function uniqueSorted(values) {
1600
- return [...new Set(values)].sort();
1601
- }
1602
-
1603
1239
  function installFingerprintExcludes() {
1604
- return new Set(["extend", ".goalbuddy-install.json", ".goal-maker-install.json"]);
1240
+ return new Set([".goalbuddy-install.json", ".goal-maker-install.json"]);
1605
1241
  }
1606
1242
 
1607
1243
  function installMetadataPath(target) {
@@ -1633,53 +1269,6 @@ function writeInstallMetadata(target, previousMetadata) {
1633
1269
  }, null, 2)}\n`);
1634
1270
  }
1635
1271
 
1636
- async function extensionDiscoverySummary() {
1637
- try {
1638
- const catalog = await loadCatalog();
1639
- const extensions = catalog.extensions.map(extensionWithLocalState);
1640
- return {
1641
- catalog_url: catalog.url,
1642
- catalog_version: catalog.version || null,
1643
- available_count: extensions.length,
1644
- installed_count: extensions.filter((extension) => extension.state.installed).length,
1645
- available: extensions.map((extension) => ({
1646
- id: extension.id,
1647
- name: extension.name,
1648
- kind: extension.kind,
1649
- version: extension.version,
1650
- summary: extension.summary,
1651
- activation: extension.activation,
1652
- safe_by_default: extension.safe_by_default,
1653
- installed: extension.state.installed,
1654
- configured: extension.state.configured,
1655
- use_when: extension.use_when,
1656
- next_command: `${canonicalCliName} extend ${extension.id}`,
1657
- })),
1658
- recommended: extensions
1659
- .filter((extension) => extension.safe_by_default && !extension.state.installed)
1660
- .map((extension) => ({
1661
- id: extension.id,
1662
- name: extension.name,
1663
- kind: extension.kind,
1664
- activation: extension.activation,
1665
- summary: extension.summary,
1666
- use_when: extension.use_when.slice(0, 1),
1667
- next_command: `${canonicalCliName} extend ${extension.id}`,
1668
- })),
1669
- };
1670
- } catch (error) {
1671
- return {
1672
- catalog_url: catalogUrl(),
1673
- catalog_version: null,
1674
- available_count: 0,
1675
- installed_count: 0,
1676
- available: [],
1677
- recommended: [],
1678
- error: error.message,
1679
- };
1680
- }
1681
- }
1682
-
1683
1272
  function printInstallReport(report) {
1684
1273
  const verb = report.command === "update" ? "Updated" : "Installed";
1685
1274
  const previous = report.package.previous_version && report.package.previous_version !== report.package.current_version
@@ -1692,31 +1281,11 @@ function printInstallReport(report) {
1692
1281
  console.log(`Compatibility skill: ${report.skill.compatibility_path}`);
1693
1282
  const agentSummary = summarizeStatuses(report.agents);
1694
1283
  console.log(`Agents: ${agentSummary}`);
1695
- if (report.skill.preserved_extensions.length) {
1696
- console.log(`Preserved extensions: ${report.skill.preserved_extensions.join(", ")}`);
1697
- }
1698
-
1699
- if (report.extensions.error) {
1700
- console.log("");
1701
- console.log(`Extensions: unavailable (${report.extensions.error})`);
1702
- } else {
1703
- console.log("");
1704
- console.log(`Extensions: ${report.extensions.available_count} available from ${report.extensions.catalog_url}`);
1705
- if (report.extensions.recommended.length) {
1706
- console.log("");
1707
- console.log("Recommended:");
1708
- for (const extension of report.extensions.recommended.slice(0, 3)) {
1709
- console.log(` ${extension.name || extension.id}`);
1710
- if (extension.summary) console.log(` ${extension.summary}`);
1711
- console.log(` Details: npx ${extension.next_command}`);
1712
- }
1713
- }
1714
- }
1715
1284
 
1716
1285
  console.log("");
1717
1286
  console.log("Next:");
1718
1287
  console.log(` $${canonicalSkillName}`);
1719
- console.log(` ${canonicalCliName} extend`);
1288
+ console.log(` ${canonicalCliName} board docs/goals/<slug>`);
1720
1289
  console.log(` ${legacyCliName} remains a temporary compatibility alias.`);
1721
1290
  }
1722
1291
 
@@ -1730,9 +1299,6 @@ function printEverywhereInstallReport(report) {
1730
1299
  console.log(`Codex: not completed (${report.codex.error})`);
1731
1300
  } else if (report.codex) {
1732
1301
  console.log(`Codex: plugin ${report.codex.version} enabled at ${report.codex.cache_path}`);
1733
- if (report.codex.preserved_extensions?.length) {
1734
- console.log(`Codex preserved extensions: ${report.codex.preserved_extensions.join(", ")}`);
1735
- }
1736
1302
  }
1737
1303
 
1738
1304
  if (report.claude?.ok === false) {
@@ -1767,12 +1333,6 @@ function summarizeStatuses(items) {
1767
1333
  .join(", ");
1768
1334
  }
1769
1335
 
1770
- function assertSkillInstalledForExtensionInstall() {
1771
- if (!existsSync(join(activeSkillRoot(), "SKILL.md"))) {
1772
- throw new Error(`${canonicalProductName} skill is not installed. Run: npx ${canonicalCliName}`);
1773
- }
1774
- }
1775
-
1776
1336
  function latestPublishedVersion() {
1777
1337
  if (process.env.GOALBUDDY_TEST_NPM_LATEST_VERSION) {
1778
1338
  return normalizeVersion(process.env.GOALBUDDY_TEST_NPM_LATEST_VERSION);
@@ -1818,106 +1378,6 @@ function compareVersions(left, right) {
1818
1378
  return left.localeCompare(right);
1819
1379
  }
1820
1380
 
1821
- function installedExtensions() {
1822
- const root = extendRoot();
1823
- if (!existsSync(root)) return [];
1824
- return readdirSync(root, { withFileTypes: true })
1825
- .filter((entry) => entry.isDirectory())
1826
- .map((entry) => readInstalledExtension(join(root, entry.name)))
1827
- .filter(Boolean);
1828
- }
1829
-
1830
- function readInstalledExtension(path) {
1831
- const installedPath = join(path, ".installed.json");
1832
- if (!existsSync(installedPath)) {
1833
- return { id: basename(path), path, issues: ["missing .installed.json"] };
1834
- }
1835
- const data = JSON.parse(readFileSync(installedPath, "utf8"));
1836
- return {
1837
- ...data,
1838
- path,
1839
- };
1840
- }
1841
-
1842
- function doctorInstalledExtension(extension) {
1843
- const issues = [];
1844
- for (const file of extension.files || []) {
1845
- const path = join(extension.path, safeRelativePath(file.path));
1846
- if (!existsSync(path)) {
1847
- issues.push(`missing file: ${file.path}`);
1848
- continue;
1849
- }
1850
- const actualSha = sha256(readFileSync(path));
1851
- if (actualSha !== file.sha256) {
1852
- issues.push(`checksum mismatch: ${file.path}`);
1853
- }
1854
- }
1855
- for (const envName of extension.manifest?.auth?.env || []) {
1856
- if (!process.env[envName]) issues.push(`missing env: ${envName}`);
1857
- }
1858
-
1859
- return {
1860
- id: extension.id,
1861
- version: extension.version || "",
1862
- kind: extension.kind || "",
1863
- path: extension.path,
1864
- ok: issues.length === 0,
1865
- issues,
1866
- };
1867
- }
1868
-
1869
- function publicExtension(extension) {
1870
- return {
1871
- id: extension.id,
1872
- name: extension.name || "",
1873
- kind: extension.kind || "",
1874
- version: extension.version || "",
1875
- summary: extension.summary || "",
1876
- description: extension.description || "",
1877
- local_use_prompt: extension.local_use_prompt || "",
1878
- source: extension.source || "",
1879
- docs: extension.docs || "",
1880
- use_when: extension.use_when || [],
1881
- activation: extension.activation || "",
1882
- outputs: extension.outputs || [],
1883
- requires_approval: extension.requires_approval || false,
1884
- safe_by_default: extension.safe_by_default || false,
1885
- applies_to: extension.applies_to || {},
1886
- reads: extension.reads || [],
1887
- writes: extension.writes || [],
1888
- side_effects: extension.side_effects || [],
1889
- agent_instructions: extension.agent_instructions || [],
1890
- auth: extension.auth || { env: [] },
1891
- supports: extension.supports || {},
1892
- source_of_truth: extension.source_of_truth || "local",
1893
- files: (extension.files || []).map((file) => ({
1894
- path: file.path,
1895
- sha256: file.sha256,
1896
- })),
1897
- };
1898
- }
1899
-
1900
- function extensionWithLocalState(extension) {
1901
- return {
1902
- ...publicExtension(extension),
1903
- state: {
1904
- available: true,
1905
- installed: existsSync(extensionTarget(extension.id)),
1906
- enabled: false,
1907
- configured: configuredFor(extension),
1908
- missing_env: missingEnv(extension),
1909
- },
1910
- };
1911
- }
1912
-
1913
- function configuredFor(extension) {
1914
- return missingEnv(extension).length === 0;
1915
- }
1916
-
1917
- function missingEnv(extension) {
1918
- return (extension.auth?.env || []).filter((envName) => !process.env[envName]);
1919
- }
1920
-
1921
1381
  function printJson(value) {
1922
1382
  console.log(JSON.stringify(value, null, 2));
1923
1383
  }