@gajae-code/coding-agent 0.13.3 → 0.14.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 (599) hide show
  1. package/CHANGELOG.md +184 -2
  2. package/README.md +2 -0
  3. package/bin/gajaessi.js +4 -0
  4. package/dist/types/async/job-manager.d.ts +55 -2
  5. package/dist/types/async/support.d.ts +14 -0
  6. package/dist/types/capability/mcp.d.ts +6 -0
  7. package/dist/types/capability/types.d.ts +6 -0
  8. package/dist/types/cli/accounts-cli.d.ts +19 -0
  9. package/dist/types/cli/args.d.ts +3 -0
  10. package/dist/types/cli/config-cli.d.ts +6 -0
  11. package/dist/types/cli/crash-cli.d.ts +10 -0
  12. package/dist/types/cli/customize-doctor.d.ts +145 -0
  13. package/dist/types/cli/daemon-cli.d.ts +3 -1
  14. package/dist/types/cli/mcp-cli.d.ts +11 -0
  15. package/dist/types/cli/notify-cli.d.ts +1 -1
  16. package/dist/types/cli/root-flags.d.ts +12 -0
  17. package/dist/types/cli/setup-cli.d.ts +3 -1
  18. package/dist/types/cli/skills-cli.d.ts +2 -1
  19. package/dist/types/cli/update-cli.d.ts +19 -1
  20. package/dist/types/cli.d.ts +6 -0
  21. package/dist/types/commands/accounts.d.ts +40 -0
  22. package/dist/types/commands/crash.d.ts +24 -0
  23. package/dist/types/commands/customize.d.ts +22 -0
  24. package/dist/types/commands/daemon.d.ts +1 -1
  25. package/dist/types/commands/launch.d.ts +6 -0
  26. package/dist/types/commands/setup.d.ts +6 -0
  27. package/dist/types/commands/skills.d.ts +6 -1
  28. package/dist/types/commands/update.d.ts +7 -0
  29. package/dist/types/config/atomic-yaml-patch-worker.d.ts +25 -0
  30. package/dist/types/config/atomic-yaml-patch.d.ts +62 -2
  31. package/dist/types/config/config-schema-version.d.ts +10 -0
  32. package/dist/types/config/file-lock.d.ts +18 -0
  33. package/dist/types/config/model-profile-activation.d.ts +3 -1
  34. package/dist/types/config/model-registry.d.ts +9 -1
  35. package/dist/types/config/models-config-schema.d.ts +2 -0
  36. package/dist/types/config/settings-schema.d.ts +70 -6
  37. package/dist/types/config/settings.d.ts +63 -0
  38. package/dist/types/config/skill-settings-defaults.d.ts +26 -0
  39. package/dist/types/config/task-delegation.d.ts +39 -0
  40. package/dist/types/config/update-channel.d.ts +10 -0
  41. package/dist/types/coordinator-mcp/server.d.ts +5 -1
  42. package/dist/types/crash/index-store.d.ts +87 -0
  43. package/dist/types/crash/nudge.d.ts +50 -0
  44. package/dist/types/crash/record-loader.d.ts +17 -0
  45. package/dist/types/crash/report.d.ts +134 -0
  46. package/dist/types/crash/sanitize.d.ts +25 -0
  47. package/dist/types/cursor.d.ts +13 -2
  48. package/dist/types/customization/import.d.ts +37 -0
  49. package/dist/types/customization/index.d.ts +4 -0
  50. package/dist/types/customization/inventory.d.ts +25 -0
  51. package/dist/types/customization/mutations.d.ts +32 -0
  52. package/dist/types/customization/types.d.ts +115 -0
  53. package/dist/types/daemon/control-types.d.ts +8 -1
  54. package/dist/types/daemon/operator-contract.d.ts +4 -4
  55. package/dist/types/discovery/claude.d.ts +39 -1
  56. package/dist/types/discovery/codex.d.ts +39 -1
  57. package/dist/types/discovery/index.d.ts +2 -0
  58. package/dist/types/discovery/mcp-compat.d.ts +30 -0
  59. package/dist/types/edit/renderer.d.ts +23 -0
  60. package/dist/types/extensibility/extensions/runner.d.ts +3 -0
  61. package/dist/types/extensibility/extensions/types.d.ts +21 -4
  62. package/dist/types/extensibility/gjc-plugins/observability.d.ts +6 -1
  63. package/dist/types/extensibility/gjc-plugins/registry.d.ts +8 -1
  64. package/dist/types/extensibility/hooks/loader.d.ts +19 -5
  65. package/dist/types/extensibility/runtime-skill-discovery.d.ts +22 -1
  66. package/dist/types/extensibility/skill-management.d.ts +107 -0
  67. package/dist/types/gjc-runtime/deep-interview-runtime.d.ts +3 -1
  68. package/dist/types/gjc-runtime/launch-worktree.d.ts +8 -0
  69. package/dist/types/gjc-runtime/linux-proc.d.ts +3 -0
  70. package/dist/types/gjc-runtime/ralplan-runtime.d.ts +10 -6
  71. package/dist/types/gjc-runtime/session-state-lock.d.ts +104 -0
  72. package/dist/types/gjc-runtime/session-state-sidecar.d.ts +120 -2
  73. package/dist/types/gjc-runtime/state-writer.d.ts +10 -1
  74. package/dist/types/gjc-runtime/ultragoal-guard.d.ts +4 -2
  75. package/dist/types/gjc-runtime/ultragoal-runtime.d.ts +8 -6
  76. package/dist/types/gjc-runtime/workflow-settings.d.ts +109 -0
  77. package/dist/types/hooks/events.d.ts +60 -0
  78. package/dist/types/hooks/normalize.d.ts +83 -0
  79. package/dist/types/hooks/skill-state.d.ts +9 -2
  80. package/dist/types/main.d.ts +1 -1
  81. package/dist/types/modes/acp/abort-scope.d.ts +13 -0
  82. package/dist/types/modes/acp/acp-agent.d.ts +7 -4
  83. package/dist/types/modes/components/customization/customization-dashboard.d.ts +44 -0
  84. package/dist/types/modes/components/customization/import-wizard.d.ts +21 -0
  85. package/dist/types/modes/components/customization/index.d.ts +1 -0
  86. package/dist/types/modes/components/gajae-pet-widget.d.ts +8 -30
  87. package/dist/types/modes/components/irc-sidebar.d.ts +1 -0
  88. package/dist/types/modes/components/iterm-pet-transport.d.ts +122 -0
  89. package/dist/types/modes/components/oauth-selector.d.ts +10 -4
  90. package/dist/types/modes/components/pet-capability.d.ts +11 -25
  91. package/dist/types/modes/components/status-line/gh.d.ts +6 -2
  92. package/dist/types/modes/controllers/extension-ui-controller.d.ts +1 -1
  93. package/dist/types/modes/controllers/selector-controller.d.ts +7 -8
  94. package/dist/types/modes/controllers/stt-controller.d.ts +1 -1
  95. package/dist/types/modes/interactive-mode.d.ts +1 -0
  96. package/dist/types/modes/types.d.ts +1 -0
  97. package/dist/types/modes/utils/injected-user-submission.d.ts +10 -0
  98. package/dist/types/runtime-mcp/config.d.ts +9 -0
  99. package/dist/types/runtime-mcp/manager.d.ts +26 -1
  100. package/dist/types/runtime-mcp/oauth-discovery.d.ts +4 -0
  101. package/dist/types/runtime-mcp/oauth-flow.d.ts +24 -1
  102. package/dist/types/runtime-mcp/pool-key.d.ts +2 -0
  103. package/dist/types/runtime-mcp/protocol.d.ts +278 -0
  104. package/dist/types/runtime-mcp/scope-config.d.ts +29 -0
  105. package/dist/types/runtime-mcp/tool-bridge.d.ts +15 -13
  106. package/dist/types/runtime-mcp/transports/http.d.ts +9 -0
  107. package/dist/types/runtime-mcp/types.d.ts +102 -10
  108. package/dist/types/sdk/acp/adapter.d.ts +26 -5
  109. package/dist/types/sdk/broker/broker.d.ts +3 -55
  110. package/dist/types/sdk/broker/lifecycle.d.ts +18 -12
  111. package/dist/types/sdk/broker/session-index.d.ts +14 -5
  112. package/dist/types/sdk/broker/startup-failure.d.ts +16 -1
  113. package/dist/types/sdk/bus/chat-daemon-control.d.ts +51 -1
  114. package/dist/types/sdk/bus/chat-daemon-runtime.d.ts +4 -41
  115. package/dist/types/sdk/bus/config.d.ts +11 -0
  116. package/dist/types/sdk/bus/conversation-store.d.ts +0 -1
  117. package/dist/types/sdk/bus/daemon-paths.d.ts +6 -0
  118. package/dist/types/sdk/bus/discord-conversation.d.ts +2 -0
  119. package/dist/types/sdk/bus/discord-daemon.d.ts +17 -12
  120. package/dist/types/sdk/bus/existing-thread-readiness.d.ts +4 -34
  121. package/dist/types/sdk/bus/inbound-reaction-ordering.d.ts +61 -0
  122. package/dist/types/sdk/bus/index.d.ts +85 -99
  123. package/dist/types/sdk/bus/kind-aware-reconciliation.d.ts +29 -7
  124. package/dist/types/sdk/bus/managed-daemon.d.ts +2 -32
  125. package/dist/types/sdk/bus/notification-service.d.ts +52 -0
  126. package/dist/types/sdk/bus/operator-runtime.d.ts +1 -1
  127. package/dist/types/sdk/bus/prompt-reconciliation.d.ts +16 -5
  128. package/dist/types/sdk/bus/reconciliation-store.d.ts +113 -5
  129. package/dist/types/sdk/bus/session-control.d.ts +25 -1
  130. package/dist/types/sdk/bus/slack-conversation.d.ts +4 -0
  131. package/dist/types/sdk/bus/slack-daemon.d.ts +9 -17
  132. package/dist/types/sdk/bus/slack-live-provider.d.ts +3 -1
  133. package/dist/types/sdk/bus/slack-provider.d.ts +4 -0
  134. package/dist/types/sdk/bus/slack-thread-binding.d.ts +6 -40
  135. package/dist/types/sdk/bus/telegram-adoption-intent.d.ts +30 -104
  136. package/dist/types/sdk/bus/telegram-daemon-cli.d.ts +10 -0
  137. package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +262 -19
  138. package/dist/types/sdk/bus/telegram-daemon-orphan-reap.d.ts +125 -0
  139. package/dist/types/sdk/bus/telegram-daemon-owner-registry.d.ts +22 -0
  140. package/dist/types/sdk/bus/telegram-daemon.d.ts +119 -310
  141. package/dist/types/sdk/bus/telegram-reference.d.ts +1 -47
  142. package/dist/types/sdk/bus/threaded-render.d.ts +1 -0
  143. package/dist/types/sdk/bus/topic-registry.d.ts +33 -17
  144. package/dist/types/sdk/cli/rows.d.ts +2 -11
  145. package/dist/types/sdk/cli/session-cli.d.ts +15 -25
  146. package/dist/types/sdk/client/client.d.ts +97 -1
  147. package/dist/types/sdk/host/control/dispatch.d.ts +0 -7
  148. package/dist/types/sdk/host/control/index.d.ts +1 -1
  149. package/dist/types/sdk/host/control/operations.d.ts +18 -5
  150. package/dist/types/sdk/host/control/runtime-gate.d.ts +19 -0
  151. package/dist/types/sdk/host/host.d.ts +15 -1
  152. package/dist/types/sdk/host/query/handlers.d.ts +5 -0
  153. package/dist/types/sdk/host/reverse-leases.d.ts +1 -1
  154. package/dist/types/sdk/host/session-runtime.d.ts +127 -7
  155. package/dist/types/sdk/host/types.d.ts +1 -1
  156. package/dist/types/sdk/host/websocket-transport.d.ts +1 -1
  157. package/dist/types/sdk/index.d.ts +2 -1
  158. package/dist/types/sdk/lifecycle/broker-client.d.ts +8 -0
  159. package/dist/types/sdk/lifecycle/client.d.ts +59 -0
  160. package/dist/types/sdk/lifecycle/index.d.ts +3 -0
  161. package/dist/types/sdk/lifecycle/service.d.ts +232 -0
  162. package/dist/types/sdk/mcp/server.d.ts +9 -6
  163. package/dist/types/sdk/prompt-deadline-lease.d.ts +43 -0
  164. package/dist/types/sdk/prompt-deadline-manager.d.ts +28 -0
  165. package/dist/types/sdk/prompt-status.d.ts +10 -3
  166. package/dist/types/sdk/protocol/operation-registry.d.ts +5 -0
  167. package/dist/types/sdk/receipt-state.d.ts +17 -0
  168. package/dist/types/sdk/reconciliation-extensions.d.ts +2 -0
  169. package/dist/types/sdk/router/index.d.ts +1 -0
  170. package/dist/types/sdk/router/session-router.d.ts +152 -0
  171. package/dist/types/sdk/session-list.d.ts +22 -0
  172. package/dist/types/sdk/session-reconnect.d.ts +43 -1
  173. package/dist/types/sdk/session.d.ts +34 -1
  174. package/dist/types/sdk/transport/serve-cli.d.ts +1 -4
  175. package/dist/types/session/account-inventory.d.ts +86 -0
  176. package/dist/types/session/agent-session.d.ts +145 -6
  177. package/dist/types/session/agent-storage.d.ts +10 -0
  178. package/dist/types/session/coordinator-tool-label.d.ts +28 -0
  179. package/dist/types/session/internal/managed-session-storage.d.ts +23 -4
  180. package/dist/types/session/messages.d.ts +1 -1
  181. package/dist/types/session/secure-token-file.d.ts +3 -0
  182. package/dist/types/session/session-manager.d.ts +40 -1
  183. package/dist/types/session/startup-auth-config.d.ts +44 -0
  184. package/dist/types/session/terminal-abort.d.ts +378 -0
  185. package/dist/types/session/yield-queue.d.ts +7 -0
  186. package/dist/types/session-import/claude.d.ts +26 -0
  187. package/dist/types/session-import/codex.d.ts +88 -0
  188. package/dist/types/session-import/command.d.ts +35 -0
  189. package/dist/types/session-import/detect.d.ts +22 -0
  190. package/dist/types/session-import/index.d.ts +7 -0
  191. package/dist/types/session-import/provider-codex.d.ts +22 -0
  192. package/dist/types/session-import/provider-service.d.ts +38 -0
  193. package/dist/types/session-import/redact.d.ts +23 -0
  194. package/dist/types/session-import/service.d.ts +47 -0
  195. package/dist/types/session-import/types.d.ts +120 -0
  196. package/dist/types/setup/paseo/check.d.ts +16 -0
  197. package/dist/types/setup/paseo/install-saga.d.ts +115 -0
  198. package/dist/types/setup/paseo/json-publisher.d.ts +76 -0
  199. package/dist/types/setup/paseo/orchestration-preferences.d.ts +24 -0
  200. package/dist/types/setup/paseo/paseo-ownership.d.ts +87 -0
  201. package/dist/types/setup/paseo/paseo-setup.d.ts +29 -0
  202. package/dist/types/setup/paseo/provider-config.d.ts +35 -0
  203. package/dist/types/setup/paseo/remove.d.ts +14 -0
  204. package/dist/types/setup/paseo/result-types.d.ts +59 -0
  205. package/dist/types/setup/paseo/setup-deps.d.ts +73 -0
  206. package/dist/types/setup/paseo/skills-bridge.d.ts +44 -0
  207. package/dist/types/skill-state/active-state.d.ts +5 -1
  208. package/dist/types/slash-commands/acp-builtins.d.ts +5 -1
  209. package/dist/types/slash-commands/builtin-registry.d.ts +2 -0
  210. package/dist/types/slash-commands/helpers/credential-switch.d.ts +12 -0
  211. package/dist/types/slash-commands/helpers/usage-report.d.ts +5 -6
  212. package/dist/types/slash-commands/types.d.ts +7 -1
  213. package/dist/types/task/executor.d.ts +10 -0
  214. package/dist/types/task/index.d.ts +3 -1
  215. package/dist/types/tools/ask-answer-registry.d.ts +6 -3
  216. package/dist/types/tools/bash-sleep-advisory.d.ts +14 -0
  217. package/dist/types/tools/bash.d.ts +2 -6
  218. package/dist/types/tools/browser/attach.d.ts +11 -0
  219. package/dist/types/tools/browser/launch.d.ts +20 -0
  220. package/dist/types/tools/browser/profile-discovery.d.ts +6 -0
  221. package/dist/types/tools/browser/render.d.ts +1 -0
  222. package/dist/types/tools/browser.d.ts +3 -1
  223. package/dist/types/tools/image-gen.d.ts +9 -2
  224. package/dist/types/tools/index.d.ts +8 -0
  225. package/dist/types/tools/monitor.d.ts +1 -1
  226. package/dist/types/tools/skill-discovery.d.ts +8 -1
  227. package/dist/types/tools/subagent.d.ts +2 -0
  228. package/dist/types/tools/ultragoal-ask-guard.d.ts +2 -2
  229. package/dist/types/utils/herdr-pane.d.ts +76 -0
  230. package/package.json +19 -9
  231. package/scripts/build-binary.ts +2 -1
  232. package/scripts/build-sdk-package-smoke.ts +2 -13
  233. package/scripts/capture-sticky-viewport-showcase.ts +0 -1
  234. package/scripts/compile-args.ts +2 -0
  235. package/scripts/generate-sdk-adapter-parity-manifest.ts +31 -16
  236. package/scripts/generate-sdk-operation-inventory.ts +28 -0
  237. package/scripts/generate-telegram-baseline-manifest.ts +1 -0
  238. package/scripts/generate-tool-catalog.ts +6 -4
  239. package/scripts/verify-gjc-sdk-canonicalization.ts +5 -39
  240. package/src/async/job-manager.ts +352 -17
  241. package/src/async/support.ts +25 -0
  242. package/src/autoresearch/dashboard.ts +28 -13
  243. package/src/capability/index.ts +3 -1
  244. package/src/capability/mcp.ts +6 -0
  245. package/src/capability/types.ts +6 -0
  246. package/src/cli/accounts-cli.ts +395 -0
  247. package/src/cli/args.ts +23 -0
  248. package/src/cli/auth-broker-cli.ts +136 -66
  249. package/src/cli/auth-gateway-cli.ts +94 -70
  250. package/src/cli/config-cli.ts +27 -2
  251. package/src/cli/crash-cli.ts +78 -0
  252. package/src/cli/customize-doctor.ts +1605 -0
  253. package/src/cli/daemon-cli.ts +12 -3
  254. package/src/cli/fast-help.ts +1 -0
  255. package/src/cli/mcp-cli.ts +128 -60
  256. package/src/cli/notify-cli.ts +8 -14
  257. package/src/cli/root-flags.ts +8 -0
  258. package/src/cli/setup-cli.ts +145 -0
  259. package/src/cli/skills-cli.ts +38 -2
  260. package/src/cli/update-cli.ts +167 -12
  261. package/src/cli.ts +4 -0
  262. package/src/commands/accounts.ts +71 -0
  263. package/src/commands/crash.ts +35 -0
  264. package/src/commands/customize.ts +84 -0
  265. package/src/commands/daemon.ts +2 -2
  266. package/src/commands/deep-interview.ts +2 -0
  267. package/src/commands/launch.ts +1 -0
  268. package/src/commands/mcp.ts +12 -7
  269. package/src/commands/ralplan.ts +2 -0
  270. package/src/commands/sdk.ts +65 -32
  271. package/src/commands/setup.ts +5 -0
  272. package/src/commands/skills.ts +14 -4
  273. package/src/commands/ultragoal.ts +10 -1
  274. package/src/commands/update.ts +26 -15
  275. package/src/config/atomic-yaml-patch-worker.ts +57 -0
  276. package/src/config/atomic-yaml-patch.ts +769 -29
  277. package/src/config/config-schema-version.ts +10 -0
  278. package/src/config/file-lock.ts +70 -1
  279. package/src/config/mcp-schema.json +52 -14
  280. package/src/config/model-profile-activation.ts +20 -1
  281. package/src/config/model-profiles.ts +29 -19
  282. package/src/config/model-registry.ts +132 -21
  283. package/src/config/models-config-schema.ts +1 -1
  284. package/src/config/settings-schema.ts +76 -6
  285. package/src/config/settings.ts +4038 -199
  286. package/src/config/skill-settings-defaults.ts +41 -3
  287. package/src/config/task-delegation.ts +87 -0
  288. package/src/config/update-channel.ts +23 -0
  289. package/src/coordinator-mcp/server.ts +377 -308
  290. package/src/crash/index-store.ts +723 -0
  291. package/src/crash/nudge.ts +89 -0
  292. package/src/crash/record-loader.ts +149 -0
  293. package/src/crash/report.ts +551 -0
  294. package/src/crash/sanitize.ts +129 -0
  295. package/src/cursor.ts +36 -10
  296. package/src/customization/import.ts +946 -0
  297. package/src/customization/index.ts +4 -0
  298. package/src/customization/inventory.ts +335 -0
  299. package/src/customization/mutations.ts +141 -0
  300. package/src/customization/types.ts +223 -0
  301. package/src/daemon/control-types.ts +8 -1
  302. package/src/daemon/operator-contract.ts +6 -6
  303. package/src/defaults/gjc/skills/deep-interview/SKILL.md +11 -22
  304. package/src/defaults/gjc/skills/ralplan/SKILL.md +36 -23
  305. package/src/defaults/gjc/skills/ultragoal/SKILL.md +16 -0
  306. package/src/discovery/claude-hooks.ts +38 -0
  307. package/src/discovery/claude.ts +142 -209
  308. package/src/discovery/codex-hooks.ts +38 -0
  309. package/src/discovery/codex.ts +132 -198
  310. package/src/discovery/helpers.ts +8 -1
  311. package/src/discovery/index.ts +2 -0
  312. package/src/discovery/mcp-compat.ts +234 -0
  313. package/src/discovery/mcp-json.ts +6 -0
  314. package/src/edit/renderer.ts +40 -0
  315. package/src/edit/streaming.ts +4 -2
  316. package/src/exec/bash-executor.ts +24 -20
  317. package/src/export/html/index.ts +51 -2
  318. package/src/extensibility/extensions/loader.ts +9 -1
  319. package/src/extensibility/extensions/runner.ts +17 -4
  320. package/src/extensibility/extensions/types.ts +31 -6
  321. package/src/extensibility/gjc-plugins/constrained-hooks.ts +33 -0
  322. package/src/extensibility/gjc-plugins/installer.ts +9 -5
  323. package/src/extensibility/gjc-plugins/observability.ts +8 -2
  324. package/src/extensibility/gjc-plugins/registry.ts +13 -2
  325. package/src/extensibility/gjc-plugins/runtime-adapters.ts +3 -1
  326. package/src/extensibility/gjc-plugins/schema.ts +222 -39
  327. package/src/extensibility/hooks/loader.ts +154 -10
  328. package/src/extensibility/runtime-skill-discovery.ts +257 -60
  329. package/src/extensibility/skill-management.ts +368 -0
  330. package/src/extensibility/skills.ts +52 -16
  331. package/src/gjc-runtime/deep-interview-runtime.ts +45 -61
  332. package/src/gjc-runtime/launch-worktree.ts +56 -2
  333. package/src/gjc-runtime/linux-proc.ts +7 -1
  334. package/src/gjc-runtime/ralplan-runtime.ts +100 -164
  335. package/src/gjc-runtime/session-state-lock.ts +992 -0
  336. package/src/gjc-runtime/session-state-sidecar.ts +644 -142
  337. package/src/gjc-runtime/state-writer.ts +39 -14
  338. package/src/gjc-runtime/tmux-owner-isolation.ts +5 -4
  339. package/src/gjc-runtime/ultragoal-guard.ts +11 -4
  340. package/src/gjc-runtime/ultragoal-runtime.ts +44 -41
  341. package/src/gjc-runtime/workflow-settings.ts +596 -0
  342. package/src/goals/tools/goal-tool.ts +6 -1
  343. package/src/hooks/events.ts +291 -0
  344. package/src/hooks/native-skill-hook.ts +4 -0
  345. package/src/hooks/normalize.ts +369 -0
  346. package/src/hooks/skill-state.ts +165 -46
  347. package/src/internal-urls/docs-index.generated.ts +40 -32
  348. package/src/main.ts +32 -5
  349. package/src/migrate/mcp-mapper.ts +25 -2
  350. package/src/modes/acp/abort-scope.ts +29 -0
  351. package/src/modes/acp/acp-agent.ts +423 -98
  352. package/src/modes/components/assistant-message.ts +6 -1
  353. package/src/modes/components/customization/customization-dashboard.ts +443 -0
  354. package/src/modes/components/customization/import-wizard.ts +372 -0
  355. package/src/modes/components/customization/index.ts +1 -0
  356. package/src/modes/components/gajae-pet-widget.ts +539 -213
  357. package/src/modes/components/irc-sidebar.ts +11 -0
  358. package/src/modes/components/iterm-pet-transport.ts +585 -0
  359. package/src/modes/components/oauth-selector.ts +277 -86
  360. package/src/modes/components/pet-capability.ts +39 -40
  361. package/src/modes/components/settings-selector.ts +9 -0
  362. package/src/modes/components/status-line/gh.ts +45 -1
  363. package/src/modes/components/status-line/segments.ts +16 -1
  364. package/src/modes/components/tool-status-header.ts +13 -3
  365. package/src/modes/controllers/command-controller.ts +20 -8
  366. package/src/modes/controllers/extension-ui-controller.ts +56 -257
  367. package/src/modes/controllers/input-controller.ts +10 -0
  368. package/src/modes/controllers/runtime-mcp-command-controller.ts +16 -3
  369. package/src/modes/controllers/selector-controller.ts +177 -72
  370. package/src/modes/controllers/stt-controller.ts +3 -3
  371. package/src/modes/interactive-mode.ts +88 -16
  372. package/src/modes/print-mode.ts +35 -6
  373. package/src/modes/runtime-init.ts +21 -68
  374. package/src/modes/types.ts +1 -0
  375. package/src/modes/utils/injected-user-submission.ts +25 -0
  376. package/src/modes/utils/terminal-bell.ts +15 -1
  377. package/src/prompts/agent-fragments/ralplan-persistence.md +1 -1
  378. package/src/prompts/agent-fragments/restricted-bash.md +1 -1
  379. package/src/prompts/agents/critic.md +1 -1
  380. package/src/prompts/system/rlm-research.md +1 -1
  381. package/src/prompts/system/system-prompt.md +0 -2
  382. package/src/prompts/tools/bash.md +7 -1
  383. package/src/prompts/tools/browser.md +1 -1
  384. package/src/prompts/tools/cron.md +6 -6
  385. package/src/prompts/tools/read.md +0 -2
  386. package/src/prompts/tools/skill-discovery.md +4 -2
  387. package/src/runtime-mcp/client.ts +427 -23
  388. package/src/runtime-mcp/config.ts +37 -5
  389. package/src/runtime-mcp/manager-pool.test.ts +56 -0
  390. package/src/runtime-mcp/manager.ts +52 -2
  391. package/src/runtime-mcp/oauth-discovery.ts +13 -0
  392. package/src/runtime-mcp/oauth-flow.ts +46 -1
  393. package/src/runtime-mcp/pool-key.ts +5 -1
  394. package/src/runtime-mcp/pool.test.ts +9 -0
  395. package/src/runtime-mcp/protocol.test.ts +353 -0
  396. package/src/runtime-mcp/protocol.ts +624 -0
  397. package/src/runtime-mcp/scope-config.ts +78 -0
  398. package/src/runtime-mcp/tool-bridge.ts +44 -10
  399. package/src/runtime-mcp/transports/http.ts +84 -13
  400. package/src/runtime-mcp/types.ts +123 -12
  401. package/src/sdk/acp/adapter.ts +328 -97
  402. package/src/sdk/broker/broker.ts +160 -741
  403. package/src/sdk/broker/ensure.ts +9 -1
  404. package/src/sdk/broker/lifecycle.ts +510 -116
  405. package/src/sdk/broker/operation-receipt-ledger.ts +11 -2
  406. package/src/sdk/broker/process-incarnation.ts +19 -0
  407. package/src/sdk/broker/session-index.ts +516 -117
  408. package/src/sdk/broker/startup-failure.ts +28 -5
  409. package/src/sdk/broker/transport.ts +1 -13
  410. package/src/sdk/bus/chat-daemon-cli.ts +4 -2
  411. package/src/sdk/bus/chat-daemon-control.ts +67 -6
  412. package/src/sdk/bus/chat-daemon-runtime.ts +183 -859
  413. package/src/sdk/bus/config.ts +13 -0
  414. package/src/sdk/bus/conversation-store.ts +59 -171
  415. package/src/sdk/bus/daemon-paths.ts +20 -0
  416. package/src/sdk/bus/discord-conversation.ts +2 -0
  417. package/src/sdk/bus/discord-daemon.ts +524 -126
  418. package/src/sdk/bus/discord-live-provider.ts +206 -79
  419. package/src/sdk/bus/existing-thread-readiness.ts +4 -97
  420. package/src/sdk/bus/inbound-reaction-ordering.ts +122 -0
  421. package/src/sdk/bus/index.ts +2480 -456
  422. package/src/sdk/bus/kind-aware-reconciliation.ts +204 -34
  423. package/src/sdk/bus/lifecycle-commands.ts +2 -2
  424. package/src/sdk/bus/managed-daemon.ts +2 -145
  425. package/src/sdk/bus/notification-service.ts +196 -0
  426. package/src/sdk/bus/operator-runtime.ts +1 -1
  427. package/src/sdk/bus/prompt-reconciliation.ts +24 -15
  428. package/src/sdk/bus/reconciliation-store.ts +553 -24
  429. package/src/sdk/bus/session-control.ts +49 -3
  430. package/src/sdk/bus/slack-conversation.ts +4 -0
  431. package/src/sdk/bus/slack-daemon.ts +733 -134
  432. package/src/sdk/bus/slack-live-provider.ts +65 -15
  433. package/src/sdk/bus/slack-provider.ts +4 -0
  434. package/src/sdk/bus/slack-thread-binding.ts +9 -104
  435. package/src/sdk/bus/telegram-adoption-intent.ts +299 -209
  436. package/src/sdk/bus/telegram-daemon-cli.ts +194 -7
  437. package/src/sdk/bus/telegram-daemon-contract.ts +265 -19
  438. package/src/sdk/bus/telegram-daemon-control.ts +3 -13
  439. package/src/sdk/bus/telegram-daemon-orphan-reap.ts +498 -0
  440. package/src/sdk/bus/telegram-daemon-owner-registry.ts +176 -0
  441. package/src/sdk/bus/telegram-daemon.ts +2552 -2280
  442. package/src/sdk/bus/telegram-reference.ts +1 -226
  443. package/src/sdk/bus/telegram-setup.ts +2 -2
  444. package/src/sdk/bus/threaded-render.ts +1 -0
  445. package/src/sdk/bus/topic-registry.ts +279 -146
  446. package/src/sdk/cli/rows.ts +2 -35
  447. package/src/sdk/cli/session-cli.ts +776 -700
  448. package/src/sdk/client/client.ts +892 -1
  449. package/src/sdk/host/control/dispatch.ts +93 -58
  450. package/src/sdk/host/control/index.ts +1 -1
  451. package/src/sdk/host/control/operations.ts +18 -10
  452. package/src/sdk/host/control/runtime-gate.ts +40 -0
  453. package/src/sdk/host/host.ts +63 -12
  454. package/src/sdk/host/query/handlers.ts +66 -7
  455. package/src/sdk/host/reverse-leases.ts +2 -2
  456. package/src/sdk/host/sdk-surface-parity.test.ts +1 -1
  457. package/src/sdk/host/session-runtime.test.ts +2787 -34
  458. package/src/sdk/host/session-runtime.ts +1489 -101
  459. package/src/sdk/host/surface-policy.ts +12 -8
  460. package/src/sdk/host/types.ts +1 -1
  461. package/src/sdk/host/websocket-transport.lifecycle.test.ts +31 -59
  462. package/src/sdk/host/websocket-transport.ts +85 -50
  463. package/src/sdk/index.ts +2 -1
  464. package/src/sdk/lifecycle/broker-client.ts +53 -0
  465. package/src/sdk/lifecycle/client.ts +234 -0
  466. package/src/sdk/lifecycle/index.ts +3 -0
  467. package/src/sdk/{bus/recent-activity.ts → lifecycle/recent-sessions.ts} +5 -2
  468. package/src/sdk/lifecycle/service.ts +720 -0
  469. package/src/sdk/mcp/server.ts +226 -188
  470. package/src/sdk/prompt-deadline-lease.ts +55 -0
  471. package/src/sdk/prompt-deadline-manager.ts +148 -0
  472. package/src/sdk/prompt-status.ts +11 -3
  473. package/src/sdk/protocol/operation-inventory.generated.json +272 -129
  474. package/src/sdk/protocol/operation-registry.ts +71 -8
  475. package/src/sdk/receipt-state.ts +43 -0
  476. package/src/sdk/reconciliation-extensions.ts +14 -0
  477. package/src/sdk/router/index.ts +1 -0
  478. package/src/sdk/router/session-router.ts +1658 -0
  479. package/src/sdk/session-list.ts +93 -0
  480. package/src/sdk/session-reconnect.ts +45 -1
  481. package/src/sdk/session.ts +716 -120
  482. package/src/sdk/transport/serve-cli.ts +18 -34
  483. package/src/session/account-inventory.ts +603 -0
  484. package/src/session/agent-session.ts +2857 -582
  485. package/src/session/agent-storage.ts +35 -0
  486. package/src/session/blob-store.ts +32 -1
  487. package/src/session/coordinator-tool-label.ts +40 -0
  488. package/src/session/internal/managed-session-scope.ts +4 -0
  489. package/src/session/internal/managed-session-storage.ts +173 -12
  490. package/src/session/messages.ts +1 -1
  491. package/src/session/secure-token-file.ts +80 -0
  492. package/src/session/session-manager.ts +730 -58
  493. package/src/session/session-storage.ts +25 -16
  494. package/src/session/startup-auth-config.ts +297 -0
  495. package/src/session/terminal-abort.ts +1253 -0
  496. package/src/session/yield-queue.ts +57 -15
  497. package/src/session-import/claude.ts +334 -0
  498. package/src/session-import/codex.ts +1033 -0
  499. package/src/session-import/command.ts +77 -0
  500. package/src/session-import/detect.ts +133 -0
  501. package/src/session-import/index.ts +7 -0
  502. package/src/session-import/provider-codex.ts +327 -0
  503. package/src/session-import/provider-service.ts +457 -0
  504. package/src/session-import/redact.ts +128 -0
  505. package/src/session-import/service.ts +1264 -0
  506. package/src/session-import/types.ts +148 -0
  507. package/src/setup/paseo/check.ts +213 -0
  508. package/src/setup/paseo/install-saga.ts +293 -0
  509. package/src/setup/paseo/json-publisher.ts +263 -0
  510. package/src/setup/paseo/orchestration-preferences.ts +86 -0
  511. package/src/setup/paseo/paseo-ownership.ts +224 -0
  512. package/src/setup/paseo/paseo-setup.ts +253 -0
  513. package/src/setup/paseo/provider-config.ts +139 -0
  514. package/src/setup/paseo/remove.ts +183 -0
  515. package/src/setup/paseo/result-types.ts +79 -0
  516. package/src/setup/paseo/setup-deps.ts +175 -0
  517. package/src/setup/paseo/skills-bridge.ts +260 -0
  518. package/src/skill-state/active-state.ts +21 -11
  519. package/src/slash-commands/acp-builtins.ts +9 -1
  520. package/src/slash-commands/builtin-registry.ts +98 -16
  521. package/src/slash-commands/helpers/credential-switch.ts +143 -0
  522. package/src/slash-commands/helpers/mcp.ts +9 -3
  523. package/src/slash-commands/helpers/usage-report.ts +61 -56
  524. package/src/slash-commands/types.ts +7 -2
  525. package/src/task/executor.ts +30 -11
  526. package/src/task/index.ts +66 -7
  527. package/src/task/render.ts +23 -9
  528. package/src/tools/ask-answer-registry.ts +23 -7
  529. package/src/tools/ask.ts +8 -4
  530. package/src/tools/bash-sleep-advisory.ts +115 -0
  531. package/src/tools/bash.ts +98 -18
  532. package/src/tools/browser/attach.ts +71 -3
  533. package/src/tools/browser/launch.ts +86 -13
  534. package/src/tools/browser/profile-discovery.ts +57 -7
  535. package/src/tools/browser/render.ts +8 -1
  536. package/src/tools/browser.ts +130 -21
  537. package/src/tools/computer.enforcement.test.ts +1 -1
  538. package/src/tools/computer.ts +3 -2
  539. package/src/tools/cron.ts +12 -1
  540. package/src/tools/image-gen.ts +27 -16
  541. package/src/tools/index.ts +8 -0
  542. package/src/tools/job.ts +33 -6
  543. package/src/tools/monitor.ts +41 -2
  544. package/src/tools/skill-discovery.ts +21 -29
  545. package/src/tools/subagent.ts +142 -24
  546. package/src/tools/tool-catalog.generated.ts +6 -6
  547. package/src/tools/ultragoal-ask-guard.ts +10 -3
  548. package/src/utils/herdr-pane.ts +345 -0
  549. package/src/utils/title-generator.ts +2 -0
  550. package/src/web/scrapers/index.ts +6 -4
  551. package/dist/types/cli/malloc-env-guard.test.d.ts +0 -1
  552. package/dist/types/defaults/gjc-defaults.test.d.ts +0 -1
  553. package/dist/types/memory-backend/lazy-loading.test.d.ts +0 -1
  554. package/dist/types/runtime/boot-ordering.test.d.ts +0 -1
  555. package/dist/types/runtime/lazy-service.test.d.ts +0 -1
  556. package/dist/types/runtime/network-prewarm-service.test.d.ts +0 -1
  557. package/dist/types/runtime/workspace-tree-service.test.d.ts +0 -1
  558. package/dist/types/runtime-mcp/config.test.d.ts +0 -1
  559. package/dist/types/runtime-mcp/http.test.d.ts +0 -1
  560. package/dist/types/runtime-mcp/manager-pool.test.d.ts +0 -1
  561. package/dist/types/runtime-mcp/pool-key.test.d.ts +0 -1
  562. package/dist/types/runtime-mcp/pool.test.d.ts +0 -1
  563. package/dist/types/sdk/bus/lifecycle-control-runtime.d.ts +0 -117
  564. package/dist/types/sdk/bus/lifecycle-orchestrator.d.ts +0 -141
  565. package/dist/types/sdk/bus/telegram-cli.d.ts +0 -31
  566. package/dist/types/sdk/elevation/allowlist.d.ts +0 -20
  567. package/dist/types/sdk/elevation/audit.d.ts +0 -93
  568. package/dist/types/sdk/elevation/capability.d.ts +0 -3
  569. package/dist/types/sdk/elevation/digest.d.ts +0 -72
  570. package/dist/types/sdk/elevation/dispatch-receipt.d.ts +0 -85
  571. package/dist/types/sdk/elevation/grant-ledger.d.ts +0 -163
  572. package/dist/types/sdk/elevation/owner.d.ts +0 -48
  573. package/dist/types/sdk/host/import-graph.test.d.ts +0 -1
  574. package/dist/types/sdk/host/sdk-surface-parity.test.d.ts +0 -1
  575. package/dist/types/sdk/host/session-runtime.test.d.ts +0 -1
  576. package/dist/types/sdk/host/websocket-transport.lifecycle.test.d.ts +0 -1
  577. package/dist/types/session/auth-broker-config.d.ts +0 -13
  578. package/dist/types/session/heap-eviction-artifacts.test.d.ts +0 -1
  579. package/dist/types/skills/skills.test.d.ts +0 -1
  580. package/dist/types/slash-commands/notify-command.test.d.ts +0 -1
  581. package/dist/types/task/token-log.test.d.ts +0 -1
  582. package/dist/types/tools/computer.enforcement.test.d.ts +0 -1
  583. package/dist/types/tools/descriptors.test.d.ts +0 -1
  584. package/scripts/g005-daemon-smoke.ts +0 -180
  585. package/scripts/g011-daemon-path-smoke.ts +0 -153
  586. package/src/sdk/bus/lifecycle-control-runtime.ts +0 -1733
  587. package/src/sdk/bus/lifecycle-orchestrator.ts +0 -489
  588. package/src/sdk/bus/telegram-cli.ts +0 -212
  589. package/src/sdk/elevation/allowlist.ts +0 -54
  590. package/src/sdk/elevation/audit.ts +0 -208
  591. package/src/sdk/elevation/capability.ts +0 -41
  592. package/src/sdk/elevation/digest.ts +0 -129
  593. package/src/sdk/elevation/dispatch-receipt.ts +0 -184
  594. package/src/sdk/elevation/grant-ledger.ts +0 -1142
  595. package/src/sdk/elevation/owner.ts +0 -109
  596. package/src/session/auth-broker-config.ts +0 -109
  597. /package/dist/types/{ai-core-import-gate.test.d.ts → discovery/claude-hooks.d.ts} +0 -0
  598. /package/dist/types/{cli/fixture-report.test.d.ts → discovery/codex-hooks.d.ts} +0 -0
  599. /package/dist/types/sdk/{bus/recent-activity.d.ts → lifecycle/recent-sessions.d.ts} +0 -0
@@ -0,0 +1,1605 @@
1
+ /**
2
+ * `gjc customize doctor` — provenance-aware customization inspection.
3
+ *
4
+ * Answers, for one project: what did GJC discover, which source convention won,
5
+ * what is disabled/shadowed/rejected/quarantined, and why is a tool, skill,
6
+ * hook, extension, command, MCP server, or plugin bundle absent.
7
+ *
8
+ * The command is read-only and reuses the canonical discovery registry
9
+ * (`loadCapability`) and the exact session-startup consumers (`loadSkills`,
10
+ * `loadSlashCommands`, `loadAllMCPConfigs`, plugin-bundle registry). It never
11
+ * re-implements discovery, never executes hooks, never connects MCP servers,
12
+ * and never prints credentials, endpoint tokens, auth headers, env values, or
13
+ * raw config dumps.
14
+ */
15
+
16
+ import * as fs from "node:fs/promises";
17
+ import * as os from "node:os";
18
+ import * as path from "node:path";
19
+ import {
20
+ getMCPConfigPath,
21
+ getPluginsLockfile,
22
+ getPluginsNodeModules,
23
+ getPluginsPackageJson,
24
+ getProjectDir,
25
+ } from "@gajae-code/utils";
26
+ import { type ExtensionModule, extensionModuleCapability } from "../capability/extension-module";
27
+ import { findRepoRoot } from "../capability/fs";
28
+ import { type Hook, hookCapability } from "../capability/hook";
29
+ import { type MCPServer, mcpCapability } from "../capability/mcp";
30
+ import { type Skill, skillCapability } from "../capability/skill";
31
+ import { type SlashCommand, slashCommandCapability } from "../capability/slash-command";
32
+ import { type CustomTool, toolCapability } from "../capability/tool";
33
+ import type { Capability, LoadContext, LoadResult, SourceMeta } from "../capability/types";
34
+ import { Settings, type Settings as SettingsInstance } from "../config/settings";
35
+ import { resolveSkillScopeTrust } from "../config/skill-settings-defaults";
36
+ import { getEmbeddedDefaultGjcSkills } from "../defaults/gjc-defaults";
37
+ import { initializeWithSettings, loadCapability } from "../discovery";
38
+ import { inspectClaudeConvention } from "../discovery/claude";
39
+ import { inspectCodexConvention } from "../discovery/codex";
40
+ import { scanSkillsFromDir } from "../discovery/helpers";
41
+ import { summarizeGjcPluginObservability } from "../extensibility/gjc-plugins/observability";
42
+ import { loadEffectiveGjcPluginRegistry } from "../extensibility/gjc-plugins/registry";
43
+ import { getEnabledPlugins } from "../extensibility/plugins/loader";
44
+ import { loadSkills } from "../extensibility/skills";
45
+ import { loadSlashCommands } from "../extensibility/slash-commands";
46
+ import { loadAllMCPConfigs } from "../runtime-mcp/config";
47
+ import { readDisabledServers } from "../runtime-mcp/config-writer";
48
+ import { canonicalizeMCPEndpoint } from "../runtime-mcp/pool-key";
49
+ import { redactMCPEndpoint } from "../runtime-mcp/redaction";
50
+ import { CANONICAL_GJC_WORKFLOW_SKILLS } from "../skill-state/canonical-skills";
51
+ import { expandTilde } from "../tools/path-utils";
52
+
53
+ // =============================================================================
54
+ // Public types (stable JSON contract)
55
+ // =============================================================================
56
+
57
+ export type CustomizeSurfaceKind = "mcp" | "skill" | "hook" | "tool" | "extension" | "command" | "plugin-bundle";
58
+
59
+ /**
60
+ * Provenance class of a discovered item — the reusable read model for the
61
+ * `/extensions` surface (#4291) and CI/setup tooling.
62
+ *
63
+ * - `canonical`: project/global `.gjc` entries and GJC bundled defaults — the
64
+ * primary load path and the persisted authority for sessions.
65
+ * - `convention`: items from registered non-native conventions that are part of
66
+ * the discovery load path (claude-plugins, claude/codex hooks, agents,
67
+ * cursor, gemini, opencode, windsurf, cline, github, mcp-json, ssh).
68
+ * - `import-candidate`: Claude Code / Codex project (+ global) files on
69
+ * surfaces GJC deliberately never loads. Reported for provenance only; never
70
+ * active runtime authority. Candidates for a future import flow (#4291).
71
+ * - `imported`: items carrying explicit import provenance (reserved; no import
72
+ * command exists yet, so nothing emits this today).
73
+ * - `plugin`: plugin bundles (npm plugin packages + GJC plugin bundles).
74
+ */
75
+ export type CustomizeSourceClass = "canonical" | "convention" | "import-candidate" | "imported" | "plugin";
76
+
77
+ export type CustomizeItemStatus =
78
+ | "loaded"
79
+ | "disabled"
80
+ | "shadowed"
81
+ | "rejected"
82
+ | "quarantined"
83
+ | "stored-only"
84
+ | "ignored";
85
+
86
+ /**
87
+ * Bounded, documented reason codes. One reason per item; distinct examples
88
+ * (malformed, disabled, shadowed, quarantined, policy-blocked) are always
89
+ * distinguishable by `reason` alone.
90
+ */
91
+ export type CustomizeReasonCode =
92
+ | "loaded"
93
+ | "managed"
94
+ | "storage-only"
95
+ | "disabled-extension"
96
+ | "disabled-provider"
97
+ | "disabled-server"
98
+ | "disabled-bundle"
99
+ | "shadowed-by-precedence"
100
+ | "invalid-config"
101
+ | "load-error"
102
+ | "quarantined"
103
+ | "policy-blocked"
104
+ | "source-ignored";
105
+
106
+ export interface CustomizePrecedenceEntry {
107
+ provider: string;
108
+ displayName: string;
109
+ priority: number;
110
+ enabled: boolean;
111
+ }
112
+
113
+ export interface McpSafeSummary {
114
+ transport?: "stdio" | "http" | "sse";
115
+ /** Executable path for stdio servers (safe: no credential values). */
116
+ command?: string;
117
+ /** Arguments with secret-looking values replaced by "<redacted>". */
118
+ args?: string[];
119
+ /** Endpoint with userinfo/path/query redacted via redactMCPEndpoint. */
120
+ url?: string;
121
+ /** Environment variable names only — values are never emitted. */
122
+ envKeys?: string[];
123
+ hasHeaders: boolean;
124
+ hasAuth: boolean;
125
+ hasOauth: boolean;
126
+ enabled?: boolean;
127
+ autoload?: boolean;
128
+ /** True when session startup would include this server in its connect set. */
129
+ connectable: boolean;
130
+ }
131
+
132
+ export interface CustomizeDoctorItem {
133
+ name: string;
134
+ kind: CustomizeSurfaceKind;
135
+ /** Provenance class (see CustomizeSourceClass). */
136
+ sourceClass: CustomizeSourceClass;
137
+ convention: string;
138
+ provider: string;
139
+ providerName: string;
140
+ scope: "user" | "project" | "native";
141
+ path: string;
142
+ status: CustomizeItemStatus;
143
+ reason: CustomizeReasonCode;
144
+ detail: string;
145
+ remediation: string[];
146
+ trust: string;
147
+ restartRequired: boolean;
148
+ precedence: {
149
+ priority: number;
150
+ shadowedBy?: { provider: string; scope: string };
151
+ };
152
+ /** Present only for MCP servers. */
153
+ mcp?: McpSafeSummary;
154
+ /** Present only for quarantined plugin-bundle surfaces. */
155
+ quarantineCode?: string;
156
+ }
157
+
158
+ export interface CustomizeDoctorSurface {
159
+ kind: CustomizeSurfaceKind;
160
+ displayName: string;
161
+ description: string;
162
+ precedence: CustomizePrecedenceEntry[];
163
+ items: CustomizeDoctorItem[];
164
+ /** Skill-surface policy notes (why discovery is off or scoped). */
165
+ skillScopeNotes?: string[];
166
+ /** Surface-level warnings (for example a failed startup projection). */
167
+ warnings?: string[];
168
+ }
169
+
170
+ export interface CustomizeDoctorReport {
171
+ schemaVersion: 1;
172
+ command: "customize doctor";
173
+ cwd: string;
174
+ generatedAt: string;
175
+ policy: {
176
+ skillsEnabled: boolean;
177
+ skillScopeNotes: string[];
178
+ disabledProviders: string[];
179
+ mcpNote: string;
180
+ /** Conventions present on disk but deliberately not in the load path. */
181
+ conventionsNotLoaded: string[];
182
+ /** User-home convention directories present (global import candidates). */
183
+ globalImportCandidateDirs: string[];
184
+ /** Stable descriptor table for the sourceClass taxonomy (CI contract). */
185
+ sourceClasses: Array<{ sourceClass: CustomizeSourceClass; description: string }>;
186
+ };
187
+ surfaces: CustomizeDoctorSurface[];
188
+ summary: Record<string, number>;
189
+ warnings: string[];
190
+ }
191
+
192
+ export interface CustomizeDoctorCommandOptions {
193
+ json: boolean;
194
+ cwd?: string;
195
+ }
196
+
197
+ // =============================================================================
198
+ // Constants
199
+ // =============================================================================
200
+
201
+ const REDACTED = "<redacted>";
202
+
203
+ const BUILT_IN_SKILL_NAMES = new Set<string>(CANONICAL_GJC_WORKFLOW_SKILLS);
204
+
205
+ const SENSITIVE_KEY_PATTERN =
206
+ /(?:token|secret|key|credential|password|passwd|pwd|authorization|auth|bearer|cookie|session|apikey)/i;
207
+
208
+ const SURFACE_ORDER: CustomizeSurfaceKind[] = ["mcp", "skill", "hook", "tool", "extension", "command", "plugin-bundle"];
209
+
210
+ const SURFACE_DISPLAY: Record<CustomizeSurfaceKind, string> = {
211
+ mcp: "MCP Servers",
212
+ skill: "Skills",
213
+ hook: "Hooks",
214
+ tool: "Custom Tools",
215
+ extension: "Extension Modules",
216
+ command: "Slash Commands",
217
+ "plugin-bundle": "Plugin Bundles",
218
+ };
219
+
220
+ const SURFACE_DESCRIPTION: Record<CustomizeSurfaceKind, string> = {
221
+ mcp: "Model Context Protocol server configurations discovered from GJC and other registered conventions",
222
+ skill: "Skill definitions discovered from GJC and other registered conventions",
223
+ hook: "Hook files discovered from GJC and other registered conventions",
224
+ tool: "Custom tool modules discovered from GJC and other registered conventions",
225
+ extension: "Extension modules discovered from GJC and other registered conventions",
226
+ command: "Slash command templates discovered from GJC and other registered conventions",
227
+ "plugin-bundle": "Installed GJC plugin bundles and npm plugin packages",
228
+ };
229
+
230
+ /**
231
+ * Stable descriptor table for the sourceClass taxonomy. Serves as the read
232
+ * model contract for `/extensions` (#4291): consumers can rely on the class
233
+ * names and descriptions without parsing item internals.
234
+ */
235
+ const SOURCE_CLASS_DESCRIPTIONS: Array<{ sourceClass: CustomizeSourceClass; description: string }> = [
236
+ {
237
+ sourceClass: "canonical",
238
+ description:
239
+ "Project/global .gjc entries and GJC bundled defaults — the primary, authoritative load path for sessions.",
240
+ },
241
+ {
242
+ sourceClass: "convention",
243
+ description:
244
+ "Items from other registered conventions (claude-plugins, claude/codex hooks, agents, cursor, gemini, opencode, windsurf, cline, github, mcp-json, ssh) that are part of the discovery load path.",
245
+ },
246
+ {
247
+ sourceClass: "import-candidate",
248
+ description:
249
+ "Claude Code / Codex project (and global) files on surfaces GJC deliberately never loads. Reported for provenance as import candidates; never active runtime authority.",
250
+ },
251
+ {
252
+ sourceClass: "imported",
253
+ description:
254
+ "Items carrying explicit import provenance from another host. Reserved: no import command exists today, so nothing emits this class yet.",
255
+ },
256
+ {
257
+ sourceClass: "plugin",
258
+ description: "Plugin bundles (npm plugin packages and GJC plugin bundles) contributed by the plugin runtime.",
259
+ },
260
+ ];
261
+
262
+ const TRUST_BY_KIND: Record<CustomizeSurfaceKind, string> = {
263
+ mcp: "MCP servers run arbitrary local commands and may reach the network and filesystem. Only connect servers you trust.",
264
+ skill: "Skills are markdown prompts; they do not execute code by themselves.",
265
+ hook: "Hook files execute arbitrary code when a session runs them.",
266
+ tool: "Custom tool modules execute arbitrary code.",
267
+ extension: "Extension modules execute arbitrary code.",
268
+ command: "Slash commands expand to prompt text; they do not execute code.",
269
+ "plugin-bundle":
270
+ "Plugin bundles execute code and may add tools, hooks, MCP servers, and appendices. Only install bundles you trust.",
271
+ };
272
+
273
+ /** Whether a restart/new session is required for changes to take effect. */
274
+ function restartRequiredFor(kind: CustomizeSurfaceKind): boolean {
275
+ // MCP servers are connectable on demand inside a running session
276
+ // (`/mcp connect`); every other surface is fixed at session startup.
277
+ return kind !== "mcp";
278
+ }
279
+
280
+ function sourceClassFor(provider: string): CustomizeSourceClass {
281
+ switch (provider) {
282
+ case "native":
283
+ case "bundled":
284
+ return "canonical";
285
+ case "plugin":
286
+ case "gjc-bundle":
287
+ return "plugin";
288
+ // "claude"/"codex" items arriving through the registry (hooks) are load-path
289
+ // participants, so they classify as "convention"; the import-candidate class
290
+ // is only assigned by the direct foreign-convention inspection below.
291
+ default:
292
+ return "convention";
293
+ }
294
+ }
295
+
296
+ function conventionForProvider(provider: string): string {
297
+ switch (provider) {
298
+ case "native":
299
+ case "bundled":
300
+ return "gjc";
301
+ case "claude":
302
+ return "claude-project";
303
+ case "claude-plugins":
304
+ return "claude-plugin";
305
+ case "codex":
306
+ return "codex-project";
307
+ case "cursor":
308
+ return "cursor-project";
309
+ case "gemini":
310
+ return "gemini-project";
311
+ case "opencode":
312
+ return "opencode-project";
313
+ case "windsurf":
314
+ return "windsurf-project";
315
+ case "cline":
316
+ return "cline-project";
317
+ case "agents":
318
+ return "agents-md";
319
+ case "mcp-json":
320
+ return "mcp-json";
321
+ case "github":
322
+ return "github";
323
+ case "ssh":
324
+ return "ssh-config";
325
+ case "plugin":
326
+ return "plugin";
327
+ case "custom":
328
+ return "explicit-config";
329
+ default:
330
+ return provider;
331
+ }
332
+ }
333
+
334
+ function scopeOf(meta: SourceMeta): "user" | "project" | "native" {
335
+ return meta.level;
336
+ }
337
+
338
+ // =============================================================================
339
+ // Discovery helper (canonical `loadCapability` projection, no re-implementation)
340
+ // =============================================================================
341
+
342
+ interface DiscoveredEntry<T> {
343
+ item: T;
344
+ name: string;
345
+ path: string;
346
+ provider: string;
347
+ providerName: string;
348
+ scope: "user" | "project" | "native";
349
+ priority: number;
350
+ shadowed: boolean;
351
+ shadowedBy?: { provider: string; scope: string };
352
+ invalidReason?: string;
353
+ /** Canonical dashboard id (`capability.toExtensionId`), when defined. */
354
+ extensionId?: string;
355
+ }
356
+
357
+ interface DiscoveredSet<T> {
358
+ entries: DiscoveredEntry<T>[];
359
+ precedence: CustomizePrecedenceEntry[];
360
+ }
361
+
362
+ async function discoverCapability<T extends { _source: SourceMeta }>(
363
+ capability: Capability<T>,
364
+ cwd: string,
365
+ activeSettings: SettingsInstance,
366
+ options: {
367
+ nameOf: (item: T) => string;
368
+ pathOf: (item: T) => string;
369
+ extensionIdOf?: (item: T) => string | undefined;
370
+ },
371
+ ): Promise<DiscoveredSet<T>> {
372
+ const disabledProviders = new Set(activeSettings.get("disabledProviders"));
373
+ const result = await loadCapability<T>(capability.id, {
374
+ cwd,
375
+ settings: activeSettings,
376
+ includeDisabled: true,
377
+ includeInvalid: true,
378
+ includeDisabledProviders: true,
379
+ });
380
+
381
+ // First item per dedup key wins (highest priority provider first in `all`).
382
+ const winners = new Map<string, { provider: string; scope: "user" | "project" | "native" }>();
383
+ const entries: DiscoveredEntry<T>[] = [];
384
+ for (const item of result.all) {
385
+ const key = capability.key(item);
386
+ const scope = scopeOf(item._source);
387
+ let shadowed = (item as { _shadowed?: boolean })._shadowed === true;
388
+ let shadowedBy: { provider: string; scope: string } | undefined;
389
+ if (key !== undefined) {
390
+ const winner = winners.get(key);
391
+ if (winner) {
392
+ shadowed = true;
393
+ shadowedBy = winner;
394
+ } else if (!disabledProviders.has(item._source.provider)) {
395
+ // Only enabled providers can own a dedup key; a disabled
396
+ // higher-priority provider must never shadow an enabled
397
+ // lower-priority source (includeDisabledProviders reports it
398
+ // as a listed item without giving it effective precedence).
399
+ winners.set(key, { provider: item._source.provider, scope });
400
+ }
401
+ }
402
+ entries.push({
403
+ item,
404
+ name: options.nameOf(item),
405
+ path: options.pathOf(item),
406
+ provider: item._source.provider,
407
+ providerName: item._source.providerName,
408
+ scope,
409
+ priority: capability.providers.find(p => p.id === item._source.provider)?.priority ?? 0,
410
+ shadowed,
411
+ shadowedBy,
412
+ invalidReason: capability.validate?.(item),
413
+ extensionId: options.extensionIdOf?.(item),
414
+ });
415
+ }
416
+
417
+ entries.sort((a, b) => a.name.localeCompare(b.name) || a.provider.localeCompare(b.provider));
418
+
419
+ const precedence: CustomizePrecedenceEntry[] = capability.providers.map(p => ({
420
+ provider: p.id,
421
+ displayName: p.displayName,
422
+ priority: p.priority,
423
+ enabled: !disabledProviders.has(p.id),
424
+ }));
425
+
426
+ return { entries, precedence };
427
+ }
428
+
429
+ function disabledExtensionIds(activeSettings: SettingsInstance): Set<string> {
430
+ return new Set(activeSettings.get("disabledExtensions"));
431
+ }
432
+
433
+ // =============================================================================
434
+ // MCP safe summary / redaction
435
+ // =============================================================================
436
+
437
+ function redactArgs(args: string[] | undefined): string[] | undefined {
438
+ if (!args || args.length === 0) return undefined;
439
+ const redacted: string[] = [];
440
+ let redactNext = false;
441
+ for (const arg of args) {
442
+ if (redactNext) {
443
+ redacted.push(REDACTED);
444
+ redactNext = false;
445
+ continue;
446
+ }
447
+ const equalsIndex = arg.indexOf("=");
448
+ if (equalsIndex > 0) {
449
+ const key = arg.slice(0, equalsIndex);
450
+ redacted.push(SENSITIVE_KEY_PATTERN.test(key) ? `${key}=${REDACTED}` : arg);
451
+ continue;
452
+ }
453
+ if (arg.startsWith("-") && SENSITIVE_KEY_PATTERN.test(arg)) {
454
+ redacted.push(arg);
455
+ redactNext = true;
456
+ continue;
457
+ }
458
+ redacted.push(SENSITIVE_KEY_PATTERN.test(arg) ? REDACTED : arg);
459
+ }
460
+ return redacted;
461
+ }
462
+
463
+ function safeMcpSummary(server: MCPServer, connectable: boolean): McpSafeSummary {
464
+ const envKeys = server.env ? Object.keys(server.env).sort() : undefined;
465
+ return {
466
+ transport: server.transport ?? (server.command ? "stdio" : server.url ? "http" : undefined),
467
+ command: server.command,
468
+ args: redactArgs(server.args),
469
+ // The canonical redactor runs values through the URL API, which
470
+ // percent-encodes the "<redacted>" placeholder; decode it for display.
471
+ url: redactMCPEndpoint(server.url)?.replaceAll("%3Credacted%3E", REDACTED),
472
+ envKeys,
473
+ hasHeaders: server.headers !== undefined && Object.keys(server.headers).length > 0,
474
+ hasAuth: server.auth !== undefined,
475
+ hasOauth: server.oauth !== undefined,
476
+ enabled: server.enabled,
477
+ autoload: server.autoload,
478
+ connectable,
479
+ };
480
+ }
481
+
482
+ // =============================================================================
483
+ // Item builders
484
+ // =============================================================================
485
+
486
+ function baseItem<T extends { _source: SourceMeta }>(
487
+ kind: CustomizeSurfaceKind,
488
+ entry: DiscoveredEntry<T>,
489
+ ): Omit<CustomizeDoctorItem, "status" | "reason" | "detail" | "remediation"> {
490
+ return {
491
+ name: entry.name,
492
+ kind,
493
+ sourceClass: sourceClassFor(entry.provider),
494
+ convention: conventionForProvider(entry.provider),
495
+ provider: entry.provider,
496
+ providerName: entry.providerName,
497
+ scope: entry.scope,
498
+ path: entry.path,
499
+ trust: TRUST_BY_KIND[kind],
500
+ restartRequired: restartRequiredFor(kind),
501
+ precedence: {
502
+ priority: entry.priority,
503
+ shadowedBy: entry.shadowedBy,
504
+ },
505
+ };
506
+ }
507
+
508
+ function finalizeItem(
509
+ base: Omit<CustomizeDoctorItem, "status" | "reason" | "detail" | "remediation">,
510
+ status: CustomizeItemStatus,
511
+ reason: CustomizeReasonCode,
512
+ detail: string,
513
+ remediation: string[],
514
+ extra?: Partial<CustomizeDoctorItem>,
515
+ ): CustomizeDoctorItem {
516
+ return { ...base, status, reason, detail, remediation, ...extra };
517
+ }
518
+
519
+ // =============================================================================
520
+ // Surface collectors
521
+ // =============================================================================
522
+
523
+ async function collectMcps(cwd: string, activeSettings: SettingsInstance): Promise<CustomizeDoctorSurface> {
524
+ const { entries, precedence } = await discoverCapability(mcpCapability, cwd, activeSettings, {
525
+ nameOf: server => server.name,
526
+ pathOf: server => server._source.path,
527
+ extensionIdOf: server => mcpCapability.toExtensionId?.(server),
528
+ });
529
+ const disabledServers = new Set(await readDisabledServers(getMCPConfigPath("user", cwd)));
530
+ const disabledExts = disabledExtensionIds(activeSettings);
531
+ const disabledProviders = new Set(activeSettings.get("disabledProviders"));
532
+ // The startup projection: `loadAllMCPConfigs` is what a session uses when
533
+ // connecting MCP servers (`/mcp connect` or `--mcp-config`). A single
534
+ // policy-violating endpoint (for example userinfo in a URL) fails the whole
535
+ // projection closed at startup, so catch it and report instead of losing
536
+ // the surface.
537
+ let connectableNames = new Set<string>();
538
+ const surfaceWarnings: string[] = [];
539
+ try {
540
+ connectableNames = new Set(Object.keys((await loadAllMCPConfigs(cwd)).configs));
541
+ } catch (error) {
542
+ surfaceWarnings.push(
543
+ `Startup MCP projection failed closed: ${error instanceof Error ? error.message : String(error)}. Sessions cannot connect any discovered server until this is fixed.`,
544
+ );
545
+ }
546
+
547
+ const items: CustomizeDoctorItem[] = entries.map(entry => {
548
+ const server = entry.item as MCPServer;
549
+ const base = baseItem("mcp", entry);
550
+ if (entry.invalidReason) {
551
+ return finalizeItem(
552
+ base,
553
+ "rejected",
554
+ "invalid-config",
555
+ `Invalid MCP server definition: ${entry.invalidReason}.`,
556
+ [`Fix ${entry.path}`],
557
+ );
558
+ }
559
+ if (entry.extensionId !== undefined && disabledExts.has(entry.extensionId)) {
560
+ return finalizeItem(
561
+ base,
562
+ "disabled",
563
+ "disabled-extension",
564
+ `MCP server "${entry.name}" is disabled via the disabledExtensions setting.`,
565
+ ["gjc config set disabledExtensions '[]'", "or re-enable it in the extension dashboard"],
566
+ );
567
+ }
568
+ if (disabledProviders.has(entry.provider)) {
569
+ return finalizeItem(
570
+ base,
571
+ "disabled",
572
+ "disabled-provider",
573
+ `Provider "${entry.providerName}" is disabled via the disabledProviders setting, so startup ignores this config source.`,
574
+ ["gjc config set disabledProviders '[]'"],
575
+ );
576
+ }
577
+ if (entry.shadowed && entry.shadowedBy) {
578
+ return finalizeItem(
579
+ base,
580
+ "shadowed",
581
+ "shadowed-by-precedence",
582
+ `"${entry.name}" from ${entry.providerName} (priority ${entry.priority}) loses to the same name from ${entry.shadowedBy.provider} (higher priority).`,
583
+ [
584
+ `Disable the winning source to let this one load: gjc config set disabledProviders '["${entry.shadowedBy.provider}"]'`,
585
+ `or rename "${entry.name}" in ${entry.path}`,
586
+ ],
587
+ );
588
+ }
589
+ // Startup endpoint policy: conversion canonicalizes http/sse endpoints
590
+ // before the enabled filter, so a violating endpoint (userinfo, relative
591
+ // URL) rejects the server even when it is disabled. Reuse the canonical
592
+ // validator rather than duplicating the policy.
593
+ if (server.url !== undefined) {
594
+ try {
595
+ canonicalizeMCPEndpoint(server.url);
596
+ } catch (error) {
597
+ return finalizeItem(
598
+ base,
599
+ "rejected",
600
+ "policy-blocked",
601
+ `MCP endpoint rejected by startup policy: ${error instanceof Error ? error.message : String(error)}.`,
602
+ [`Fix the endpoint in ${entry.path} (no userinfo; absolute http(s) URL)`],
603
+ { mcp: safeMcpSummary(server, false) },
604
+ );
605
+ }
606
+ }
607
+ if (server.enabled === false || disabledServers.has(entry.name)) {
608
+ return finalizeItem(
609
+ base,
610
+ "disabled",
611
+ "disabled-server",
612
+ `MCP server "${entry.name}" is disabled (enabled: false or listed in disabledServers).`,
613
+ [`Set enabled: true or remove "${entry.name}" from disabledServers in ${entry.path}`],
614
+ );
615
+ }
616
+ const connectable = connectableNames.has(entry.name);
617
+ return finalizeItem(
618
+ base,
619
+ "stored-only",
620
+ "storage-only",
621
+ "Discovered but never auto-connected by standalone sessions. Connect it explicitly when you need it.",
622
+ ["Run /mcp connect inside a session", `gjc --mcp-config ${entry.path} to load one exact config file`],
623
+ { mcp: safeMcpSummary(server, connectable) },
624
+ );
625
+ });
626
+
627
+ return {
628
+ kind: "mcp",
629
+ displayName: SURFACE_DISPLAY.mcp,
630
+ description: SURFACE_DESCRIPTION.mcp,
631
+ precedence,
632
+ items,
633
+ warnings: surfaceWarnings.length > 0 ? surfaceWarnings : undefined,
634
+ };
635
+ }
636
+
637
+ async function collectSkills(cwd: string, activeSettings: SettingsInstance): Promise<CustomizeDoctorSurface> {
638
+ const { entries, precedence } = await discoverCapability(skillCapability, cwd, activeSettings, {
639
+ nameOf: skill => skill.name,
640
+ pathOf: skill => skill.path,
641
+ extensionIdOf: skill => skillCapability.toExtensionId?.(skill),
642
+ });
643
+ const skillsEnabled = activeSettings.get("skills.enabled") === true;
644
+ const disabledExts = disabledExtensionIds(activeSettings);
645
+ const disabledProviders = new Set(activeSettings.get("disabledProviders"));
646
+ const ignoredPatterns = activeSettings.get("skills.ignoredSkills") ?? [];
647
+ const includePatterns = activeSettings.get("skills.includeSkills") ?? [];
648
+
649
+ // Exact session-startup consumer (sdk/session.ts): loadSkills only runs when
650
+ // skills.enabled is true, and only GJC (native) skills survive its filters.
651
+ const loadedNames = new Set<string>();
652
+ if (skillsEnabled) {
653
+ const result = await loadSkills({
654
+ ...activeSettings.getGroup("skills"),
655
+ cwd,
656
+ disabledExtensions: activeSettings.get("disabledExtensions"),
657
+ });
658
+ for (const skill of result.skills) loadedNames.add(skill.name);
659
+ }
660
+
661
+ const skillScopeNotes: string[] = [];
662
+ if (!skillsEnabled) {
663
+ skillScopeNotes.push(
664
+ "skills.enabled is false: sessions load only the four bundled workflow skills. Enable discovery with `gjc config set skills.enabled true`.",
665
+ );
666
+ }
667
+ if (skillsEnabled && !resolveSkillScopeTrust(activeSettings.getGroup("skills"), "project")) {
668
+ skillScopeNotes.push(
669
+ "Project skills are not trusted (skills.trustProjectSkills is false, or legacy skills.enablePiProject is false): project .gjc/skills are not loaded even with skills.enabled true.",
670
+ );
671
+ }
672
+ if (skillsEnabled && !resolveSkillScopeTrust(activeSettings.getGroup("skills"), "user")) {
673
+ skillScopeNotes.push(
674
+ "User skills are not trusted (skills.trustUserSkills is false, or legacy skills.enablePiUser is false): user-level skills are not loaded even with skills.enabled true.",
675
+ );
676
+ }
677
+
678
+ const items: CustomizeDoctorItem[] = entries.map(entry => {
679
+ const base = baseItem("skill", entry);
680
+ if (entry.invalidReason) {
681
+ return finalizeItem(base, "rejected", "invalid-config", `Invalid skill: ${entry.invalidReason}.`, [
682
+ `Fix ${entry.path}`,
683
+ ]);
684
+ }
685
+ if (!skillsEnabled) {
686
+ return finalizeItem(
687
+ base,
688
+ "disabled",
689
+ "policy-blocked",
690
+ "Skill discovery is disabled (skills.enabled is false), so this skill is not loaded by sessions.",
691
+ ["gjc config set skills.enabled true"],
692
+ );
693
+ }
694
+ if (entry.extensionId !== undefined && disabledExts.has(entry.extensionId)) {
695
+ return finalizeItem(
696
+ base,
697
+ "disabled",
698
+ "disabled-extension",
699
+ `Skill "${entry.name}" is disabled via the disabledExtensions setting.`,
700
+ ["gjc config set disabledExtensions '[]'", "or re-enable it in the extension dashboard"],
701
+ );
702
+ }
703
+ if (disabledProviders.has(entry.provider)) {
704
+ return finalizeItem(
705
+ base,
706
+ "disabled",
707
+ "disabled-provider",
708
+ `Provider "${entry.providerName}" is disabled via the disabledProviders setting, so startup ignores this config source.`,
709
+ ["gjc config set disabledProviders '[]'"],
710
+ );
711
+ }
712
+ if (entry.shadowed && entry.shadowedBy) {
713
+ return finalizeItem(
714
+ base,
715
+ "shadowed",
716
+ "shadowed-by-precedence",
717
+ `"${entry.name}" from ${entry.providerName} (priority ${entry.priority}) loses to the same name from ${entry.shadowedBy.provider} (higher priority).`,
718
+ [
719
+ `Disable the winning source to let this one load: gjc config set disabledProviders '["${entry.shadowedBy.provider}"]'`,
720
+ `or rename "${entry.name}" in ${entry.path}`,
721
+ ],
722
+ );
723
+ }
724
+ if (entry.provider !== "native") {
725
+ return finalizeItem(
726
+ base,
727
+ "ignored",
728
+ "source-ignored",
729
+ "GJC loads skills only from .gjc (native) locations. Skills discovered from other agent conventions are deliberately not loaded at session startup.",
730
+ [`Move the skill under .gjc/skills/<name>/SKILL.md to make it loadable`],
731
+ );
732
+ }
733
+ if (BUILT_IN_SKILL_NAMES.has(entry.name)) {
734
+ return finalizeItem(
735
+ base,
736
+ "shadowed",
737
+ "shadowed-by-precedence",
738
+ `"${entry.name}" is a bundled GJC workflow skill; the bundled definition takes precedence in sessions and this discovered copy is never used (sdk/session.ts withEmbeddedDefaultGjcSkills).`,
739
+ [`Rename ${entry.path} to make it loadable`],
740
+ { precedence: { priority: entry.priority, shadowedBy: { provider: "bundled", scope: "native" } } },
741
+ );
742
+ }
743
+ if (loadedNames.has(entry.name)) {
744
+ return finalizeItem(base, "loaded", "loaded", "Loaded by session startup.", []);
745
+ }
746
+ if (ignoredPatterns.length > 0 || includePatterns.length > 0) {
747
+ return finalizeItem(
748
+ base,
749
+ "disabled",
750
+ "policy-blocked",
751
+ "Discovered but not present in the session skill list; check skills.ignoredSkills and skills.includeSkills patterns.",
752
+ ["gjc config reset skills.ignoredSkills", "gjc config reset skills.includeSkills"],
753
+ );
754
+ }
755
+ return finalizeItem(
756
+ base,
757
+ "stored-only",
758
+ "policy-blocked",
759
+ "Discovered but not present in the session skill list (name collision, untrusted scope, or include/ignore pattern).",
760
+ ["Check skills.ignoredSkills, skills.includeSkills, skills.trustUserSkills, and skills.trustProjectSkills"],
761
+ );
762
+ });
763
+
764
+ // Explicit-config skills (skills.customDirectories). Session startup scans
765
+ // these outside the capability registry via loadSkills (providerId "custom");
766
+ // mirror that scan with the same helper and options so the report cannot
767
+ // drift from startup.
768
+ const customDirs = activeSettings.get("skills.customDirectories") ?? [];
769
+ for (const dir of [...customDirs].sort()) {
770
+ const scan = await scanSkillsFromDir(
771
+ { cwd, home: os.homedir(), repoRoot: null },
772
+ { dir: expandTilde(dir), providerId: "custom", level: "user", requireDescription: true },
773
+ );
774
+ for (const skill of scan.items) {
775
+ const base: Omit<CustomizeDoctorItem, "status" | "reason" | "detail" | "remediation"> = {
776
+ name: skill.name,
777
+ kind: "skill",
778
+ sourceClass: "canonical",
779
+ convention: "explicit-config",
780
+ provider: "custom",
781
+ providerName: "Custom",
782
+ scope: "user",
783
+ path: skill.path,
784
+ trust: TRUST_BY_KIND.skill,
785
+ restartRequired: restartRequiredFor("skill"),
786
+ precedence: { priority: 0 },
787
+ };
788
+ if (!skillsEnabled) {
789
+ items.push(
790
+ finalizeItem(
791
+ base,
792
+ "disabled",
793
+ "policy-blocked",
794
+ "Skill discovery is disabled (skills.enabled is false), so this skill is not loaded by sessions.",
795
+ ["gjc config set skills.enabled true"],
796
+ ),
797
+ );
798
+ continue;
799
+ }
800
+ if (disabledExts.has(`skill:${skill.name}`)) {
801
+ items.push(
802
+ finalizeItem(
803
+ base,
804
+ "disabled",
805
+ "disabled-extension",
806
+ `Skill "${skill.name}" is disabled via the disabledExtensions setting.`,
807
+ ["gjc config set disabledExtensions '[]'", "or re-enable it in the extension dashboard"],
808
+ ),
809
+ );
810
+ continue;
811
+ }
812
+ if (BUILT_IN_SKILL_NAMES.has(skill.name)) {
813
+ items.push(
814
+ finalizeItem(
815
+ base,
816
+ "shadowed",
817
+ "shadowed-by-precedence",
818
+ `"${skill.name}" is a bundled GJC workflow skill; the bundled definition takes precedence in sessions and this custom-directory copy is never used (sdk/session.ts withEmbeddedDefaultGjcSkills).`,
819
+ [`Rename ${skill.path} to make it loadable`],
820
+ { precedence: { priority: 0, shadowedBy: { provider: "bundled", scope: "native" } } },
821
+ ),
822
+ );
823
+ continue;
824
+ }
825
+ // Check for an already-loaded effective winner (native/convention
826
+ // skill of the same name) BEFORE the loadedNames shortcut: when
827
+ // loadSkills also scans customDirectories, a custom copy can appear
828
+ // in loadedNames even though it loses the name collision to an
829
+ // earlier-priority source. The collision must report shadowed-by-
830
+ // precedence, not a second "loaded" item.
831
+ const collisionWinner = items.find(i => i.name === skill.name && i.status === "loaded");
832
+ if (collisionWinner) {
833
+ items.push(
834
+ finalizeItem(
835
+ base,
836
+ "shadowed",
837
+ "shadowed-by-precedence",
838
+ `Custom-directory skill "${skill.name}" loses the name collision to the already-loaded ${collisionWinner.convention}/${collisionWinner.scope} skill of the same name.`,
839
+ [`Rename ${skill.path} to make it loadable`],
840
+ {
841
+ precedence: {
842
+ priority: 0,
843
+ shadowedBy: { provider: collisionWinner.provider, scope: collisionWinner.scope },
844
+ },
845
+ },
846
+ ),
847
+ );
848
+ continue;
849
+ }
850
+ if (loadedNames.has(skill.name)) {
851
+ items.push(
852
+ finalizeItem(base, "loaded", "loaded", "Loaded by session startup from skills.customDirectories.", []),
853
+ );
854
+ continue;
855
+ }
856
+ items.push(
857
+ finalizeItem(
858
+ base,
859
+ "stored-only",
860
+ "policy-blocked",
861
+ "Discovered in skills.customDirectories but not present in the session skill list (include/ignore pattern or a name collision with another custom directory).",
862
+ ["Check skills.ignoredSkills and skills.includeSkills"],
863
+ ),
864
+ );
865
+ }
866
+ }
867
+
868
+ // Bundled GJC workflow skills are a product invariant: sessions always
869
+ // include them, and the bundled definition always wins over any discovered
870
+ // same-name filesystem skill (sdk/session.ts withEmbeddedDefaultGjcSkills,
871
+ // extensibility/skills.ts collision warning). Discovered same-name copies
872
+ // are marked shadowed by the bundled entry above.
873
+ for (const embedded of getEmbeddedDefaultGjcSkills()) {
874
+ const base: Omit<CustomizeDoctorItem, "status" | "reason" | "detail" | "remediation"> = {
875
+ name: embedded.name,
876
+ kind: "skill",
877
+ sourceClass: "canonical",
878
+ convention: "gjc",
879
+ provider: "bundled",
880
+ providerName: "GJC Bundled",
881
+ scope: "native",
882
+ path: embedded.filePath,
883
+ trust: TRUST_BY_KIND.skill,
884
+ restartRequired: restartRequiredFor("skill"),
885
+ precedence: { priority: 0 },
886
+ };
887
+ items.push(
888
+ finalizeItem(
889
+ base,
890
+ "loaded",
891
+ "loaded",
892
+ "Bundled GJC workflow skill — always available to sessions (product invariant), even when skills.enabled is false, and always takes precedence over any discovered same-name filesystem skill.",
893
+ [],
894
+ ),
895
+ );
896
+ }
897
+
898
+ return {
899
+ kind: "skill",
900
+ displayName: SURFACE_DISPLAY.skill,
901
+ description: SURFACE_DESCRIPTION.skill,
902
+ precedence,
903
+ items,
904
+ skillScopeNotes,
905
+ };
906
+ }
907
+
908
+ async function collectHooks(cwd: string, activeSettings: SettingsInstance): Promise<CustomizeDoctorSurface> {
909
+ return collectNotExecutedAtStartup("hook", hookCapability, cwd, activeSettings, {
910
+ nameOf: hook => hook.name,
911
+ pathOf: hook => hook.path,
912
+ notExecutedDetail:
913
+ "Discovered and shown in the extension dashboard, but standalone sessions do not execute hook files. Runtime hooks come from validated GJC plugin bundles.",
914
+ notExecutedRemediation: ["See `gjc plugin list` for plugin bundles that contribute runtime hooks"],
915
+ });
916
+ }
917
+
918
+ async function collectTools(cwd: string, activeSettings: SettingsInstance): Promise<CustomizeDoctorSurface> {
919
+ return collectNotExecutedAtStartup("tool", toolCapability, cwd, activeSettings, {
920
+ nameOf: tool => tool.name,
921
+ pathOf: tool => tool.path,
922
+ notExecutedDetail:
923
+ "Discovered and shown in the extension dashboard, but standalone sessions do not execute custom tool modules. Runtime custom tools come from validated GJC plugin bundles and MCP servers.",
924
+ notExecutedRemediation: ["See `gjc plugin list` for plugin bundles that contribute runtime tools"],
925
+ });
926
+ }
927
+
928
+ async function collectExtensions(cwd: string, activeSettings: SettingsInstance): Promise<CustomizeDoctorSurface> {
929
+ return collectNotExecutedAtStartup("extension", extensionModuleCapability, cwd, activeSettings, {
930
+ nameOf: ext => ext.name,
931
+ pathOf: ext => ext.path,
932
+ notExecutedDetail:
933
+ "Discovered and shown in the extension dashboard, but session startup does not load filesystem extension modules. Runtime extensions come from validated GJC plugin bundles.",
934
+ notExecutedRemediation: ["See `gjc plugin list` for plugin bundles that contribute extensions"],
935
+ });
936
+ }
937
+
938
+ interface NotExecutedOptions<T extends { _source: SourceMeta }> {
939
+ nameOf: (item: T) => string;
940
+ pathOf: (item: T) => string;
941
+ notExecutedDetail: string;
942
+ notExecutedRemediation: string[];
943
+ }
944
+
945
+ async function collectNotExecutedAtStartup<T extends { _source: SourceMeta }>(
946
+ kind: "hook" | "tool" | "extension",
947
+ capability: Capability<T>,
948
+ cwd: string,
949
+ activeSettings: SettingsInstance,
950
+ options: NotExecutedOptions<T>,
951
+ ): Promise<CustomizeDoctorSurface> {
952
+ const { entries, precedence } = await discoverCapability(capability, cwd, activeSettings, {
953
+ nameOf: options.nameOf,
954
+ pathOf: options.pathOf,
955
+ extensionIdOf: item => capability.toExtensionId?.(item),
956
+ });
957
+ const disabledExts = disabledExtensionIds(activeSettings);
958
+ const disabledProviders = new Set(activeSettings.get("disabledProviders"));
959
+
960
+ const items: CustomizeDoctorItem[] = entries.map(entry => {
961
+ const base = baseItem(kind, entry);
962
+ if (entry.invalidReason) {
963
+ return finalizeItem(
964
+ base,
965
+ "rejected",
966
+ "invalid-config",
967
+ `Invalid ${kind} definition: ${entry.invalidReason}.`,
968
+ [`Fix ${entry.path}`],
969
+ );
970
+ }
971
+ if (entry.extensionId !== undefined && disabledExts.has(entry.extensionId)) {
972
+ return finalizeItem(
973
+ base,
974
+ "disabled",
975
+ "disabled-extension",
976
+ `"${entry.name}" is disabled via the disabledExtensions setting (${entry.extensionId}).`,
977
+ ["gjc config set disabledExtensions '[]'", "or re-enable it in the extension dashboard"],
978
+ );
979
+ }
980
+ if (disabledProviders.has(entry.provider)) {
981
+ return finalizeItem(
982
+ base,
983
+ "disabled",
984
+ "disabled-provider",
985
+ `Provider "${entry.providerName}" is disabled via the disabledProviders setting, so startup ignores this config source.`,
986
+ ["gjc config set disabledProviders '[]'"],
987
+ );
988
+ }
989
+ if (entry.shadowed && entry.shadowedBy) {
990
+ return finalizeItem(
991
+ base,
992
+ "shadowed",
993
+ "shadowed-by-precedence",
994
+ `"${entry.name}" from ${entry.providerName} (priority ${entry.priority}) loses to the same name from ${entry.shadowedBy.provider} (higher priority).`,
995
+ [
996
+ `Disable the winning source to let this one load: gjc config set disabledProviders '["${entry.shadowedBy.provider}"]'`,
997
+ `or rename "${entry.name}" in ${entry.path}`,
998
+ ],
999
+ );
1000
+ }
1001
+ return finalizeItem(base, "stored-only", "managed", options.notExecutedDetail, options.notExecutedRemediation);
1002
+ });
1003
+
1004
+ return { kind, displayName: SURFACE_DISPLAY[kind], description: SURFACE_DESCRIPTION[kind], precedence, items };
1005
+ }
1006
+
1007
+ async function collectCommands(cwd: string, activeSettings: SettingsInstance): Promise<CustomizeDoctorSurface> {
1008
+ const { entries, precedence } = await discoverCapability(slashCommandCapability, cwd, activeSettings, {
1009
+ nameOf: cmd => cmd.name,
1010
+ pathOf: cmd => cmd.path,
1011
+ extensionIdOf: cmd => slashCommandCapability.toExtensionId?.(cmd),
1012
+ });
1013
+ const disabledExts = disabledExtensionIds(activeSettings);
1014
+ const disabledProviders = new Set(activeSettings.get("disabledProviders"));
1015
+ // Exact session-startup consumer (interactive/print modes).
1016
+ const loadedNames = new Set((await loadSlashCommands({ cwd })).map(cmd => cmd.name));
1017
+
1018
+ const items: CustomizeDoctorItem[] = entries.map(entry => {
1019
+ const base = baseItem("command", entry);
1020
+ if (entry.invalidReason) {
1021
+ return finalizeItem(base, "rejected", "invalid-config", `Invalid slash command: ${entry.invalidReason}.`, [
1022
+ `Fix ${entry.path}`,
1023
+ ]);
1024
+ }
1025
+ if (entry.extensionId !== undefined && disabledExts.has(entry.extensionId)) {
1026
+ return finalizeItem(
1027
+ base,
1028
+ "disabled",
1029
+ "disabled-extension",
1030
+ `Slash command "${entry.name}" is disabled via the disabledExtensions setting.`,
1031
+ ["gjc config set disabledExtensions '[]'", "or re-enable it in the extension dashboard"],
1032
+ );
1033
+ }
1034
+ if (disabledProviders.has(entry.provider)) {
1035
+ return finalizeItem(
1036
+ base,
1037
+ "disabled",
1038
+ "disabled-provider",
1039
+ `Provider "${entry.providerName}" is disabled via the disabledProviders setting, so startup ignores this config source.`,
1040
+ ["gjc config set disabledProviders '[]'"],
1041
+ );
1042
+ }
1043
+ if (entry.shadowed && entry.shadowedBy) {
1044
+ return finalizeItem(
1045
+ base,
1046
+ "shadowed",
1047
+ "shadowed-by-precedence",
1048
+ `"${entry.name}" from ${entry.providerName} (priority ${entry.priority}) loses to the same name from ${entry.shadowedBy.provider} (higher priority).`,
1049
+ [
1050
+ `Disable the winning source to let this one load: gjc config set disabledProviders '["${entry.shadowedBy.provider}"]'`,
1051
+ `or rename "${entry.name}" in ${entry.path}`,
1052
+ ],
1053
+ );
1054
+ }
1055
+ if (loadedNames.has(entry.name)) {
1056
+ return finalizeItem(base, "loaded", "loaded", "Loaded at session startup as a slash command.", []);
1057
+ }
1058
+ return finalizeItem(
1059
+ base,
1060
+ "stored-only",
1061
+ "managed",
1062
+ "Discovered but not present in the session slash-command list.",
1063
+ ["Check disabledExtensions and provider enablement"],
1064
+ );
1065
+ });
1066
+
1067
+ return {
1068
+ kind: "command",
1069
+ displayName: SURFACE_DISPLAY.command,
1070
+ description: SURFACE_DESCRIPTION.command,
1071
+ precedence,
1072
+ items,
1073
+ };
1074
+ }
1075
+
1076
+ async function collectPluginBundles(cwd: string): Promise<CustomizeDoctorSurface> {
1077
+ // npm plugin packages (convention "plugin") — the startup consumer is
1078
+ // getEnabledPlugins(cwd); the lockfile provides the disabled set.
1079
+ const enabledNames = new Set((await getEnabledPlugins(cwd)).map(p => p.name));
1080
+ const lockNames = new Map<string, { enabled?: boolean }>();
1081
+ const pkgPath = getPluginsPackageJson();
1082
+ let depNames: string[] = [];
1083
+ try {
1084
+ const pkg = (await Bun.file(pkgPath).json()) as { dependencies?: Record<string, string> };
1085
+ depNames = Object.keys(pkg.dependencies ?? {}).sort();
1086
+ } catch {
1087
+ // No plugin package.json — no npm plugins installed.
1088
+ }
1089
+ try {
1090
+ const lock = (await Bun.file(getPluginsLockfile()).json()) as { plugins?: Record<string, { enabled?: boolean }> };
1091
+ for (const [name, state] of Object.entries(lock.plugins ?? {})) lockNames.set(name, state);
1092
+ } catch {
1093
+ // No lockfile — no disabled npm plugins to report.
1094
+ }
1095
+
1096
+ const npmItems: CustomizeDoctorItem[] = [];
1097
+ for (const name of depNames) {
1098
+ const enabled = enabledNames.has(name);
1099
+ const disabledByConfig = lockNames.get(name)?.enabled === false || !enabled;
1100
+ const base: Omit<CustomizeDoctorItem, "status" | "reason" | "detail" | "remediation"> = {
1101
+ name,
1102
+ kind: "plugin-bundle",
1103
+ sourceClass: "plugin",
1104
+ convention: "plugin",
1105
+ provider: "plugin",
1106
+ providerName: "Plugin",
1107
+ scope: "user",
1108
+ path: path.join(getPluginsNodeModules(), name),
1109
+ trust: TRUST_BY_KIND["plugin-bundle"],
1110
+ restartRequired: true,
1111
+ precedence: { priority: 0 },
1112
+ };
1113
+ if (enabled) {
1114
+ npmItems.push(
1115
+ finalizeItem(
1116
+ base,
1117
+ "loaded",
1118
+ "loaded",
1119
+ "Enabled npm plugin package; its tools/hooks/commands/extensions load at session startup.",
1120
+ [],
1121
+ ),
1122
+ );
1123
+ } else if (disabledByConfig) {
1124
+ npmItems.push(
1125
+ finalizeItem(
1126
+ base,
1127
+ "disabled",
1128
+ "disabled-bundle",
1129
+ "Installed npm plugin package that is disabled in plugin runtime config or project overrides.",
1130
+ ["gjc plugin enable <name>", "or remove it from plugin-overrides.json disabled list"],
1131
+ ),
1132
+ );
1133
+ }
1134
+ }
1135
+
1136
+ // GJC plugin bundles (convention "gjc-bundle") — canonical registry +
1137
+ // observability; never raw locators or config values.
1138
+ // Read-only: migrate:false keeps the doctor from persisting registry
1139
+ // migrations/legacy discovery (startup activation owns those writes).
1140
+ const bundleEntries = await loadEffectiveGjcPluginRegistry(cwd, { migrate: false });
1141
+ const observability = await summarizeGjcPluginObservability(cwd, { migrate: false });
1142
+ const items: CustomizeDoctorItem[] = [...npmItems];
1143
+ for (const entry of bundleEntries) {
1144
+ const base: Omit<CustomizeDoctorItem, "status" | "reason" | "detail" | "remediation"> = {
1145
+ name: entry.name,
1146
+ kind: "plugin-bundle",
1147
+ sourceClass: "plugin",
1148
+ convention: "gjc-bundle",
1149
+ provider: "gjc-bundle",
1150
+ providerName: "GJC Bundle",
1151
+ scope: entry.scope,
1152
+ path: entry.pluginRoot,
1153
+ trust: TRUST_BY_KIND["plugin-bundle"],
1154
+ restartRequired: true,
1155
+ precedence: { priority: 0 },
1156
+ };
1157
+ if (!entry.enabled) {
1158
+ items.push(
1159
+ finalizeItem(
1160
+ base,
1161
+ "disabled",
1162
+ "disabled-bundle",
1163
+ `GJC plugin bundle "${entry.name}" is disabled in the plugin registry.`,
1164
+ [`gjc plugin enable ${entry.name} --${entry.scope}`],
1165
+ ),
1166
+ );
1167
+ continue;
1168
+ }
1169
+ const surfaces = observability.surfaces.filter(s => s.plugin === entry.name && s.scope === entry.scope);
1170
+ const quarantined = surfaces.filter(s => s.status === "quarantined");
1171
+ if (quarantined.length > 0) {
1172
+ const codes = Array.from(new Set(quarantined.map(s => s.quarantineCode ?? "unknown"))).join(", ");
1173
+ items.push(
1174
+ finalizeItem(
1175
+ base,
1176
+ "quarantined",
1177
+ "quarantined",
1178
+ `${quarantined.length} surface(s) of bundle "${entry.name}" are quarantined (${codes}); quarantined surfaces are not activated.`,
1179
+ [
1180
+ `Reinstall the bundle: gjc plugin uninstall ${entry.name} --${entry.scope} && gjc plugin install <source> --${entry.scope}`,
1181
+ "Quarantine is triggered by file-hash drift or session collisions; verify the installed files",
1182
+ ],
1183
+ { quarantineCode: quarantined[0]?.quarantineCode },
1184
+ ),
1185
+ );
1186
+ continue;
1187
+ }
1188
+ const disabledSurfaceIds = entry.disabledSurfaceIds;
1189
+ const surfaceDetail =
1190
+ disabledSurfaceIds.length > 0
1191
+ ? ` ${disabledSurfaceIds.length} surface(s) disabled: ${disabledSurfaceIds.join(", ")}.`
1192
+ : "";
1193
+ items.push(
1194
+ finalizeItem(
1195
+ base,
1196
+ "loaded",
1197
+ "loaded",
1198
+ `Enabled GJC plugin bundle with ${surfaces.length} surface(s).${surfaceDetail}`,
1199
+ disabledSurfaceIds.length > 0 ? ["gjc plugin enable <name> --surface <id> to re-enable a surface"] : [],
1200
+ ),
1201
+ );
1202
+ }
1203
+
1204
+ items.sort((a, b) => a.name.localeCompare(b.name));
1205
+ return {
1206
+ kind: "plugin-bundle",
1207
+ displayName: SURFACE_DISPLAY["plugin-bundle"],
1208
+ description: SURFACE_DESCRIPTION["plugin-bundle"],
1209
+ precedence: [],
1210
+ items,
1211
+ };
1212
+ }
1213
+
1214
+ // =============================================================================
1215
+ // Foreign conventions (Claude project / Codex project)
1216
+ //
1217
+ // Claude Code / Codex project hooks are discovered by the registered
1218
+ // claude-hooks/codex-hooks providers (see discovery/index.ts) and appear in the
1219
+ // Hooks surface above; sessions surface them in the extension dashboard but do
1220
+ // not execute hook files. Every OTHER `.claude`/`.codex` surface (MCP, skills,
1221
+ // tools, extensions, commands, prompts, settings) is deliberately not part of
1222
+ // the load path: the doctor reports those files by invoking the canonical
1223
+ // provider load functions directly (without registering the providers) and
1224
+ // marks every such item as `ignored`.
1225
+ // =============================================================================
1226
+
1227
+ interface ForeignConventionDescriptor {
1228
+ convention: string;
1229
+ provider: string;
1230
+ providerName: string;
1231
+ dirName: string;
1232
+ inspect(ctx: LoadContext): Promise<{
1233
+ mcps: LoadResult<MCPServer>;
1234
+ skills: LoadResult<Skill>;
1235
+ hooks: LoadResult<Hook>;
1236
+ tools: LoadResult<CustomTool>;
1237
+ extensions: LoadResult<ExtensionModule>;
1238
+ commands: LoadResult<SlashCommand>;
1239
+ }>;
1240
+ }
1241
+
1242
+ const FOREIGN_CONVENTIONS: ForeignConventionDescriptor[] = [
1243
+ {
1244
+ convention: "claude-project",
1245
+ provider: "claude",
1246
+ providerName: "Claude Code",
1247
+ dirName: ".claude",
1248
+ inspect: inspectClaudeConvention,
1249
+ },
1250
+ {
1251
+ convention: "codex-project",
1252
+ provider: "codex",
1253
+ providerName: "OpenAI Codex",
1254
+ dirName: ".codex",
1255
+ inspect: inspectCodexConvention,
1256
+ },
1257
+ ];
1258
+
1259
+ interface ForeignConventionCollection {
1260
+ /** Foreign items appended to the matching surface kind. */
1261
+ itemsByKind: Map<CustomizeSurfaceKind, CustomizeDoctorItem[]>;
1262
+ /** Policy note about conventions that are not part of the load path. */
1263
+ policyNote: string;
1264
+ }
1265
+
1266
+ function foreignMcpItem(
1267
+ desc: ForeignConventionDescriptor,
1268
+ server: MCPServer,
1269
+ shadowedBy: { provider: string; scope: string } | undefined,
1270
+ ): CustomizeDoctorItem {
1271
+ const base: Omit<CustomizeDoctorItem, "status" | "reason" | "detail" | "remediation"> = {
1272
+ name: server.name,
1273
+ kind: "mcp",
1274
+ sourceClass: "import-candidate",
1275
+ convention: desc.convention,
1276
+ provider: desc.provider,
1277
+ providerName: desc.providerName,
1278
+ scope: "project",
1279
+ path: server._source.path,
1280
+ trust: TRUST_BY_KIND.mcp,
1281
+ restartRequired: false,
1282
+ precedence: { priority: 0, shadowedBy },
1283
+ };
1284
+ return {
1285
+ ...base,
1286
+ status: "ignored",
1287
+ reason: "source-ignored",
1288
+ detail: `${desc.providerName} project convention (${desc.dirName}/) is not part of the GJC load path; sessions never discover this MCP server.`,
1289
+ remediation: [
1290
+ `Move the server definition to .gjc/mcp.json to make it discoverable`,
1291
+ "or connect it explicitly via /mcp connect with an exact config file",
1292
+ ],
1293
+ mcp: safeMcpSummary(server, false),
1294
+ };
1295
+ }
1296
+
1297
+ function foreignItem(
1298
+ kind: CustomizeSurfaceKind,
1299
+ desc: ForeignConventionDescriptor,
1300
+ name: string,
1301
+ filePath: string,
1302
+ shadowedBy: { provider: string; scope: string } | undefined,
1303
+ ): CustomizeDoctorItem {
1304
+ return {
1305
+ name,
1306
+ kind,
1307
+ sourceClass: "import-candidate",
1308
+ convention: desc.convention,
1309
+ provider: desc.provider,
1310
+ providerName: desc.providerName,
1311
+ scope: "project",
1312
+ path: filePath,
1313
+ status: "ignored",
1314
+ reason: "source-ignored",
1315
+ detail: `${desc.providerName} project convention (${desc.dirName}/) is not part of the GJC load path; sessions never discover this file.`,
1316
+ remediation: [`Move it to the equivalent .gjc location`, "or install it via a plugin (see `gjc plugin list`)"],
1317
+ trust: TRUST_BY_KIND[kind],
1318
+ restartRequired: restartRequiredFor(kind),
1319
+ precedence: { priority: 0, shadowedBy },
1320
+ };
1321
+ }
1322
+
1323
+ async function collectForeignConventions(cwd: string): Promise<ForeignConventionCollection> {
1324
+ const ctx: LoadContext = { cwd, home: os.homedir(), repoRoot: await findRepoRoot(cwd) };
1325
+ const itemsByKind: Map<CustomizeSurfaceKind, CustomizeDoctorItem[]> = new Map();
1326
+ const policyNote = `${FOREIGN_CONVENTIONS.map(d => d.dirName).join(" and ")} project hooks are discovered by GJC (see the Hooks surface); all other ${FOREIGN_CONVENTIONS.map(d => d.providerName).join(" / ")} project config surfaces (MCP servers, skills, tools, extensions, commands, prompts, settings) are not part of the GJC load path. Those files are reported below as import candidates for provenance but are never discovered by sessions.`;
1327
+
1328
+ for (const desc of FOREIGN_CONVENTIONS) {
1329
+ const inspection = await desc.inspect(ctx);
1330
+ for (const server of inspection.mcps.items) {
1331
+ pushForeign(itemsByKind, "mcp", foreignMcpItem(desc, server, undefined));
1332
+ }
1333
+ for (const skill of inspection.skills.items) {
1334
+ pushForeign(itemsByKind, "skill", foreignItem("skill", desc, skill.name, skill.path, undefined));
1335
+ }
1336
+ // Hooks are intentionally skipped here: the registered claude-hooks and
1337
+ // codex-hooks providers already surface .claude/.codex hooks in the Hooks
1338
+ // surface, so reporting them again as ignored import candidates would
1339
+ // double-count and contradict the load path.
1340
+ for (const tool of inspection.tools.items) {
1341
+ pushForeign(itemsByKind, "tool", foreignItem("tool", desc, tool.name, tool.path, undefined));
1342
+ }
1343
+ for (const ext of inspection.extensions.items) {
1344
+ pushForeign(itemsByKind, "extension", foreignItem("extension", desc, ext.name, ext.path, undefined));
1345
+ }
1346
+ for (const cmd of inspection.commands.items) {
1347
+ pushForeign(itemsByKind, "command", foreignItem("command", desc, cmd.name, cmd.path, undefined));
1348
+ }
1349
+ }
1350
+ return { itemsByKind, policyNote };
1351
+ }
1352
+
1353
+ function pushForeign(
1354
+ itemsByKind: Map<CustomizeSurfaceKind, CustomizeDoctorItem[]>,
1355
+ kind: CustomizeSurfaceKind,
1356
+ item: CustomizeDoctorItem,
1357
+ ): void {
1358
+ const items = itemsByKind.get(kind) ?? [];
1359
+ items.push(item);
1360
+ itemsByKind.set(kind, items);
1361
+ }
1362
+
1363
+ /**
1364
+ * Attach provenance details to foreign items: when a registered source already
1365
+ * provides the same name, report it as the effective winner.
1366
+ */
1367
+ function annotateForeignShadowing(
1368
+ foreign: ForeignConventionCollection,
1369
+ registeredByKind: Map<CustomizeSurfaceKind, CustomizeDoctorItem[]>,
1370
+ ): void {
1371
+ for (const [kind, foreignItems] of foreign.itemsByKind) {
1372
+ const registered = registeredByKind.get(kind) ?? [];
1373
+ for (const item of foreignItems) {
1374
+ const winner = registered.find(r => r.name === item.name);
1375
+ if (winner) {
1376
+ item.precedence.shadowedBy = { provider: winner.provider, scope: winner.scope };
1377
+ item.detail += ` A ${winner.convention}/${winner.scope} item with the same name is the effective one (${winner.providerName}).`;
1378
+ }
1379
+ }
1380
+ }
1381
+ }
1382
+
1383
+ // =============================================================================
1384
+ // Orchestration
1385
+ // =============================================================================
1386
+
1387
+ /**
1388
+ * Build the full doctor report for a project directory.
1389
+ *
1390
+ * @param cwd Project directory; defaults to getProjectDir().
1391
+ * @param activeSettings Session-equivalent settings; when omitted,
1392
+ * Settings.loadReadonly is called for the given cwd (CLI path). The
1393
+ * read-only load never opens the DB, runs migrations, or writes files.
1394
+ * Tests pass an in-memory instance.
1395
+ */
1396
+ export async function runCustomizeDoctor(
1397
+ cwd?: string,
1398
+ activeSettings?: SettingsInstance,
1399
+ ): Promise<CustomizeDoctorReport> {
1400
+ const projectDir = cwd ?? getProjectDir();
1401
+ const settings = activeSettings ?? (await Settings.loadReadonly({ cwd: projectDir }));
1402
+ // Mirror session startup: the capability system and the downstream startup
1403
+ // consumers (loadAllMCPConfigs, loadSlashCommands) resolve provider policy
1404
+ // from the initialized session settings, so the doctor must initialize with
1405
+ // the same settings even when a caller supplied them.
1406
+ initializeWithSettings(settings);
1407
+
1408
+ const warnings: string[] = [];
1409
+ const surfaces: CustomizeDoctorSurface[] = [];
1410
+
1411
+ async function collect(kind: CustomizeSurfaceKind, fn: () => Promise<CustomizeDoctorSurface>): Promise<void> {
1412
+ try {
1413
+ surfaces.push(await fn());
1414
+ } catch (error) {
1415
+ warnings.push(`[${kind}] Failed to inspect: ${error instanceof Error ? error.message : String(error)}`);
1416
+ }
1417
+ }
1418
+
1419
+ await collect("mcp", () => collectMcps(projectDir, settings));
1420
+ await collect("skill", () => collectSkills(projectDir, settings));
1421
+ await collect("hook", () => collectHooks(projectDir, settings));
1422
+ await collect("tool", () => collectTools(projectDir, settings));
1423
+ await collect("extension", () => collectExtensions(projectDir, settings));
1424
+ await collect("command", () => collectCommands(projectDir, settings));
1425
+ await collect("plugin-bundle", () => collectPluginBundles(projectDir));
1426
+
1427
+ // Foreign conventions (Claude Code / Codex project dirs) are never part of
1428
+ // the load path; report them as import candidates for provenance.
1429
+ let foreignPolicyNote = "";
1430
+ try {
1431
+ const foreign = await collectForeignConventions(projectDir);
1432
+ foreignPolicyNote = foreign.policyNote;
1433
+ const byKind = new Map<CustomizeSurfaceKind, CustomizeDoctorItem[]>();
1434
+ for (const surface of surfaces) byKind.set(surface.kind, surface.items);
1435
+ annotateForeignShadowing(foreign, byKind);
1436
+ for (const [kind, items] of foreign.itemsByKind) {
1437
+ const surface = surfaces.find(s => s.kind === kind);
1438
+ if (surface) {
1439
+ surface.items.push(...items);
1440
+ } else {
1441
+ warnings.push(`[${kind}] foreign convention items found but the surface failed to inspect`);
1442
+ }
1443
+ }
1444
+ } catch (error) {
1445
+ warnings.push(
1446
+ `[foreign-conventions] Failed to inspect: ${error instanceof Error ? error.message : String(error)}`,
1447
+ );
1448
+ }
1449
+
1450
+ // Deterministic surface order; deterministic item order inside each surface.
1451
+ surfaces.sort((a, b) => SURFACE_ORDER.indexOf(a.kind) - SURFACE_ORDER.indexOf(b.kind));
1452
+ for (const surface of surfaces)
1453
+ surface.items.sort((a, b) => a.name.localeCompare(b.name) || a.path.localeCompare(b.path));
1454
+
1455
+ const summary: Record<string, number> = {};
1456
+ for (const surface of surfaces) {
1457
+ for (const item of surface.items) {
1458
+ summary[item.status] = (summary[item.status] ?? 0) + 1;
1459
+ }
1460
+ }
1461
+
1462
+ const skillScopeNotes = surfaces.find(s => s.kind === "skill")?.skillScopeNotes ?? [];
1463
+ const globalImportCandidateDirs = await findGlobalImportCandidateDirs();
1464
+
1465
+ return {
1466
+ schemaVersion: 1,
1467
+ command: "customize doctor",
1468
+ cwd: projectDir,
1469
+ generatedAt: new Date().toISOString(),
1470
+ policy: {
1471
+ skillsEnabled: settings.get("skills.enabled") === true,
1472
+ skillScopeNotes,
1473
+ disabledProviders: settings.get("disabledProviders"),
1474
+ mcpNote:
1475
+ "Standalone sessions never auto-connect MCP servers. Discovered servers are connectable on demand via /mcp connect, or with gjc --mcp-config <path>.",
1476
+ conventionsNotLoaded: foreignPolicyNote ? [foreignPolicyNote] : [],
1477
+ globalImportCandidateDirs,
1478
+ sourceClasses: SOURCE_CLASS_DESCRIPTIONS,
1479
+ },
1480
+ surfaces,
1481
+ summary,
1482
+ warnings,
1483
+ };
1484
+ }
1485
+
1486
+ /**
1487
+ * User-home convention directories that exist (global import candidates).
1488
+ * GJC never loads these; they are reported so the read model can distinguish
1489
+ * project and global Claude/Codex candidate locations.
1490
+ */
1491
+ async function findGlobalImportCandidateDirs(): Promise<string[]> {
1492
+ const home = os.homedir();
1493
+ const candidates: string[] = [];
1494
+ for (const dirName of [".claude", ".codex"]) {
1495
+ const dirPath = path.join(home, dirName);
1496
+ try {
1497
+ const stat = await fs.stat(dirPath);
1498
+ if (stat.isDirectory()) candidates.push(dirPath);
1499
+ } catch {
1500
+ // Missing or unreadable — not a candidate.
1501
+ }
1502
+ }
1503
+ return candidates.sort();
1504
+ }
1505
+
1506
+ // =============================================================================
1507
+ // Output rendering
1508
+ // =============================================================================
1509
+
1510
+ function formatShadowedBy(item: CustomizeDoctorItem): string {
1511
+ if (!item.precedence.shadowedBy) return "";
1512
+ const { provider, scope } = item.precedence.shadowedBy;
1513
+ return ` (shadowed by ${provider}/${scope})`;
1514
+ }
1515
+
1516
+ function formatMcpSummary(item: CustomizeDoctorItem): string {
1517
+ const mcp = item.mcp;
1518
+ if (!mcp) return "";
1519
+ const parts: string[] = [`transport: ${mcp.transport ?? "unknown"}`];
1520
+ if (mcp.command) parts.push(`command: ${mcp.command}`);
1521
+ if (mcp.args && mcp.args.length > 0) parts.push(`args: ${mcp.args.join(" ")}`);
1522
+ if (mcp.url) parts.push(`url: ${mcp.url}`);
1523
+ if (mcp.envKeys && mcp.envKeys.length > 0) parts.push(`env: ${mcp.envKeys.join(",")}`);
1524
+ if (mcp.hasHeaders) parts.push("headers: present (values redacted)");
1525
+ if (mcp.hasAuth) parts.push("auth: present (values redacted)");
1526
+ if (mcp.hasOauth) parts.push("oauth: present (values redacted)");
1527
+ if (mcp.connectable) parts.push("connectable: yes");
1528
+ return `\n ${parts.join("\n ")}`;
1529
+ }
1530
+
1531
+ export function renderCustomizeDoctorText(report: CustomizeDoctorReport): string {
1532
+ const lines: string[] = [];
1533
+ lines.push(`${report.command} — provenance and status for discovered customizations`);
1534
+ lines.push(`cwd: ${report.cwd}`);
1535
+ lines.push("");
1536
+
1537
+ const policyHints: string[] = [];
1538
+ for (const note of report.policy.skillScopeNotes) policyHints.push(note);
1539
+ if (report.policy.disabledProviders.length > 0) {
1540
+ policyHints.push(
1541
+ `Disabled providers: ${report.policy.disabledProviders.join(", ")} — their config sources are ignored at startup. Fix: gjc config set disabledProviders '[]'`,
1542
+ );
1543
+ }
1544
+ for (const note of report.policy.conventionsNotLoaded) policyHints.push(note);
1545
+ if (report.policy.globalImportCandidateDirs.length > 0) {
1546
+ policyHints.push(
1547
+ `Global import candidate dirs present in home: ${report.policy.globalImportCandidateDirs.join(", ")} — never loaded by GJC sessions.`,
1548
+ );
1549
+ }
1550
+ policyHints.push(report.policy.mcpNote);
1551
+ if (policyHints.length > 0) {
1552
+ lines.push("Policy");
1553
+ for (const hint of policyHints) lines.push(` - ${hint}`);
1554
+ lines.push("");
1555
+ }
1556
+
1557
+ for (const surface of report.surfaces) {
1558
+ lines.push(`${surface.displayName} (${surface.kind}) — ${surface.items.length} discovered`);
1559
+ for (const warning of surface.warnings ?? []) lines.push(` warning: ${warning}`);
1560
+ for (const p of surface.precedence) {
1561
+ lines.push(
1562
+ ` precedence: ${p.displayName} (${p.provider}) priority ${p.priority} ${p.enabled ? "enabled" : "disabled"}`,
1563
+ );
1564
+ }
1565
+ if (surface.items.length === 0) {
1566
+ lines.push(" (none discovered)");
1567
+ lines.push("");
1568
+ continue;
1569
+ }
1570
+ for (const item of surface.items) {
1571
+ lines.push(
1572
+ ` ${item.name} [${item.status}] ${item.convention}/${item.scope} (${item.sourceClass})${formatShadowedBy(item)}`,
1573
+ );
1574
+ lines.push(` reason: ${item.reason} — ${item.detail}`);
1575
+ lines.push(` source: ${item.path}`);
1576
+ if (item.quarantineCode) lines.push(` quarantine code: ${item.quarantineCode}`);
1577
+ for (const fix of item.remediation) lines.push(` fix: ${fix}`);
1578
+ lines.push(` trust: ${item.trust}`);
1579
+ lines.push(` restart required: ${item.restartRequired ? "yes (new session)" : "no"}`);
1580
+ if (item.mcp) lines.push(formatMcpSummary(item));
1581
+ }
1582
+ lines.push("");
1583
+ }
1584
+
1585
+ const summaryLine = Object.entries(report.summary)
1586
+ .map(([status, count]) => `${status}: ${count}`)
1587
+ .join(", ");
1588
+ lines.push(`Summary — ${summaryLine || "nothing discovered"}`);
1589
+ if (report.warnings.length > 0) {
1590
+ lines.push("");
1591
+ lines.push("Warnings");
1592
+ for (const warning of report.warnings) lines.push(` - ${warning}`);
1593
+ }
1594
+ return `${lines.join("\n")}\n`;
1595
+ }
1596
+
1597
+ export function renderCustomizeDoctorJson(report: CustomizeDoctorReport): string {
1598
+ return `${JSON.stringify(report, null, 2)}\n`;
1599
+ }
1600
+
1601
+ /** CLI entry: run the doctor and write text or JSON to stdout. */
1602
+ export async function runCustomizeDoctorCommand(options: CustomizeDoctorCommandOptions): Promise<void> {
1603
+ const report = await runCustomizeDoctor(options.cwd);
1604
+ process.stdout.write(options.json ? renderCustomizeDoctorJson(report) : renderCustomizeDoctorText(report));
1605
+ }