@skillcap/gdh 0.6.0 → 0.8.0

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 (69) hide show
  1. package/INSTALL-BUNDLE.json +1 -1
  2. package/README.md +23 -5
  3. package/node_modules/@gdh/adapters/dist/index.d.ts +6 -3
  4. package/node_modules/@gdh/adapters/dist/index.d.ts.map +1 -1
  5. package/node_modules/@gdh/adapters/dist/index.js +222 -258
  6. package/node_modules/@gdh/adapters/dist/index.js.map +1 -1
  7. package/node_modules/@gdh/adapters/dist/self-update-mechanics.d.ts.map +1 -1
  8. package/node_modules/@gdh/adapters/dist/self-update-mechanics.js +9 -1
  9. package/node_modules/@gdh/adapters/dist/self-update-mechanics.js.map +1 -1
  10. package/node_modules/@gdh/adapters/package.json +8 -8
  11. package/node_modules/@gdh/authoring/dist/prepare.d.ts.map +1 -1
  12. package/node_modules/@gdh/authoring/dist/prepare.js +5 -1
  13. package/node_modules/@gdh/authoring/dist/prepare.js.map +1 -1
  14. package/node_modules/@gdh/authoring/package.json +2 -2
  15. package/node_modules/@gdh/cli/dist/index.d.ts +35 -0
  16. package/node_modules/@gdh/cli/dist/index.d.ts.map +1 -1
  17. package/node_modules/@gdh/cli/dist/index.js +279 -150
  18. package/node_modules/@gdh/cli/dist/index.js.map +1 -1
  19. package/node_modules/@gdh/cli/dist/migrate.d.ts +5 -1
  20. package/node_modules/@gdh/cli/dist/migrate.d.ts.map +1 -1
  21. package/node_modules/@gdh/cli/dist/migrate.js +57 -3
  22. package/node_modules/@gdh/cli/dist/migrate.js.map +1 -1
  23. package/node_modules/@gdh/cli/dist/self-update.d.ts.map +1 -1
  24. package/node_modules/@gdh/cli/dist/self-update.js +52 -3
  25. package/node_modules/@gdh/cli/dist/self-update.js.map +1 -1
  26. package/node_modules/@gdh/cli/dist/setup.d.ts +41 -0
  27. package/node_modules/@gdh/cli/dist/setup.d.ts.map +1 -1
  28. package/node_modules/@gdh/cli/dist/setup.js +181 -26
  29. package/node_modules/@gdh/cli/dist/setup.js.map +1 -1
  30. package/node_modules/@gdh/cli/package.json +10 -10
  31. package/node_modules/@gdh/core/dist/index.d.ts +77 -4
  32. package/node_modules/@gdh/core/dist/index.d.ts.map +1 -1
  33. package/node_modules/@gdh/core/dist/index.js +45 -6
  34. package/node_modules/@gdh/core/dist/index.js.map +1 -1
  35. package/node_modules/@gdh/core/dist/update-cache.d.ts +12 -0
  36. package/node_modules/@gdh/core/dist/update-cache.d.ts.map +1 -1
  37. package/node_modules/@gdh/core/dist/update-cache.js +20 -0
  38. package/node_modules/@gdh/core/dist/update-cache.js.map +1 -1
  39. package/node_modules/@gdh/core/dist/update-probe.d.ts.map +1 -1
  40. package/node_modules/@gdh/core/dist/update-probe.js +7 -1
  41. package/node_modules/@gdh/core/dist/update-probe.js.map +1 -1
  42. package/node_modules/@gdh/core/package.json +1 -1
  43. package/node_modules/@gdh/docs/dist/index.d.ts +2 -0
  44. package/node_modules/@gdh/docs/dist/index.d.ts.map +1 -1
  45. package/node_modules/@gdh/docs/dist/index.js +1 -0
  46. package/node_modules/@gdh/docs/dist/index.js.map +1 -1
  47. package/node_modules/@gdh/docs/dist/recovery-hints.d.ts +43 -0
  48. package/node_modules/@gdh/docs/dist/recovery-hints.d.ts.map +1 -0
  49. package/node_modules/@gdh/docs/dist/recovery-hints.js +203 -0
  50. package/node_modules/@gdh/docs/dist/recovery-hints.js.map +1 -0
  51. package/node_modules/@gdh/docs/package.json +2 -2
  52. package/node_modules/@gdh/mcp/package.json +8 -8
  53. package/node_modules/@gdh/observability/package.json +2 -2
  54. package/node_modules/@gdh/runtime/package.json +2 -2
  55. package/node_modules/@gdh/scan/dist/detect-existing-onboarding.d.ts +19 -0
  56. package/node_modules/@gdh/scan/dist/detect-existing-onboarding.d.ts.map +1 -0
  57. package/node_modules/@gdh/scan/dist/detect-existing-onboarding.js +58 -0
  58. package/node_modules/@gdh/scan/dist/detect-existing-onboarding.js.map +1 -0
  59. package/node_modules/@gdh/scan/dist/index.d.ts +1 -0
  60. package/node_modules/@gdh/scan/dist/index.d.ts.map +1 -1
  61. package/node_modules/@gdh/scan/dist/index.js +1 -0
  62. package/node_modules/@gdh/scan/dist/index.js.map +1 -1
  63. package/node_modules/@gdh/scan/dist/onboard.d.ts +2 -0
  64. package/node_modules/@gdh/scan/dist/onboard.d.ts.map +1 -1
  65. package/node_modules/@gdh/scan/dist/onboard.js +12 -4
  66. package/node_modules/@gdh/scan/dist/onboard.js.map +1 -1
  67. package/node_modules/@gdh/scan/package.json +3 -3
  68. package/node_modules/@gdh/verify/package.json +7 -7
  69. package/package.json +11 -11
@@ -9,8 +9,8 @@ import { CLAUDE_CHECK_UPDATE_HOOK_RELATIVE_PATH, CLAUDE_CHECK_UPDATE_WORKER_RELA
9
9
  import { renderClaudeCheckUpdateWorker } from "./claude-update-worker-render.js";
10
10
  import { CLAUDE_STATUSLINE_RELATIVE_PATH, renderClaudeUpdateStatusline, } from "./claude-statusline-render.js";
11
11
  import { CLAUDE_SETTINGS_RELATIVE_PATH, patchClaudeSettingsForGdhSessionStart, patchClaudeSettingsForGdhStatusline, } from "./claude-settings-patch.js";
12
- import { GDH_AGENT_CONTRACT_VERSION, GDH_CURSOR_RULE_VERSION, GDH_GUIDANCE_INDEX_VERSION, GDH_GUIDANCE_UNIT_VERSION, GDH_MCP_LAUNCHER_VERSION, GDH_PROJECT_CONFIG_VERSION, GDH_RECIPE_SCHEMA_VERSION, GDH_RULES_SCHEMA_VERSION, GDH_SCENARIO_SCHEMA_VERSION, definePackageBoundary, resolveCurrentGdhInstall, resolveGdhProductMetadata, } from "@gdh/core";
13
- import { createDefaultGuidanceUnits, getGuidanceStatus, resolveGuidanceQuery, } from "@gdh/docs";
12
+ import { GDH_AGENT_CONTRACT_VERSION, GDH_CURSOR_RULE_VERSION, GDH_GUIDANCE_INDEX_VERSION, GDH_GUIDANCE_UNIT_VERSION, GDH_PROJECT_CONFIG_VERSION, GDH_RECIPE_SCHEMA_VERSION, GDH_RULES_SCHEMA_VERSION, GDH_SCENARIO_SCHEMA_VERSION, assertProjectLifecycleCompatibilityInvariant, definePackageBoundary, resolveCurrentGdhInstall, resolveGdhProductMetadata, } from "@gdh/core";
13
+ import { createDefaultGuidanceUnits, getGuidanceStatus, resolveGuidanceQuery, resolveRecoveryHints, } from "@gdh/docs";
14
14
  import { inspectGuidanceAudit } from "@gdh/observability";
15
15
  import { inspectRuntimeBridgeSurface } from "@gdh/runtime";
16
16
  import { scanGodotProjectInventory } from "@gdh/scan";
@@ -65,8 +65,13 @@ export const CLAUDE_SCAN_COMMAND_RELATIVE_PATH = ".claude/commands/gdh/scan.md";
65
65
  export const CODEX_SCAN_SKILL_RELATIVE_PATH = ".codex/skills/gdh-scan/SKILL.md";
66
66
  export const CURSOR_SCAN_SKILL_RELATIVE_PATH = ".cursor/skills/gdh-scan/SKILL.md";
67
67
  export const LOCAL_PATH_HINTS_RELATIVE_PATH = ".gdh-state/local-paths.json";
68
- export const MCP_LAUNCHER_RELATIVE_PATH = ".gdh/bin/gdh-mcp.mjs";
68
+ export const CODEX_PROJECT_CONFIG_RELATIVE_PATH = ".codex/config.toml";
69
69
  export const GDH_MCP_SERVER_NAME = "gdh";
70
+ // Phase 17 UX-01: single source of truth for the `/gdh-status` preview-header
71
+ // literal. The character between "dry-run)" and "not" is em-dash U+2014 (NOT
72
+ // two hyphens). Centralizing here prevents silent drift across the 3
73
+ // `/gdh-status` renderer bodies (Pitfall #5 three-adapter parity).
74
+ const PREVIEW_HEADER_LITERAL = "Preview (dry-run) — not applied";
70
75
  const execFile = promisify(execFileCallback);
71
76
  export async function getSupportedAgentAdaptersStatus(targetPath, options = {}) {
72
77
  const includeUserLocal = options.includeUserLocal ?? true;
@@ -216,17 +221,18 @@ export async function inspectProjectLifecycleCompatibility(targetPath) {
216
221
  await inspectGuidanceUnitLifecycleSurface(resolvedTargetPath, projectConfig),
217
222
  inspectMcpManifestLifecycleSurface(resolvedTargetPath, projectConfig.mcp.enabled, adapterStatus),
218
223
  inspectCursorRuleLifecycleSurface(resolvedTargetPath, adapterStatus),
219
- inspectMcpLauncherLifecycleSurface(resolvedTargetPath, projectConfig.mcp.enabled, adapterStatus),
220
224
  inspectRuntimeBridgeLifecycleSurface(resolvedTargetPath, bridgeStatus),
221
225
  ]);
222
226
  }
223
227
  export async function buildGdhStatusResult(targetPath) {
224
228
  const context = await buildAuthoringContext(targetPath);
225
229
  const projectLifecycle = await inspectProjectLifecycleCompatibility(targetPath);
230
+ const recoveryHints = resolveRecoveryHints(projectLifecycle.reasons);
226
231
  return {
227
232
  ...context.status,
228
233
  product: resolveGdhProductMetadata(),
229
234
  projectLifecycle,
235
+ recoveryHints,
230
236
  };
231
237
  }
232
238
  export async function createGsdSnapshot(targetPath, options = {}) {
@@ -471,9 +477,12 @@ export function renderClaudeStatusCommand(pinnedVersion) {
471
477
  "Follow this order:",
472
478
  "",
473
479
  `1. Run \`npx -y @skillcap/gdh@${pinnedVersion} status\` and explain each readiness field.`,
474
- `2. Check if migration is needed with \`npx -y @skillcap/gdh@${pinnedVersion} migrate\`.`,
475
- "3. Surface any degraded or unavailable capabilities.",
476
- "4. Suggest the most productive next step based on current state.",
480
+ "2. Distinguish `blockingReasons` (needs /gdh-update or manual intervention) from `coupledReasons` (fixable via /gdh-migrate).",
481
+ `3. Check if migration is needed with \`npx -y @skillcap/gdh@${pinnedVersion} migrate\`.`,
482
+ `4. ${PREVIEW_HEADER_LITERAL} migrate output at this step describes planned actions ("would delete …") not applied ones ("deleted").`,
483
+ "5. If status JSON `recoveryHints` array is non-empty, surface each entry's `{command, skillName}` pair.",
484
+ "6. Surface any degraded or unavailable capabilities.",
485
+ "7. Suggest the most productive next step based on current state.",
477
486
  "</process>",
478
487
  "",
479
488
  "<rules>",
@@ -512,7 +521,10 @@ export function renderCodexStatusSkill(pinnedVersion) {
512
521
  "Follow this order:",
513
522
  "",
514
523
  `- run \`npx -y @skillcap/gdh@${pinnedVersion} status\` and explain each readiness field`,
524
+ "- distinguish `blockingReasons` (needs /gdh-update or manual intervention) from `coupledReasons` (fixable via /gdh-migrate)",
515
525
  `- check if migration is needed with \`npx -y @skillcap/gdh@${pinnedVersion} migrate\``,
526
+ `- ${PREVIEW_HEADER_LITERAL} — migrate output describes planned actions ("would delete …") not applied ones ("deleted")`,
527
+ "- if status JSON `recoveryHints` array is non-empty, surface each entry's `{command, skillName}` pair",
516
528
  "- surface any degraded or unavailable capabilities",
517
529
  "- suggest the most productive next step based on current state",
518
530
  "</process>",
@@ -551,7 +563,10 @@ export function renderCursorStatusSkill(pinnedVersion) {
551
563
  "Follow this order:",
552
564
  "",
553
565
  `- run \`npx -y @skillcap/gdh@${pinnedVersion} status\` and explain each readiness field`,
566
+ "- distinguish `blockingReasons` (needs /gdh-update or manual intervention) from `coupledReasons` (fixable via /gdh-migrate)",
554
567
  `- check if migration is needed with \`npx -y @skillcap/gdh@${pinnedVersion} migrate\``,
568
+ `- ${PREVIEW_HEADER_LITERAL} — migrate output describes planned actions ("would delete …") not applied ones ("deleted")`,
569
+ "- if status JSON `recoveryHints` array is non-empty, surface each entry's `{command, skillName}` pair",
555
570
  "- surface any degraded or unavailable capabilities",
556
571
  "- suggest the most productive next step based on current state",
557
572
  "</process>",
@@ -695,10 +710,11 @@ export function renderClaudeMigrateCommand(pinnedVersion) {
695
710
  "<process>",
696
711
  "Follow this order:",
697
712
  "",
698
- `1. Run \`npx -y @skillcap/gdh@${pinnedVersion} migrate\` to preview pending migrations.`,
699
- "2. Explain what each migration step will change and why.",
700
- `3. Offer to run \`npx -y @skillcap/gdh@${pinnedVersion} migrate --apply\` if the user approves.`,
701
- `4. After apply, run \`npx -y @skillcap/gdh@${pinnedVersion} status\` to verify the migration succeeded.`,
713
+ `1. Run \`npx -y @skillcap/gdh@${pinnedVersion} migrate\` to preview pending migrations (this is a dry-run; the JSON \`mode\` field will be \`"preview"\`).`,
714
+ "2. Inspect `compatibility.blockingReasons` vs `compatibility.coupledReasons` — if `blockingReasons` is non-empty, STOP: `migrate --apply` cannot fix them (recommend `/gdh-update` or manual intervention per each reason's recovery hint). If only `coupledReasons` is non-empty, proceed.",
715
+ "3. Explain what each migration step will change and why (use \"would\" verb forms — \"would delete\", \"would create\", \"would refresh\").",
716
+ `4. Offer to run \`npx -y @skillcap/gdh@${pinnedVersion} migrate --apply\` if the user approves — after apply, the JSON \`mode\` field becomes \`"applied"\` and verbs switch to past tense ("deleted", "created", "refreshed").`,
717
+ `5. After apply, run \`npx -y @skillcap/gdh@${pinnedVersion} status\` to verify the migration succeeded.`,
702
718
  "</process>",
703
719
  "",
704
720
  "<rules>",
@@ -735,9 +751,10 @@ export function renderCodexMigrateSkill(pinnedVersion) {
735
751
  "<process>",
736
752
  "Follow this order:",
737
753
  "",
738
- `- run \`npx -y @skillcap/gdh@${pinnedVersion} migrate\` to preview pending migrations`,
739
- "- explain what each migration step will change and why",
740
- `- offer to run \`npx -y @skillcap/gdh@${pinnedVersion} migrate --apply\` if the user approves`,
754
+ `- run \`npx -y @skillcap/gdh@${pinnedVersion} migrate\` to preview pending migrations (this is a dry-run; the JSON \`mode\` field will be \`"preview"\`)`,
755
+ "- inspect `compatibility.blockingReasons` vs `compatibility.coupledReasons` — if `blockingReasons` is non-empty, STOP: `migrate --apply` cannot fix them (recommend `/gdh-update` or manual intervention per each reason's recovery hint); if only `coupledReasons` is non-empty, proceed",
756
+ "- explain what each migration step will change and why (use \"would\" verb forms — \"would delete\", \"would create\", \"would refresh\")",
757
+ `- offer to run \`npx -y @skillcap/gdh@${pinnedVersion} migrate --apply\` if the user approves — after apply, the JSON \`mode\` field becomes \`"applied"\` and verbs switch to past tense ("deleted", "created", "refreshed")`,
741
758
  `- after apply, run \`npx -y @skillcap/gdh@${pinnedVersion} status\` to verify the migration succeeded`,
742
759
  "</process>",
743
760
  "",
@@ -773,9 +790,10 @@ export function renderCursorMigrateSkill(pinnedVersion) {
773
790
  "<process>",
774
791
  "Follow this order:",
775
792
  "",
776
- `- run \`npx -y @skillcap/gdh@${pinnedVersion} migrate\` to preview pending migrations`,
777
- "- explain what each migration step will change and why",
778
- `- offer to run \`npx -y @skillcap/gdh@${pinnedVersion} migrate --apply\` if the user approves`,
793
+ `- run \`npx -y @skillcap/gdh@${pinnedVersion} migrate\` to preview pending migrations (this is a dry-run; the JSON \`mode\` field will be \`"preview"\`)`,
794
+ "- inspect `compatibility.blockingReasons` vs `compatibility.coupledReasons` — if `blockingReasons` is non-empty, STOP: `migrate --apply` cannot fix them (recommend `/gdh-update` or manual intervention per each reason's recovery hint); if only `coupledReasons` is non-empty, proceed",
795
+ "- explain what each migration step will change and why (use \"would\" verb forms — \"would delete\", \"would create\", \"would refresh\")",
796
+ `- offer to run \`npx -y @skillcap/gdh@${pinnedVersion} migrate --apply\` if the user approves — after apply, the JSON \`mode\` field becomes \`"applied"\` and verbs switch to past tense ("deleted", "created", "refreshed")`,
779
797
  `- after apply, run \`npx -y @skillcap/gdh@${pinnedVersion} status\` to verify the migration succeeded`,
780
798
  "</process>",
781
799
  "",
@@ -831,6 +849,7 @@ export function renderClaudeUpdateCommand(_pinnedVersion) {
831
849
  "2. Surface the planned version delta and re-bake action count to the human conversationally.",
832
850
  "3. Apply: run `npx -y @skillcap/gdh@latest self-update [version]` (omit `[version]` to apply against npm latest; forward whatever positional the user passed to `/gdh-update`).",
833
851
  "4. Verify: run `npx -y @skillcap/gdh@latest verify drift` to confirm every baked surface matches the new pin.",
852
+ "5. Read the `pendingMigration` field in the self-update stdout JSON. When non-null, narrate the chain to the human and run `npx -y @skillcap/gdh@latest migrate --apply` as the next action; when null, confirm no migration is pending and stop.",
834
853
  "</process>",
835
854
  "",
836
855
  "<rules>",
@@ -868,6 +887,7 @@ export function renderCodexUpdateSkill(_pinnedVersion) {
868
887
  "2. Surface the planned version delta and re-bake action count to the human conversationally.",
869
888
  "3. Apply: run `npx -y @skillcap/gdh@latest self-update [version]` (omit `[version]` to apply against npm latest; forward whatever positional the user passed to `/gdh-update`).",
870
889
  "4. Verify: run `npx -y @skillcap/gdh@latest verify drift` to confirm every baked surface matches the new pin.",
890
+ "5. Read the `pendingMigration` field in the self-update stdout JSON. When non-null, narrate the chain to the human and run `npx -y @skillcap/gdh@latest migrate --apply` as the next action; when null, confirm no migration is pending and stop.",
871
891
  "</process>",
872
892
  "",
873
893
  "<rules>",
@@ -903,6 +923,7 @@ export function renderCursorUpdateSkill(_pinnedVersion) {
903
923
  "2. Surface the planned version delta and re-bake action count to the human conversationally.",
904
924
  "3. Apply: run `npx -y @skillcap/gdh@latest self-update [version]` (omit `[version]` to apply against npm latest; forward whatever positional the user passed to `/gdh-update`).",
905
925
  "4. Verify: run `npx -y @skillcap/gdh@latest verify drift` to confirm every baked surface matches the new pin.",
926
+ "5. Read the `pendingMigration` field in the self-update stdout JSON. When non-null, narrate the chain to the human and run `npx -y @skillcap/gdh@latest migrate --apply` as the next action; when null, confirm no migration is pending and stop.",
906
927
  "</process>",
907
928
  "",
908
929
  "<rules>",
@@ -1257,34 +1278,26 @@ export function renderCursorVerifySkill(pinnedVersion) {
1257
1278
  async function inspectProjectMcpSupport(targetPath, options) {
1258
1279
  const projectConfig = await readProjectConfig(targetPath);
1259
1280
  const enabled = resolveProjectMcpEnabled(projectConfig);
1260
- const launcherContent = options.pinnedVersion === null
1261
- ? null
1262
- : renderManagedMcpLauncher(options.pinnedVersion);
1263
1281
  const managedMcpEntry = buildManagedMcpServerEntry({
1264
- targetPath,
1265
- integrationRootPath: options.integrationRootPath,
1266
- launcherPathForConfig: path.resolve(options.integrationRootPath, MCP_LAUNCHER_RELATIVE_PATH),
1282
+ pinnedVersion: options.pinnedVersion ?? "latest",
1267
1283
  });
1268
- const [projectFile, cursorFile, launcherSource, localPathHints] = await Promise.all([
1284
+ const [projectFile, cursorFile, codexProjectContent, localPathHints] = await Promise.all([
1269
1285
  inspectJsonFile(path.join(options.integrationRootPath, PROJECT_MCP_RELATIVE_PATH)),
1270
1286
  inspectJsonFile(path.join(options.integrationRootPath, CURSOR_MCP_RELATIVE_PATH)),
1271
- fs.readFile(path.join(options.integrationRootPath, MCP_LAUNCHER_RELATIVE_PATH), "utf8").catch(() => null),
1287
+ fs.readFile(path.join(options.integrationRootPath, CODEX_PROJECT_CONFIG_RELATIVE_PATH), "utf8").catch(() => null),
1272
1288
  readLocalPathHints(options.integrationRootPath),
1273
1289
  ]);
1274
- const launcherBootstrap = await inspectLauncherBootstrap(localPathHints);
1275
1290
  const codexConfigPath = path.join(os.homedir(), ".codex/config.toml");
1276
1291
  const codexServerName = projectConfig === null ? null : createCodexServerName(projectConfig.projectKeySeed);
1277
1292
  return {
1278
1293
  enabled,
1279
1294
  integrationRootPath: options.integrationRootPath,
1280
- launcherContent,
1281
1295
  localPathHints,
1282
- launcherBootstrap,
1283
- launcherFile: inspectLauncherFile(launcherSource, launcherContent, enabled, launcherBootstrap),
1284
1296
  projectFile: inspectManagedMcpFile(projectFile, enabled, PROJECT_MCP_RELATIVE_PATH, "Claude project MCP config", managedMcpEntry),
1285
1297
  cursorFile: inspectManagedMcpFile(cursorFile, enabled, CURSOR_MCP_RELATIVE_PATH, "Cursor project MCP config", managedMcpEntry),
1298
+ codexProjectFile: inspectManagedCodexProjectFile(codexProjectContent, enabled, options.pinnedVersion),
1286
1299
  codexRegistration: options.includeUserLocal
1287
- ? await inspectCodexRegistration(targetPath, enabled, codexServerName, codexConfigPath, options.integrationRootPath)
1300
+ ? await inspectCodexRegistration(targetPath, enabled, codexServerName, codexConfigPath, options.integrationRootPath, options.pinnedVersion)
1288
1301
  : {
1289
1302
  present: false,
1290
1303
  state: "ready",
@@ -1361,71 +1374,52 @@ function inspectManagedMcpFile(file, enabled, relativePath, displayName, expecte
1361
1374
  summary: `${displayName} includes the managed \`${GDH_MCP_SERVER_NAME}\` MCP server entry.`,
1362
1375
  };
1363
1376
  }
1364
- function inspectLauncherFile(launcherSource, expectedContent, enabled, bootstrap) {
1377
+ function inspectManagedCodexProjectFile(content, enabled, pinnedVersion) {
1378
+ const relativePath = CODEX_PROJECT_CONFIG_RELATIVE_PATH;
1365
1379
  if (!enabled) {
1366
1380
  return {
1367
- present: launcherSource !== null,
1381
+ present: content !== null,
1368
1382
  state: "ready",
1369
- summary: "The managed GDH MCP launcher is disabled for this target.",
1383
+ summary: "Codex project MCP config is disabled for this target.",
1370
1384
  };
1371
1385
  }
1372
- if (expectedContent === null) {
1386
+ if (pinnedVersion === null) {
1373
1387
  return {
1374
- present: launcherSource !== null,
1388
+ present: content !== null,
1375
1389
  state: "missing",
1376
- summary: "The managed GDH MCP launcher is pending project onboard: no `gdh_version` is pinned yet, so there is no expected launcher content to compare against.",
1390
+ summary: `Codex project MCP config cannot be validated yet: no \`gdh_version\` is pinned (run \`gdh setup\` or \`gdh migrate --apply\`).`,
1377
1391
  };
1378
1392
  }
1379
- if (launcherSource === null) {
1393
+ if (content === null) {
1380
1394
  return {
1381
1395
  present: false,
1382
1396
  state: "missing",
1383
- summary: `The managed GDH MCP launcher is missing and should live at ${MCP_LAUNCHER_RELATIVE_PATH}.`,
1397
+ summary: `Codex project MCP config is missing and should define the managed \`[mcp_servers.${GDH_MCP_SERVER_NAME}]\` entry at ${relativePath}.`,
1384
1398
  };
1385
1399
  }
1386
- if (launcherSource !== expectedContent) {
1400
+ const existingSection = extractManagedCodexSection(content);
1401
+ const expectedSection = renderManagedCodexProjectSection(pinnedVersion);
1402
+ if (existingSection === null) {
1387
1403
  return {
1388
1404
  present: true,
1389
1405
  state: "misconfigured",
1390
- summary: "The managed GDH MCP launcher exists but no longer matches the expected launcher content.",
1406
+ summary: `${relativePath} exists but does not contain the managed \`[mcp_servers.${GDH_MCP_SERVER_NAME}]\` section.`,
1391
1407
  };
1392
1408
  }
1393
- return {
1394
- present: true,
1395
- state: bootstrap.state,
1396
- summary: bootstrap.state === "ready"
1397
- ? "The managed GDH MCP launcher is present and can resolve the current GDH bootstrap path."
1398
- : bootstrap.summary,
1399
- };
1400
- }
1401
- async function inspectLauncherBootstrap(localPathHints) {
1402
- const envDevRepoPath = process.env["GDH_DEV_REPO"]?.trim() || null;
1403
- const effectiveDevRepoPath = envDevRepoPath ?? localPathHints.gdhDevRepoPath;
1404
- if (effectiveDevRepoPath !== null) {
1405
- const cliEntryPath = path.join(effectiveDevRepoPath, "packages/cli/dist/cli.js");
1406
- if (await fileExists(cliEntryPath)) {
1407
- return {
1408
- state: "ready",
1409
- summary: `The managed GDH MCP launcher will bootstrap from the current GDH checkout at ${effectiveDevRepoPath}.`,
1410
- };
1411
- }
1409
+ if (existingSection !== expectedSection) {
1412
1410
  return {
1411
+ present: true,
1413
1412
  state: "misconfigured",
1414
- summary: `The configured GDH checkout at ${effectiveDevRepoPath} does not contain packages/cli/dist/cli.js yet. Run \`corepack yarn build\` in the GDH checkout or update the local path hints.`,
1415
- };
1416
- }
1417
- if (await executableExistsOnPath("gdh")) {
1418
- return {
1419
- state: "ready",
1420
- summary: "The managed GDH MCP launcher can resolve `gdh` from PATH.",
1413
+ summary: `${relativePath} managed \`[mcp_servers.${GDH_MCP_SERVER_NAME}]\` section does not match the expected GDH shape.`,
1421
1414
  };
1422
1415
  }
1423
1416
  return {
1424
- state: "missing",
1425
- summary: "The managed GDH MCP launcher cannot find `gdh` on PATH and no local GDH checkout hint is recorded. Run `gdh adapters install <target> --dev-repo <path-to-gdh>` or install `gdh` on PATH.",
1417
+ present: true,
1418
+ state: "ready",
1419
+ summary: `Codex project MCP config includes the managed \`[mcp_servers.${GDH_MCP_SERVER_NAME}]\` entry.`,
1426
1420
  };
1427
1421
  }
1428
- async function inspectCodexRegistration(targetPath, enabled, codexServerName, codexConfigPath, integrationRootPath) {
1422
+ async function inspectCodexRegistration(targetPath, enabled, codexServerName, codexConfigPath, integrationRootPath, pinnedVersion) {
1429
1423
  if (!enabled || codexServerName === null) {
1430
1424
  return {
1431
1425
  present: false,
@@ -1449,16 +1443,22 @@ async function inspectCodexRegistration(targetPath, enabled, codexServerName, co
1449
1443
  summary: `Codex does not yet have the expected user-local MCP registration \`${codexServerName}\` in ${codexConfigPath}.`,
1450
1444
  };
1451
1445
  }
1452
- const expectedLauncherPath = path.join(integrationRootPath, MCP_LAUNCHER_RELATIVE_PATH);
1446
+ if (pinnedVersion === null) {
1447
+ return {
1448
+ present: true,
1449
+ state: "misconfigured",
1450
+ summary: `Codex MCP registration \`${codexServerName}\` exists but no \`gdh_version\` is pinned; cannot validate the expected npx invocation.`,
1451
+ };
1452
+ }
1453
1453
  if (!isMatchingCodexRegistration(registration, {
1454
1454
  targetPath,
1455
1455
  integrationRootPath,
1456
- launcherPath: expectedLauncherPath,
1456
+ pinnedVersion,
1457
1457
  })) {
1458
1458
  return {
1459
1459
  present: true,
1460
1460
  state: "misconfigured",
1461
- summary: `Codex MCP registration \`${codexServerName}\` exists but does not point at the managed GDH launcher ${expectedLauncherPath}.`,
1461
+ summary: `Codex MCP registration \`${codexServerName}\` exists but does not match the expected \`npx -y @skillcap/gdh@${pinnedVersion} mcp serve\` invocation.`,
1462
1462
  };
1463
1463
  }
1464
1464
  return {
@@ -1648,14 +1648,14 @@ async function inspectCodexAdapter(targetPath, guidance, projectMcp, pinnedVersi
1648
1648
  ];
1649
1649
  if (projectMcp.enabled) {
1650
1650
  surfaces.push(createSurfaceStatus({
1651
- kind: "launcher_file",
1651
+ kind: "mcp_file",
1652
1652
  scope: "repo",
1653
1653
  targetPath: projectMcp.integrationRootPath,
1654
- relativePath: MCP_LAUNCHER_RELATIVE_PATH,
1655
- present: projectMcp.launcherFile.present,
1656
- state: projectMcp.launcherFile.state,
1657
- summary: projectMcp.launcherFile.summary,
1658
- version: GDH_MCP_LAUNCHER_VERSION,
1654
+ relativePath: CODEX_PROJECT_CONFIG_RELATIVE_PATH,
1655
+ present: projectMcp.codexProjectFile.present,
1656
+ state: projectMcp.codexProjectFile.state,
1657
+ summary: projectMcp.codexProjectFile.summary,
1658
+ version: null,
1659
1659
  }));
1660
1660
  if (options.includeUserLocal) {
1661
1661
  surfaces.push(createSurfaceStatus({
@@ -1763,15 +1763,6 @@ async function inspectClaudeAdapter(targetPath, guidance, projectMcp, pinnedVers
1763
1763
  ];
1764
1764
  if (projectMcp.enabled) {
1765
1765
  surfaces.push(createSurfaceStatus({
1766
- kind: "launcher_file",
1767
- scope: "repo",
1768
- targetPath: projectMcp.integrationRootPath,
1769
- relativePath: MCP_LAUNCHER_RELATIVE_PATH,
1770
- present: projectMcp.launcherFile.present,
1771
- state: projectMcp.launcherFile.state,
1772
- summary: projectMcp.launcherFile.summary,
1773
- version: GDH_MCP_LAUNCHER_VERSION,
1774
- }), createSurfaceStatus({
1775
1766
  kind: "mcp_file",
1776
1767
  scope: "repo",
1777
1768
  targetPath: projectMcp.integrationRootPath,
@@ -1863,15 +1854,6 @@ async function inspectCursorAdapter(targetPath, guidance, projectMcp, pinnedVers
1863
1854
  ];
1864
1855
  if (projectMcp.enabled) {
1865
1856
  surfaces.push(createSurfaceStatus({
1866
- kind: "launcher_file",
1867
- scope: "repo",
1868
- targetPath: projectMcp.integrationRootPath,
1869
- relativePath: MCP_LAUNCHER_RELATIVE_PATH,
1870
- present: projectMcp.launcherFile.present,
1871
- state: projectMcp.launcherFile.state,
1872
- summary: projectMcp.launcherFile.summary,
1873
- version: GDH_MCP_LAUNCHER_VERSION,
1874
- }), createSurfaceStatus({
1875
1857
  kind: "mcp_file",
1876
1858
  scope: "repo",
1877
1859
  targetPath: projectMcp.integrationRootPath,
@@ -1955,17 +1937,17 @@ async function planInstallActions(targetPath, adapters, requestedAgents, options
1955
1937
  const effectiveDevRepoPath = options.devRepoPath ?? resolveCurrentGdhInstall(import.meta.url).defaultDevRepoPath;
1956
1938
  if (options.user) {
1957
1939
  if (requestedAgents.includes("codex")) {
1958
- actions.push(...planCodexUserInstallActions(targetPath, projectMcp, options.integrationRootPath));
1940
+ actions.push(...planCodexUserInstallActions(targetPath, projectMcp, options.integrationRootPath, options.pinnedVersion));
1959
1941
  }
1960
1942
  return dedupeInstallActions(actions);
1961
1943
  }
1962
- actions.push(...planSharedRepoInstallActions(targetPath, projectMcp, requestedAgents[0] ?? "claude", effectiveDevRepoPath, options.integrationRootPath));
1944
+ actions.push(...planSharedRepoInstallActions(targetPath, projectMcp, requestedAgents[0] ?? "claude", effectiveDevRepoPath, options.integrationRootPath, options.pinnedVersion));
1963
1945
  for (const adapter of adapters) {
1964
1946
  if (!requestedAgents.includes(adapter.agent)) {
1965
1947
  continue;
1966
1948
  }
1967
1949
  if (adapter.agent === "codex") {
1968
- actions.push(...planCodexRepoInstallActions(targetPath, adapter, options.pinnedVersion));
1950
+ actions.push(...planCodexRepoInstallActions(targetPath, adapter, options.pinnedVersion, projectMcp, options.integrationRootPath));
1969
1951
  continue;
1970
1952
  }
1971
1953
  if (adapter.agent === "claude") {
@@ -1978,27 +1960,11 @@ async function planInstallActions(targetPath, adapters, requestedAgents, options
1978
1960
  }
1979
1961
  return dedupeInstallActions(actions);
1980
1962
  }
1981
- function planSharedRepoInstallActions(targetPath, projectMcp, agent, effectiveDevRepoPath, integrationRootPath) {
1963
+ function planSharedRepoInstallActions(targetPath, projectMcp, agent, effectiveDevRepoPath, integrationRootPath, pinnedVersion) {
1982
1964
  if (!projectMcp.enabled) {
1983
1965
  return [];
1984
1966
  }
1985
1967
  const actions = [];
1986
- if (projectMcp.launcherFile.state !== "ready") {
1987
- actions.push(createInstallAction({
1988
- agent,
1989
- kind: "write_file",
1990
- scope: "repo",
1991
- targetPath: integrationRootPath,
1992
- relativePath: MCP_LAUNCHER_RELATIVE_PATH,
1993
- state: "planned",
1994
- mode: projectMcp.launcherFile.present ? "replace" : "create",
1995
- summary: projectMcp.launcherFile.present
1996
- ? "Replace the managed GDH MCP launcher with the current launcher content."
1997
- : "Create the managed GDH MCP launcher under .gdh/bin/.",
1998
- version: GDH_MCP_LAUNCHER_VERSION,
1999
- content: projectMcp.launcherContent,
2000
- }));
2001
- }
2002
1968
  if (projectMcp.projectFile.state !== "ready") {
2003
1969
  actions.push(createInstallAction({
2004
1970
  agent,
@@ -2011,11 +1977,7 @@ function planSharedRepoInstallActions(targetPath, projectMcp, agent, effectiveDe
2011
1977
  summary: projectMcp.projectFile.present
2012
1978
  ? "Replace the managed `gdh` MCP entry in .mcp.json while preserving any non-GDH servers."
2013
1979
  : "Create .mcp.json with the managed `gdh` MCP entry.",
2014
- content: renderManagedMcpConfig(path.join(integrationRootPath, PROJECT_MCP_RELATIVE_PATH), buildManagedMcpServerEntry({
2015
- targetPath,
2016
- integrationRootPath,
2017
- launcherPathForConfig: path.resolve(integrationRootPath, MCP_LAUNCHER_RELATIVE_PATH),
2018
- })),
1980
+ content: renderManagedMcpConfig(path.join(integrationRootPath, PROJECT_MCP_RELATIVE_PATH), buildManagedMcpServerEntry({ pinnedVersion })),
2019
1981
  }));
2020
1982
  }
2021
1983
  if (projectMcp.cursorFile.state !== "ready") {
@@ -2030,11 +1992,7 @@ function planSharedRepoInstallActions(targetPath, projectMcp, agent, effectiveDe
2030
1992
  summary: projectMcp.cursorFile.present
2031
1993
  ? "Replace the managed `gdh` MCP entry in .cursor/mcp.json while preserving any non-GDH servers."
2032
1994
  : "Create .cursor/mcp.json with the managed `gdh` MCP entry.",
2033
- content: renderManagedMcpConfig(path.join(integrationRootPath, CURSOR_MCP_RELATIVE_PATH), buildManagedMcpServerEntry({
2034
- targetPath,
2035
- integrationRootPath,
2036
- launcherPathForConfig: path.resolve(integrationRootPath, MCP_LAUNCHER_RELATIVE_PATH),
2037
- })),
1995
+ content: renderManagedMcpConfig(path.join(integrationRootPath, CURSOR_MCP_RELATIVE_PATH), buildManagedMcpServerEntry({ pinnedVersion })),
2038
1996
  }));
2039
1997
  }
2040
1998
  if (projectMcp.localPathHints.gdhDevRepoPath !== effectiveDevRepoPath) {
@@ -2104,8 +2062,8 @@ function agentLabel(agent) {
2104
2062
  return "Cursor";
2105
2063
  }
2106
2064
  }
2107
- function planCodexRepoInstallActions(targetPath, adapter, pinnedVersion) {
2108
- return [
2065
+ function planCodexRepoInstallActions(targetPath, adapter, pinnedVersion, projectMcp, integrationRootPath) {
2066
+ const actions = [
2109
2067
  planSkillInstallAction("codex", targetPath, adapter, CODEX_ONBOARD_SKILL_RELATIVE_PATH, renderCodexOnboardSkill, "gdh-onboard", pinnedVersion),
2110
2068
  planSkillInstallAction("codex", targetPath, adapter, CODEX_STATUS_SKILL_RELATIVE_PATH, renderCodexStatusSkill, "gdh-status", pinnedVersion),
2111
2069
  planSkillInstallAction("codex", targetPath, adapter, CODEX_MIGRATE_SKILL_RELATIVE_PATH, renderCodexMigrateSkill, "gdh-migrate", pinnedVersion),
@@ -2115,18 +2073,42 @@ function planCodexRepoInstallActions(targetPath, adapter, pinnedVersion) {
2115
2073
  planSkillInstallAction("codex", targetPath, adapter, CODEX_VERIFY_SKILL_RELATIVE_PATH, renderCodexVerifySkill, "gdh-verify", pinnedVersion),
2116
2074
  planSkillInstallAction("codex", targetPath, adapter, CODEX_SCAN_SKILL_RELATIVE_PATH, renderCodexScanSkill, "gdh-scan", pinnedVersion),
2117
2075
  ];
2076
+ if (projectMcp.enabled && projectMcp.codexProjectFile.state !== "ready") {
2077
+ const absolutePath = path.join(integrationRootPath, CODEX_PROJECT_CONFIG_RELATIVE_PATH);
2078
+ const existingContent = fsSync.existsSync(absolutePath)
2079
+ ? fsSync.readFileSync(absolutePath, "utf8")
2080
+ : null;
2081
+ actions.push(createInstallAction({
2082
+ agent: "codex",
2083
+ kind: "write_file",
2084
+ scope: "repo",
2085
+ targetPath: integrationRootPath,
2086
+ relativePath: CODEX_PROJECT_CONFIG_RELATIVE_PATH,
2087
+ state: "planned",
2088
+ mode: projectMcp.codexProjectFile.present ? "replace" : "create",
2089
+ summary: projectMcp.codexProjectFile.present
2090
+ ? `Replace the managed \`[mcp_servers.${GDH_MCP_SERVER_NAME}]\` section in .codex/config.toml while preserving any non-GDH content.`
2091
+ : `Create .codex/config.toml with the managed \`[mcp_servers.${GDH_MCP_SERVER_NAME}]\` entry so Codex can discover the project MCP server without a user-global registration.`,
2092
+ content: renderManagedCodexProjectConfig(existingContent, pinnedVersion),
2093
+ }));
2094
+ }
2095
+ return actions;
2118
2096
  }
2119
- function planCodexUserInstallActions(targetPath, projectMcp, integrationRootPath) {
2097
+ function planCodexUserInstallActions(targetPath, projectMcp, integrationRootPath, pinnedVersion) {
2120
2098
  if (!projectMcp.enabled || projectMcp.codexServerName === null) {
2121
2099
  return [];
2122
2100
  }
2123
- const expectedLauncherPath = path.join(integrationRootPath, MCP_LAUNCHER_RELATIVE_PATH);
2124
- const codexArgs = buildManagedLauncherInvocationArgs({
2125
- targetPath,
2126
- integrationRootPath,
2127
- launcherPath: expectedLauncherPath,
2128
- useAbsoluteTargetPath: true,
2129
- });
2101
+ const resolvedTargetPath = path.resolve(targetPath);
2102
+ const resolvedIntegrationRootPath = path.resolve(integrationRootPath);
2103
+ const codexCommandArgs = [
2104
+ "-y",
2105
+ `@skillcap/gdh@${pinnedVersion}`,
2106
+ "mcp",
2107
+ "serve",
2108
+ ];
2109
+ if (resolvedTargetPath !== resolvedIntegrationRootPath) {
2110
+ codexCommandArgs.push("--target", resolvedTargetPath);
2111
+ }
2130
2112
  const actions = [];
2131
2113
  if (projectMcp.codexRegistration.present) {
2132
2114
  actions.push(createInstallAction({
@@ -2138,7 +2120,7 @@ function planCodexUserInstallActions(targetPath, projectMcp, integrationRootPath
2138
2120
  absolutePath: projectMcp.codexConfigPath,
2139
2121
  state: "planned",
2140
2122
  mode: "replace",
2141
- summary: `Remove the existing Codex MCP registration \`${projectMcp.codexServerName}\` before re-registering it with the managed GDH launcher.`,
2123
+ summary: `Remove the existing Codex MCP registration \`${projectMcp.codexServerName}\` before re-registering it with the managed GDH npx invocation.`,
2142
2124
  command: ["codex", "mcp", "remove", projectMcp.codexServerName],
2143
2125
  }));
2144
2126
  }
@@ -2151,15 +2133,15 @@ function planCodexUserInstallActions(targetPath, projectMcp, integrationRootPath
2151
2133
  absolutePath: projectMcp.codexConfigPath,
2152
2134
  state: "planned",
2153
2135
  mode: projectMcp.codexRegistration.present ? "replace" : "create",
2154
- summary: `Register the managed GDH launcher with Codex as \`${projectMcp.codexServerName}\`.`,
2136
+ summary: `Register the managed GDH MCP server with Codex as \`${projectMcp.codexServerName}\` via pinned npx invocation.`,
2155
2137
  command: [
2156
2138
  "codex",
2157
2139
  "mcp",
2158
2140
  "add",
2159
2141
  projectMcp.codexServerName,
2160
2142
  "--",
2161
- "node",
2162
- ...codexArgs,
2143
+ "npx",
2144
+ ...codexCommandArgs,
2163
2145
  ],
2164
2146
  }));
2165
2147
  return actions;
@@ -2377,64 +2359,11 @@ function resolveProjectMcpEnabled(projectConfig) {
2377
2359
  function createCodexServerName(projectKeySeed) {
2378
2360
  return `gdh-${projectKeySeed}`;
2379
2361
  }
2380
- export function renderManagedMcpLauncher(pinnedVersion) {
2381
- return [
2382
- "#!/usr/bin/env node",
2383
- `// GDH MCP launcher v${GDH_MCP_LAUNCHER_VERSION}`,
2384
- 'import fs from "node:fs";',
2385
- 'import path from "node:path";',
2386
- 'import { spawnSync } from "node:child_process";',
2387
- 'import { fileURLToPath } from "node:url";',
2388
- "",
2389
- "const launcherPath = fileURLToPath(import.meta.url);",
2390
- 'const integrationRootPath = path.resolve(path.dirname(launcherPath), "../..");',
2391
- 'const targetOptionIndex = process.argv.indexOf("--target");',
2392
- "const configuredTargetPath =",
2393
- ' targetOptionIndex >= 0 && typeof process.argv[targetOptionIndex + 1] === "string"',
2394
- " ? process.argv[targetOptionIndex + 1]",
2395
- " : null;",
2396
- "const targetPath = configuredTargetPath",
2397
- " ? path.resolve(integrationRootPath, configuredTargetPath)",
2398
- " : integrationRootPath;",
2399
- 'const hintsPath = path.join(integrationRootPath, ".gdh-state", "local-paths.json");',
2400
- "",
2401
- "let gdhDevRepoPath = null;",
2402
- "try {",
2403
- ' const raw = fs.readFileSync(hintsPath, "utf8");',
2404
- " const parsed = JSON.parse(raw);",
2405
- ' if (typeof parsed.gdhDevRepoPath === "string" && parsed.gdhDevRepoPath.length > 0) {',
2406
- " gdhDevRepoPath = parsed.gdhDevRepoPath;",
2407
- " }",
2408
- "} catch {}",
2409
- "",
2410
- "const effectiveDevRepoPath = process.env.GDH_DEV_REPO?.trim() || gdhDevRepoPath;",
2411
- "if (effectiveDevRepoPath) {",
2412
- ' const devCliEntryPath = path.join(effectiveDevRepoPath, "packages/cli/dist/cli.js");',
2413
- " if (fs.existsSync(devCliEntryPath)) {",
2414
- ' const result = spawnSync(process.execPath, [devCliEntryPath, "mcp", "serve", "--target", targetPath], { stdio: "inherit" });',
2415
- " process.exit(result.status ?? 1);",
2416
- " }",
2417
- "}",
2418
- "",
2419
- `const result = spawnSync("npx", ["-y", "@skillcap/gdh@${pinnedVersion}", "mcp", "serve", "--target", targetPath], { stdio: "inherit", cwd: targetPath });`,
2420
- 'if (result.error && result.error.code === "ENOENT") {',
2421
- ` console.error("GDH MCP launcher could not launch npx for @skillcap/gdh@${pinnedVersion}. Ensure Node.js 20+ is installed (npx ships with Node), or configure the contributor dev escape hatch via the GDH_DEV_REPO env var or .gdh-state/local-paths.json gdhDevRepoPath.");`,
2422
- " process.exit(1);",
2423
- "}",
2424
- "process.exit(result.status ?? 1);",
2425
- "",
2426
- ].join("\n");
2427
- }
2428
2362
  function buildManagedMcpServerEntry(input) {
2429
2363
  return {
2430
2364
  type: "stdio",
2431
- command: "node",
2432
- args: buildManagedLauncherInvocationArgs({
2433
- targetPath: input.targetPath,
2434
- integrationRootPath: input.integrationRootPath,
2435
- launcherPath: input.launcherPathForConfig,
2436
- useAbsoluteTargetPath: true,
2437
- }),
2365
+ command: "npx",
2366
+ args: ["-y", `@skillcap/gdh@${input.pinnedVersion}`, "mcp", "serve"],
2438
2367
  };
2439
2368
  }
2440
2369
  function getManagedMcpServerEntry(jsonObject) {
@@ -2475,6 +2404,64 @@ function readExistingMcpConfig(absolutePath) {
2475
2404
  return {};
2476
2405
  }
2477
2406
  }
2407
+ // Renders the managed [mcp_servers.gdh] section for Codex project-local
2408
+ // config. The section body is line-based (no TOML parser) — preserves any
2409
+ // other content the user has in the file via regex replacement of the
2410
+ // bracketed header span. Exotic forms (inline-table `mcp_servers.gdh = {...}`
2411
+ // or dotted-key `mcp_servers.gdh.command = "..."`) are not detected; users
2412
+ // with those forms may end up with a duplicate and must migrate manually.
2413
+ export function renderManagedCodexProjectSection(pinnedVersion) {
2414
+ return [
2415
+ "[mcp_servers.gdh]",
2416
+ 'command = "npx"',
2417
+ `args = ["-y", "@skillcap/gdh@${pinnedVersion}", "mcp", "serve"]`,
2418
+ ].join("\n");
2419
+ }
2420
+ export function renderManagedCodexProjectConfig(existingContent, pinnedVersion) {
2421
+ const section = renderManagedCodexProjectSection(pinnedVersion);
2422
+ if (existingContent === null || existingContent.trim() === "") {
2423
+ return `${section}\n`;
2424
+ }
2425
+ const lines = existingContent.split("\n");
2426
+ const sectionStart = lines.findIndex((line) => /^\[mcp_servers\.gdh\]\s*$/.test(line));
2427
+ if (sectionStart === -1) {
2428
+ const trailingNewline = existingContent.endsWith("\n") ? "" : "\n";
2429
+ const separator = existingContent.length > 0 ? "\n" : "";
2430
+ return `${existingContent}${trailingNewline}${separator}${section}\n`;
2431
+ }
2432
+ let sectionEnd = lines.length;
2433
+ for (let i = sectionStart + 1; i < lines.length; i++) {
2434
+ if (/^\[/.test(lines[i] ?? "")) {
2435
+ sectionEnd = i;
2436
+ break;
2437
+ }
2438
+ }
2439
+ const before = lines.slice(0, sectionStart).join("\n");
2440
+ const afterLines = lines.slice(sectionEnd);
2441
+ const after = afterLines.join("\n");
2442
+ const beforeJoin = before.length > 0 ? `${before}\n` : "";
2443
+ const afterNonEmpty = after.replace(/^\s+/, "");
2444
+ const afterJoin = afterNonEmpty.length > 0 ? `\n\n${afterNonEmpty}` : "\n";
2445
+ return `${beforeJoin}${section}${afterJoin}`;
2446
+ }
2447
+ function extractManagedCodexSection(content) {
2448
+ const lines = content.split("\n");
2449
+ const start = lines.findIndex((line) => /^\[mcp_servers\.gdh\]\s*$/.test(line));
2450
+ if (start === -1) {
2451
+ return null;
2452
+ }
2453
+ let end = lines.length;
2454
+ for (let i = start + 1; i < lines.length; i++) {
2455
+ if (/^\[/.test(lines[i] ?? "")) {
2456
+ end = i;
2457
+ break;
2458
+ }
2459
+ }
2460
+ return lines
2461
+ .slice(start, end)
2462
+ .join("\n")
2463
+ .replace(/\s+$/, "");
2464
+ }
2478
2465
  function normalizeJson(value) {
2479
2466
  if (Array.isArray(value)) {
2480
2467
  return value.map((entry) => normalizeJson(entry));
@@ -2614,14 +2601,19 @@ async function listCodexMcpServers() {
2614
2601
  }
2615
2602
  }
2616
2603
  function isMatchingCodexRegistration(registration, input) {
2617
- const expectedArgs = buildManagedLauncherInvocationArgs({
2618
- targetPath: input.targetPath,
2619
- integrationRootPath: input.integrationRootPath,
2620
- launcherPath: input.launcherPath,
2621
- useAbsoluteTargetPath: true,
2622
- });
2604
+ const resolvedTargetPath = path.resolve(input.targetPath);
2605
+ const resolvedIntegrationRootPath = path.resolve(input.integrationRootPath);
2606
+ const expectedArgs = [
2607
+ "-y",
2608
+ `@skillcap/gdh@${input.pinnedVersion}`,
2609
+ "mcp",
2610
+ "serve",
2611
+ ];
2612
+ if (resolvedTargetPath !== resolvedIntegrationRootPath) {
2613
+ expectedArgs.push("--target", resolvedTargetPath);
2614
+ }
2623
2615
  return (registration.transport?.type === "stdio" &&
2624
- registration.transport.command === "node" &&
2616
+ registration.transport.command === "npx" &&
2625
2617
  JSON.stringify(registration.transport.args ?? []) === JSON.stringify(expectedArgs));
2626
2618
  }
2627
2619
  async function inspectProjectConfigLifecycleSurface(targetPath) {
@@ -3054,60 +3046,6 @@ function inspectCursorRuleLifecycleSurface(targetPath, adapterStatus) {
3054
3046
  },
3055
3047
  });
3056
3048
  }
3057
- function inspectMcpLauncherLifecycleSurface(targetPath, mcpEnabled, adapterStatus) {
3058
- if (!mcpEnabled) {
3059
- return createLifecycleSurfaceStatus({
3060
- surface: "mcp_launcher",
3061
- management: "managed",
3062
- state: "compatible",
3063
- summary: "Project-scoped MCP launcher is not required while MCP is disabled.",
3064
- reasons: [],
3065
- probes: [],
3066
- action: null,
3067
- });
3068
- }
3069
- const launcherSurface = findAdapterSurface(adapterStatus, "claude", "launcher_file") ??
3070
- findAdapterSurface(adapterStatus, "cursor", "launcher_file") ??
3071
- findAdapterSurface(adapterStatus, "codex", "launcher_file");
3072
- const probes = launcherSurface === null
3073
- ? []
3074
- : [
3075
- createVersionProbe({
3076
- targetPath,
3077
- relativePath: launcherSurface.relativePath ?? MCP_LAUNCHER_RELATIVE_PATH,
3078
- present: launcherSurface.present,
3079
- expectedVersion: GDH_MCP_LAUNCHER_VERSION,
3080
- detectedVersion: launcherSurface.present ? launcherSurface.version : null,
3081
- }),
3082
- ];
3083
- if (launcherSurface?.state === "ready") {
3084
- return createLifecycleSurfaceStatus({
3085
- surface: "mcp_launcher",
3086
- management: "managed",
3087
- state: "compatible",
3088
- summary: "Managed MCP launcher file matches the current GDH version.",
3089
- reasons: [],
3090
- probes,
3091
- action: null,
3092
- });
3093
- }
3094
- return createLifecycleSurfaceStatus({
3095
- surface: "mcp_launcher",
3096
- management: "managed",
3097
- state: launcherSurface?.present ? "migration_available" : "migration_needed",
3098
- summary: "Managed MCP launcher file needs to be created or refreshed through the adapter install flow.",
3099
- reasons: launcherSurface?.present
3100
- ? ["mcp_launcher_misconfigured"]
3101
- : ["mcp_launcher_missing"],
3102
- probes,
3103
- action: {
3104
- kind: "run_repair",
3105
- summary: "Run GDH migrate or adapters install to refresh the managed MCP launcher file.",
3106
- command: ["gdh", "adapters", "install", targetPath],
3107
- autoApplicable: true,
3108
- },
3109
- });
3110
- }
3111
3049
  function inspectRuntimeBridgeLifecycleSurface(targetPath, bridgeStatus) {
3112
3050
  const probes = bridgeStatus.managedArtifacts.map((artifact) => createVersionProbe({
3113
3051
  targetPath,
@@ -3366,14 +3304,35 @@ async function inspectYamlSurfaceDirectory(input) {
3366
3304
  }
3367
3305
  function summarizeProjectLifecycleCompatibility(targetPath, surfaces) {
3368
3306
  const state = resolveProjectLifecycleState(surfaces);
3369
- const reasons = dedupe(surfaces.flatMap((surface) => surface.reasons).filter((reason) => reason.length > 0));
3370
- return {
3307
+ const blocking = [];
3308
+ const coupled = [];
3309
+ for (const surface of surfaces) {
3310
+ const bucket = surface.state === "migration_blocked" || surface.state === "compatibility_degraded"
3311
+ ? blocking
3312
+ : surface.state === "migration_needed" || surface.state === "migration_available"
3313
+ ? coupled
3314
+ : null;
3315
+ if (bucket) {
3316
+ for (const reason of surface.reasons) {
3317
+ if (reason.length > 0)
3318
+ bucket.push(reason);
3319
+ }
3320
+ }
3321
+ }
3322
+ const blockingReasons = dedupe(blocking);
3323
+ const coupledReasons = dedupe(coupled.filter((r) => !blockingReasons.includes(r)));
3324
+ const reasons = [...blockingReasons, ...coupledReasons];
3325
+ const result = {
3371
3326
  targetPath,
3372
3327
  state,
3373
3328
  summary: summarizeProjectLifecycleState(state, surfaces),
3374
3329
  reasons,
3330
+ blockingReasons,
3331
+ coupledReasons,
3375
3332
  surfaces,
3376
3333
  };
3334
+ assertProjectLifecycleCompatibilityInvariant(result);
3335
+ return result;
3377
3336
  }
3378
3337
  function resolveProjectLifecycleState(surfaces) {
3379
3338
  if (surfaces.some((surface) => surface.state === "migration_blocked")) {
@@ -3501,4 +3460,9 @@ function normalizeChangedFiles(files) {
3501
3460
  export { bumpAndRebakePin, } from "./self-update-mechanics.js";
3502
3461
  export { CLAUDE_CHECK_UPDATE_HOOK_RELATIVE_PATH } from "./claude-update-hook-render.js";
3503
3462
  export { CLAUDE_STATUSLINE_RELATIVE_PATH } from "./claude-statusline-render.js";
3463
+ // Internal aggregator exposed for Wave 0 unit tests
3464
+ // (packages/adapters/src/lifecycle-compatibility.test.ts). Callers outside this
3465
+ // package should continue to use inspectProjectLifecycleCompatibility, which
3466
+ // wraps this function with filesystem probing and surface discovery.
3467
+ export { summarizeProjectLifecycleCompatibility };
3504
3468
  //# sourceMappingURL=index.js.map