@gajae-code/coding-agent 0.16.7 → 0.17.1

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 (321) hide show
  1. package/CHANGELOG.md +180 -1
  2. package/dist/types/capability/fs.d.ts +37 -4
  3. package/dist/types/capability/index.d.ts +19 -0
  4. package/dist/types/capability/types.d.ts +9 -0
  5. package/dist/types/cli/doctor/args.d.ts +36 -0
  6. package/dist/types/cli/doctor/checks.d.ts +18 -0
  7. package/dist/types/cli/doctor/config-repairs.d.ts +38 -0
  8. package/dist/types/cli/doctor/context.d.ts +89 -0
  9. package/dist/types/cli/doctor/files.d.ts +27 -0
  10. package/dist/types/cli/doctor/ids.d.ts +20 -0
  11. package/dist/types/cli/doctor/install-repairs.d.ts +94 -0
  12. package/dist/types/cli/doctor/journal.d.ts +43 -0
  13. package/dist/types/cli/doctor/managed-link.d.ts +39 -0
  14. package/dist/types/cli/doctor/native-probe.d.ts +2 -0
  15. package/dist/types/cli/doctor/permission-repairs.d.ts +25 -0
  16. package/dist/types/cli/doctor/plugin-quarantine.d.ts +77 -0
  17. package/dist/types/cli/doctor/plugin-restore.d.ts +40 -0
  18. package/dist/types/cli/doctor/plugin-targets.d.ts +11 -0
  19. package/dist/types/cli/doctor/probe-client.d.ts +9 -0
  20. package/dist/types/cli/doctor/probe-types.d.ts +18 -0
  21. package/dist/types/cli/doctor/probe.d.ts +3 -0
  22. package/dist/types/cli/doctor/projection-probe.d.ts +2 -0
  23. package/dist/types/cli/doctor/repairs.d.ts +28 -0
  24. package/dist/types/cli/doctor/report.d.ts +27 -0
  25. package/dist/types/cli/doctor/runner.d.ts +4 -0
  26. package/dist/types/cli/doctor/safety.d.ts +10 -0
  27. package/dist/types/cli/doctor/service-repairs.d.ts +8 -0
  28. package/dist/types/cli/doctor/service-targets.d.ts +41 -0
  29. package/dist/types/cli/doctor/types.d.ts +145 -0
  30. package/dist/types/cli/doctor-cli.d.ts +7 -0
  31. package/dist/types/cli/doctor-supervisor.d.ts +121 -0
  32. package/dist/types/cli/doctor-worker.d.ts +29 -0
  33. package/dist/types/cli/install-activation.d.ts +158 -0
  34. package/dist/types/cli/macos-community-app.d.ts +41 -0
  35. package/dist/types/cli/mcp-cli.d.ts +0 -11
  36. package/dist/types/cli/update-cli.d.ts +41 -4
  37. package/dist/types/cli-main.d.ts +174 -0
  38. package/dist/types/cli.d.ts +1 -171
  39. package/dist/types/commands/doctor.d.ts +7 -0
  40. package/dist/types/commands/sdk.d.ts +17 -8
  41. package/dist/types/config/autorouting-tier-map.d.ts +19 -0
  42. package/dist/types/config/file-lock.d.ts +27 -3
  43. package/dist/types/config/model-preset-registry.d.ts +1 -0
  44. package/dist/types/config/model-registry.d.ts +12 -5
  45. package/dist/types/config/model-resolver.d.ts +16 -3
  46. package/dist/types/config/models-config-schema.d.ts +2 -0
  47. package/dist/types/config/settings-schema.d.ts +1 -1
  48. package/dist/types/coordinator-mcp/question-gate-codec.d.ts +1 -1
  49. package/dist/types/coordinator-mcp/session-reaper.d.ts +15 -5
  50. package/dist/types/cursor.d.ts +9 -9
  51. package/dist/types/discovery/agents-md.d.ts +6 -1
  52. package/dist/types/discovery/helpers.d.ts +36 -7
  53. package/dist/types/discovery/index.d.ts +1 -1
  54. package/dist/types/discovery/mcp-json.d.ts +2 -0
  55. package/dist/types/extensibility/extensions/loader.d.ts +22 -1
  56. package/dist/types/extensibility/gjc-plugins/lifecycle.d.ts +14 -1
  57. package/dist/types/extensibility/gjc-plugins/paths.d.ts +2 -1
  58. package/dist/types/extensibility/gjc-plugins/types.d.ts +25 -0
  59. package/dist/types/extensibility/plugins/manager.d.ts +33 -4
  60. package/dist/types/extensibility/plugins/marketplace/cache.d.ts +6 -0
  61. package/dist/types/extensibility/plugins/marketplace/manager.d.ts +186 -0
  62. package/dist/types/extensibility/plugins/marketplace/registry.d.ts +27 -0
  63. package/dist/types/extensibility/plugins/marketplace/source-resolver.d.ts +16 -1
  64. package/dist/types/extensibility/runtime-skill-discovery.d.ts +16 -3
  65. package/dist/types/gjc-runtime/autoresearch-runtime.d.ts +6 -0
  66. package/dist/types/gjc-runtime/launch-worktree.d.ts +53 -0
  67. package/dist/types/gjc-runtime/session-layout.d.ts +8 -0
  68. package/dist/types/gjc-runtime/session-state-lock.d.ts +15 -0
  69. package/dist/types/gjc-runtime/session-state-sidecar.d.ts +13 -0
  70. package/dist/types/gjc-runtime/ultragoal-runtime.d.ts +6 -0
  71. package/dist/types/main.d.ts +19 -1
  72. package/dist/types/modes/acp/acp-agent.d.ts +40 -0
  73. package/dist/types/modes/components/agent-dashboard.d.ts +1 -0
  74. package/dist/types/modes/components/model-selector.d.ts +2 -0
  75. package/dist/types/modes/interactive-mode.d.ts +1 -1
  76. package/dist/types/runtime-mcp/config-writer.d.ts +47 -0
  77. package/dist/types/runtime-mcp/policy-json-edit.d.ts +60 -0
  78. package/dist/types/runtime-mcp/startup-policy.d.ts +4 -0
  79. package/dist/types/runtime-mcp/types.d.ts +2 -2
  80. package/dist/types/sdk/acp/adapter.d.ts +5 -1
  81. package/dist/types/sdk/broker/broker.d.ts +51 -0
  82. package/dist/types/sdk/broker/daemon-entry.d.ts +39 -0
  83. package/dist/types/sdk/broker/discovery.d.ts +38 -0
  84. package/dist/types/sdk/broker/doctor-restart.d.ts +79 -0
  85. package/dist/types/sdk/broker/endpoint-authority.d.ts +7 -0
  86. package/dist/types/sdk/broker/ensure.d.ts +14 -0
  87. package/dist/types/sdk/broker/lifecycle.d.ts +2 -0
  88. package/dist/types/sdk/broker/process-incarnation.d.ts +50 -0
  89. package/dist/types/sdk/broker/runtime.d.ts +26 -0
  90. package/dist/types/sdk/broker/session-index.d.ts +13 -4
  91. package/dist/types/sdk/broker/worktree-occupancy.d.ts +14 -0
  92. package/dist/types/sdk/bus/chat-daemon-cli.d.ts +16 -0
  93. package/dist/types/sdk/bus/chat-daemon-control.d.ts +101 -6
  94. package/dist/types/sdk/bus/chat-daemon-runtime.d.ts +16 -0
  95. package/dist/types/sdk/bus/daemon-startup-exclusion.d.ts +43 -0
  96. package/dist/types/sdk/bus/doctor-daemon-restart-client.d.ts +51 -0
  97. package/dist/types/sdk/bus/doctor-daemon-restart.d.ts +39 -0
  98. package/dist/types/sdk/bus/index.d.ts +2 -0
  99. package/dist/types/sdk/bus/notification-service.d.ts +3 -0
  100. package/dist/types/sdk/bus/telegram-daemon-cli.d.ts +11 -0
  101. package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +1 -1
  102. package/dist/types/sdk/bus/telegram-daemon-control.d.ts +5 -0
  103. package/dist/types/sdk/bus/telegram-daemon-owner-registry.d.ts +1 -0
  104. package/dist/types/sdk/bus/telegram-daemon.d.ts +36 -5
  105. package/dist/types/sdk/cli/session-cli.d.ts +1 -0
  106. package/dist/types/sdk/host/session-runtime.d.ts +4 -0
  107. package/dist/types/sdk/host/surface-policy.d.ts +3 -1
  108. package/dist/types/sdk/lifecycle/service.d.ts +44 -10
  109. package/dist/types/sdk/prompt-deadline-lease.d.ts +6 -3
  110. package/dist/types/sdk/prompt-deadline-manager.d.ts +2 -2
  111. package/dist/types/sdk/prompt-failure.d.ts +47 -0
  112. package/dist/types/sdk/prompt-status.d.ts +19 -0
  113. package/dist/types/sdk/router/session-router.d.ts +3 -4
  114. package/dist/types/sdk/service-artifact-paths.d.ts +16 -0
  115. package/dist/types/sdk/session-authority.d.ts +9 -0
  116. package/dist/types/sdk/session.d.ts +3 -1
  117. package/dist/types/session/agent-session.d.ts +1 -1
  118. package/dist/types/session/blob-store.d.ts +1 -1
  119. package/dist/types/session/fallback-chain-controller.d.ts +3 -6
  120. package/dist/types/session/internal/managed-session-scope.d.ts +44 -0
  121. package/dist/types/session/primary-control-surface.d.ts +4 -0
  122. package/dist/types/session/session-manager.d.ts +6 -0
  123. package/dist/types/system-prompt.d.ts +6 -0
  124. package/dist/types/tools/ask.d.ts +25 -0
  125. package/dist/types/tools/image-gen.d.ts +0 -4
  126. package/dist/types/tools/skill-discovery.d.ts +7 -5
  127. package/dist/types/tools/ultragoal-ask-guard.d.ts +7 -4
  128. package/dist/types/utils/changelog.d.ts +0 -9
  129. package/dist/types/utils/mupdf-wasm.d.ts +1 -0
  130. package/package.json +9 -8
  131. package/scripts/compile-args.ts +8 -4
  132. package/src/capability/fs.ts +395 -17
  133. package/src/capability/index.ts +273 -10
  134. package/src/capability/types.ts +9 -0
  135. package/src/cli/customize-doctor.ts +18 -12
  136. package/src/cli/doctor/args.ts +236 -0
  137. package/src/cli/doctor/checks.ts +431 -0
  138. package/src/cli/doctor/config-repairs.ts +517 -0
  139. package/src/cli/doctor/context.ts +149 -0
  140. package/src/cli/doctor/files.ts +149 -0
  141. package/src/cli/doctor/ids.ts +115 -0
  142. package/src/cli/doctor/install-repairs.ts +335 -0
  143. package/src/cli/doctor/journal.ts +423 -0
  144. package/src/cli/doctor/managed-link.ts +514 -0
  145. package/src/cli/doctor/native-probe.ts +13 -0
  146. package/src/cli/doctor/permission-repairs.ts +242 -0
  147. package/src/cli/doctor/plugin-quarantine.ts +377 -0
  148. package/src/cli/doctor/plugin-restore.ts +59 -0
  149. package/src/cli/doctor/plugin-targets.ts +373 -0
  150. package/src/cli/doctor/probe-client.ts +195 -0
  151. package/src/cli/doctor/probe-types.ts +21 -0
  152. package/src/cli/doctor/probe.ts +42 -0
  153. package/src/cli/doctor/projection-probe.ts +249 -0
  154. package/src/cli/doctor/repairs.ts +1157 -0
  155. package/src/cli/doctor/report.ts +176 -0
  156. package/src/cli/doctor/runner.ts +342 -0
  157. package/src/cli/doctor/safety.ts +40 -0
  158. package/src/cli/doctor/service-repairs.ts +452 -0
  159. package/src/cli/doctor/service-targets.ts +274 -0
  160. package/src/cli/doctor/types.ts +164 -0
  161. package/src/cli/doctor-cli.ts +62 -0
  162. package/src/cli/doctor-supervisor.ts +571 -0
  163. package/src/cli/doctor-worker.ts +181 -0
  164. package/src/cli/install-activation.ts +666 -0
  165. package/src/cli/macos-community-app.ts +1449 -0
  166. package/src/cli/mcp-cli.ts +11 -21
  167. package/src/cli/nofile-limit.ts +5 -1
  168. package/src/cli/plugin-cli.ts +6 -1
  169. package/src/cli/update-cli.ts +362 -264
  170. package/src/cli-main.ts +517 -0
  171. package/src/cli.ts +64 -433
  172. package/src/commands/autoresearch.ts +11 -5
  173. package/src/commands/completion.ts +1 -1
  174. package/src/commands/doctor.ts +13 -0
  175. package/src/commands/sdk.ts +58 -10
  176. package/src/commands/ultragoal.ts +11 -5
  177. package/src/config/autorouting-tier-map.ts +7 -0
  178. package/src/config/file-lock.ts +736 -71
  179. package/src/config/mcp-schema.json +3 -3
  180. package/src/config/model-preset-registry.ts +1 -0
  181. package/src/config/model-registry.ts +80 -27
  182. package/src/config/model-resolver.ts +33 -4
  183. package/src/config/models-config-schema.ts +2 -0
  184. package/src/config/settings-schema.ts +6 -2
  185. package/src/coordinator-mcp/question-gate-codec.ts +1 -0
  186. package/src/coordinator-mcp/server.ts +534 -51
  187. package/src/coordinator-mcp/session-reaper.ts +64 -8
  188. package/src/cursor.ts +223 -53
  189. package/src/discovery/agents-md.ts +74 -8
  190. package/src/discovery/agents.ts +32 -7
  191. package/src/discovery/builtin.ts +184 -75
  192. package/src/discovery/claude-plugins.ts +85 -17
  193. package/src/discovery/cline.ts +38 -10
  194. package/src/discovery/cursor.ts +19 -6
  195. package/src/discovery/gemini.ts +62 -22
  196. package/src/discovery/github.ts +13 -3
  197. package/src/discovery/helpers.ts +483 -89
  198. package/src/discovery/index.ts +1 -0
  199. package/src/discovery/mcp-json.ts +36 -10
  200. package/src/discovery/opencode.ts +36 -19
  201. package/src/discovery/ssh.ts +11 -8
  202. package/src/discovery/windsurf.ts +12 -3
  203. package/src/exec/bash-executor.ts +10 -3
  204. package/src/exec/isolated-shell.ts +15 -2
  205. package/src/extensibility/extensions/loader.ts +88 -26
  206. package/src/extensibility/gjc-plugins/lifecycle.ts +189 -0
  207. package/src/extensibility/gjc-plugins/paths.ts +4 -1
  208. package/src/extensibility/gjc-plugins/types.ts +27 -0
  209. package/src/extensibility/plugins/manager.ts +200 -22
  210. package/src/extensibility/plugins/marketplace/cache.ts +55 -2
  211. package/src/extensibility/plugins/marketplace/manager.ts +1262 -162
  212. package/src/extensibility/plugins/marketplace/registry.ts +153 -0
  213. package/src/extensibility/plugins/marketplace/source-resolver.ts +116 -10
  214. package/src/extensibility/runtime-skill-discovery.ts +61 -9
  215. package/src/extensibility/skill-management.ts +7 -1
  216. package/src/gjc-runtime/autoresearch-runtime.ts +10 -1
  217. package/src/gjc-runtime/launch-worktree.test.ts +248 -0
  218. package/src/gjc-runtime/launch-worktree.ts +275 -38
  219. package/src/gjc-runtime/repository-binding.ts +26 -1
  220. package/src/gjc-runtime/session-layout.ts +10 -0
  221. package/src/gjc-runtime/session-state-lock.ts +330 -23
  222. package/src/gjc-runtime/session-state-sidecar.ts +328 -80
  223. package/src/gjc-runtime/ultragoal-runtime.ts +10 -1
  224. package/src/harness-control-plane/session-lease.ts +2 -2
  225. package/src/internal-urls/docs-index.generated.ts +28 -26
  226. package/src/main.ts +150 -29
  227. package/src/memories/index.ts +24 -2
  228. package/src/modes/acp/acp-agent.ts +522 -68
  229. package/src/modes/components/agent-dashboard.ts +3 -1
  230. package/src/modes/components/model-selector.ts +28 -19
  231. package/src/modes/components/plugin-settings.ts +2 -1
  232. package/src/modes/controllers/selector-controller.ts +15 -9
  233. package/src/modes/interactive-mode.ts +21 -3
  234. package/src/prompts/system/system-prompt.md +5 -1
  235. package/src/prompts/tools/browser.md +1 -1
  236. package/src/prompts/tools/move-session.md +4 -0
  237. package/src/prompts/tools/read.md +2 -1
  238. package/src/prompts/tools/skill-discovery.md +6 -5
  239. package/src/prompts/tools/task.md +1 -0
  240. package/src/runtime-mcp/config-writer.ts +228 -0
  241. package/src/runtime-mcp/oauth-discovery.ts +114 -14
  242. package/src/runtime-mcp/policy-json-edit.ts +129 -0
  243. package/src/runtime-mcp/startup-policy.ts +14 -0
  244. package/src/runtime-mcp/types.ts +2 -2
  245. package/src/sdk/acp/adapter.ts +27 -12
  246. package/src/sdk/broker/broker.ts +273 -44
  247. package/src/sdk/broker/daemon-entry.ts +119 -0
  248. package/src/sdk/broker/discovery.ts +173 -20
  249. package/src/sdk/broker/doctor-restart.ts +206 -0
  250. package/src/sdk/broker/endpoint-authority.ts +36 -0
  251. package/src/sdk/broker/ensure.ts +49 -14
  252. package/src/sdk/broker/lifecycle.ts +174 -42
  253. package/src/sdk/broker/process-incarnation.ts +86 -2
  254. package/src/sdk/broker/runtime.ts +145 -21
  255. package/src/sdk/broker/session-index.ts +136 -22
  256. package/src/sdk/broker/spawn-substrate.ts +4 -1
  257. package/src/sdk/broker/transport.ts +5 -0
  258. package/src/sdk/broker/worktree-occupancy.ts +21 -1
  259. package/src/sdk/bus/chat-daemon-cli.ts +63 -0
  260. package/src/sdk/bus/chat-daemon-control.ts +260 -64
  261. package/src/sdk/bus/chat-daemon-runtime.ts +141 -25
  262. package/src/sdk/bus/daemon-startup-exclusion.ts +119 -0
  263. package/src/sdk/bus/doctor-daemon-restart-client.ts +291 -0
  264. package/src/sdk/bus/doctor-daemon-restart.ts +92 -0
  265. package/src/sdk/bus/index.ts +252 -51
  266. package/src/sdk/bus/kind-aware-reconciliation.ts +76 -41
  267. package/src/sdk/bus/notification-service.ts +21 -3
  268. package/src/sdk/bus/reconciliation-store.ts +17 -11
  269. package/src/sdk/bus/telegram-daemon-cli.ts +91 -5
  270. package/src/sdk/bus/telegram-daemon-contract.ts +1 -1
  271. package/src/sdk/bus/telegram-daemon-control.ts +47 -0
  272. package/src/sdk/bus/telegram-daemon-owner-registry.ts +1 -1
  273. package/src/sdk/bus/telegram-daemon.ts +178 -13
  274. package/src/sdk/cli/session-cli.ts +100 -12
  275. package/src/sdk/host/host.ts +2 -7
  276. package/src/sdk/host/session-runtime.test.ts +329 -8
  277. package/src/sdk/host/session-runtime.ts +202 -50
  278. package/src/sdk/host/surface-policy.ts +8 -1
  279. package/src/sdk/lifecycle/service.ts +199 -22
  280. package/src/sdk/mcp/server.ts +1 -1
  281. package/src/sdk/prompt-deadline-lease.ts +12 -4
  282. package/src/sdk/prompt-deadline-manager.ts +5 -7
  283. package/src/sdk/prompt-failure.ts +148 -0
  284. package/src/sdk/prompt-status.ts +30 -1
  285. package/src/sdk/prompt-watchdog.ts +11 -3
  286. package/src/sdk/protocol/operation-inventory.generated.json +18 -0
  287. package/src/sdk/protocol/operation-registry.ts +20 -8
  288. package/src/sdk/router/session-router.ts +8 -3
  289. package/src/sdk/service-artifact-paths.ts +26 -0
  290. package/src/sdk/session-authority.ts +10 -0
  291. package/src/sdk/session.ts +361 -194
  292. package/src/session/agent-session.ts +398 -93
  293. package/src/session/blob-store.ts +9 -9
  294. package/src/session/fallback-chain-controller.ts +3 -7
  295. package/src/session/internal/managed-session-scope.ts +445 -29
  296. package/src/session/internal/managed-session-storage.ts +38 -6
  297. package/src/session/primary-control-surface.ts +12 -0
  298. package/src/session/session-manager.ts +11 -3
  299. package/src/session/session-storage.ts +1 -1
  300. package/src/session-import/codex.ts +6 -1
  301. package/src/session-import/redact.ts +7 -0
  302. package/src/skill-state/workflow-mutation-guard.ts +645 -39
  303. package/src/slash-commands/builtin-registry.ts +7 -3
  304. package/src/system-prompt.ts +8 -0
  305. package/src/task/executor.ts +36 -3
  306. package/src/task/types.ts +27 -5
  307. package/src/tools/ask.ts +114 -34
  308. package/src/tools/browser/actions.ts +2 -2
  309. package/src/tools/browser/tab-worker.ts +11 -8
  310. package/src/tools/descriptors.ts +6 -1
  311. package/src/tools/fetch.ts +7 -2
  312. package/src/tools/image-gen.ts +33 -34
  313. package/src/tools/read.ts +78 -32
  314. package/src/tools/skill-discovery.ts +17 -7
  315. package/src/tools/tool-catalog.generated.ts +8 -8
  316. package/src/tools/ultragoal-ask-guard.ts +16 -26
  317. package/src/utils/changelog.ts +7 -1
  318. package/src/utils/git.ts +3 -1
  319. package/src/utils/markit.ts +24 -2
  320. package/src/utils/mupdf-wasm.ts +26 -0
  321. package/src/web/search/providers/openai-compatible.ts +191 -14
@@ -6,10 +6,15 @@
6
6
  * - Registering providers (where to find it)
7
7
  * - Loading items for a capability across all providers
8
8
  */
9
+
10
+ import * as fs from "node:fs/promises";
9
11
  import * as path from "node:path";
10
12
  import {
13
+ CONFIG_DIR_NAME,
11
14
  getAgentDir,
12
15
  getAgentProfileAuthority,
16
+ getConfigDirName,
17
+ getPluginsDir,
13
18
  getProjectDir,
14
19
  getTrustedHomeDir,
15
20
  logger,
@@ -45,6 +50,157 @@ const providerMeta = new Map<string, { displayName: string; description: string
45
50
  /** Disabled providers (by ID) */
46
51
  const disabledProviders = new Set<string>();
47
52
 
53
+ /** User/project roots used by providers that derive paths from LoadContext.home/cwd. */
54
+ const EXPLICIT_HOME_PROVIDER_ROOTS = [
55
+ // `claude` and `codex` are import aliases for the registered hook providers.
56
+ { providerIds: ["claude", "claude-hooks"], user: null, project: ".claude" },
57
+ { providerIds: ["codex", "codex-hooks"], user: null, project: ".codex" },
58
+ { providerIds: ["gemini"], user: ".gemini", project: ".gemini" },
59
+ { providerIds: ["opencode"], user: path.join(".config", "opencode"), project: ".opencode" },
60
+ { providerIds: ["opencode"], user: null, project: "opencode.json" },
61
+ { providerIds: ["cursor"], user: ".cursor", project: ".cursor" },
62
+ { providerIds: ["windsurf"], user: path.join(".codeium", "windsurf"), project: ".windsurf" },
63
+ { providerIds: ["cline"], user: null, project: ".clinerules" },
64
+ { providerIds: ["github"], user: null, project: ".github" },
65
+ { providerIds: ["agents"], user: ".agent", project: ".agent" },
66
+ { providerIds: ["agents"], user: ".agents", project: ".agents" },
67
+ { providerIds: ["agents-md"], user: null, project: "AGENTS.md" },
68
+ { providerIds: ["mcp-json"], user: null, project: "mcp.json" },
69
+ { providerIds: ["mcp-json"], user: null, project: ".mcp.json" },
70
+ { providerIds: ["ssh-json"], user: null, project: "ssh.json" },
71
+ { providerIds: ["ssh-json"], user: null, project: ".ssh.json" },
72
+ ] as const;
73
+
74
+ const ANCESTOR_SCOPED_PROJECT_PROVIDERS = new Set(["agents", "agents-md", "cline"]);
75
+
76
+ function isWithinOrEqual(root: string, candidate: string): boolean {
77
+ const relative = path.relative(root, candidate);
78
+ return relative === "" || (!relative.startsWith(`..${path.sep}`) && relative !== ".." && !path.isAbsolute(relative));
79
+ }
80
+
81
+ /**
82
+ * Resolve a path through the real path of its deepest existing ancestor,
83
+ * re-appending any not-yet-created suffix. Explicit-home discovery accepts
84
+ * fresh project paths, but must not let a symlinked existing ancestor alias a
85
+ * directory outside the supplied home.
86
+ */
87
+ async function canonicalizeThroughExistingAncestor(target: string): Promise<string> {
88
+ const resolved = path.resolve(target);
89
+ const suffix: string[] = [];
90
+ let current = resolved;
91
+
92
+ while (true) {
93
+ try {
94
+ const real = await fs.realpath(current);
95
+ return suffix.length > 0 ? path.join(real, ...suffix.reverse()) : real;
96
+ } catch (error) {
97
+ const code = (error as NodeJS.ErrnoException).code;
98
+ if (code !== "ENOENT" && code !== "ENOTDIR") throw error;
99
+ const parent = path.dirname(current);
100
+ if (parent === current) return resolved;
101
+ suffix.push(path.basename(current));
102
+ current = parent;
103
+ }
104
+ }
105
+ }
106
+
107
+ async function canonicalizeContainedPath(root: string, target: string, label: string): Promise<string> {
108
+ const canonical = await canonicalizeThroughExistingAncestor(target);
109
+ if (!isWithinOrEqual(root, canonical)) {
110
+ throw new Error(
111
+ `loadCapabilityForHome ${label} escapes the supplied home through a symlink: "${target}" resolves to "${canonical}" outside "${root}".`,
112
+ );
113
+ }
114
+ return canonical;
115
+ }
116
+
117
+ /**
118
+ * Validate every config root that explicit-home discovery can use before any
119
+ * provider is invoked. A symlinked `.gjc`/`agent`/`plugins` directory would
120
+ * otherwise make a path that is lexically inside the supplied home read an
121
+ * unrelated profile.
122
+ */
123
+ async function assertExplicitHomeRoots(
124
+ canonicalHome: string,
125
+ cwd: string,
126
+ explicitAgentDir: boolean,
127
+ providerIds: ReadonlySet<string>,
128
+ isolatedHome: boolean,
129
+ ): Promise<void> {
130
+ const userRoots = new Map<string, string>();
131
+ const projectRoots = new Map<string, { label: string; walksAncestors: boolean }>();
132
+ const addUserRoot = (root: string, label: string): void => {
133
+ if (!userRoots.has(root)) userRoots.set(root, label);
134
+ };
135
+ const addProjectRoot = (root: string, label: string, walksAncestors = false): void => {
136
+ if (!projectRoots.has(root)) projectRoots.set(root, { label, walksAncestors });
137
+ };
138
+
139
+ // Resolve aliases before selecting roots. For example, the registered
140
+ // `claude-hooks` provider uses the `.claude` root shared by the `claude`
141
+ // import convention, while only its project scope is actually loaded.
142
+ for (const source of EXPLICIT_HOME_PROVIDER_ROOTS) {
143
+ if (!source.providerIds.some(providerId => providerIds.has(providerId))) continue;
144
+ const label = source.providerIds.join("/");
145
+ if (source.user) addUserRoot(path.join(canonicalHome, source.user), `${label} user root`);
146
+ if (source.project)
147
+ addProjectRoot(
148
+ source.project,
149
+ `${label} project root`,
150
+ source.providerIds.some(providerId => ANCESTOR_SCOPED_PROJECT_PROVIDERS.has(providerId)),
151
+ );
152
+ }
153
+
154
+ // Native and SSH providers use the managed agent directory for user scope,
155
+ // while native, SSH, and project-scoped SSH config also read the project
156
+ // config root. An explicit agent directory is intentionally allowed outside
157
+ // the supplied home and is validated separately by the caller.
158
+ const usesNative = providerIds.has("native");
159
+ const usesSsh = providerIds.has("ssh-json");
160
+ if ((usesNative || usesSsh) && !explicitAgentDir)
161
+ addUserRoot(path.join(canonicalHome, getConfigDirName(), "agent"), "user agent directory");
162
+ if (usesNative || usesSsh) {
163
+ // Native and SSH project discovery are fixed to `.gjc`; configured project
164
+ // roots are validated only by adapters that actually consume them below.
165
+ addProjectRoot(".gjc", "native/SSH project registry root");
166
+ }
167
+
168
+ // The marketplace provider derives its user registry from the XDG-aware
169
+ // helper rather than a fixed `<home>/.gjc/plugins` path.
170
+ if (providerIds.has("claude-plugins")) {
171
+ addProjectRoot(path.join(CONFIG_DIR_NAME, "plugins"), "project plugin registry root", true);
172
+ addUserRoot(getPluginsDir(canonicalHome), "plugin registry root");
173
+ }
174
+
175
+ const trustedHome = await canonicalizeThroughExistingAncestor(getTrustedHomeDir());
176
+ const currentProfilePluginRoot =
177
+ canonicalHome === trustedHome ? await canonicalizeThroughExistingAncestor(getPluginsDir(canonicalHome)) : null;
178
+ await Promise.all(
179
+ [...userRoots.entries()].map(async ([root, label]) => {
180
+ const canonicalRoot = await canonicalizeThroughExistingAncestor(root);
181
+ if (label === "plugin registry root" && canonicalRoot === currentProfilePluginRoot) return;
182
+ await canonicalizeContainedPath(canonicalHome, root, label);
183
+ }),
184
+ );
185
+
186
+ // Validate only the roots at the boundaries their providers actually inspect.
187
+ // Cwd-only providers must not fail because an unrelated ancestor contains a
188
+ // symlinked config file; ancestor-scoped providers retain the full walk.
189
+ await Promise.all(
190
+ [...projectRoots.entries()].map(async ([relativeRoot, { label, walksAncestors }]) => {
191
+ if (!isolatedHome) return;
192
+ let current = cwd;
193
+ while (true) {
194
+ await canonicalizeContainedPath(canonicalHome, path.join(current, relativeRoot), label);
195
+ if (!walksAncestors || current === canonicalHome) break;
196
+ const parent = path.dirname(current);
197
+ if (parent === current) break;
198
+ current = parent;
199
+ }
200
+ }),
201
+ );
202
+ }
203
+
48
204
  /** Settings manager for persistence (if set) */
49
205
  let settings: Settings | null = null;
50
206
  /** Session-local settings keyed by normalized working directory. */
@@ -120,7 +276,7 @@ async function loadImpl<T>(
120
276
  : new Set<string>(
121
277
  options.disabledExtensions ??
122
278
  options.settings?.get("disabledExtensions") ??
123
- settings?.get("disabledExtensions") ??
279
+ (options.isolatedHome ? undefined : settings?.get("disabledExtensions")) ??
124
280
  [],
125
281
  );
126
282
 
@@ -221,8 +377,12 @@ async function loadImpl<T>(
221
377
  * Filter providers based on options and disabled state.
222
378
  */
223
379
  function filterProviders<T>(capability: Capability<T>, options: LoadOptions): Provider<T>[] {
224
- const activeSettings = options.settings ?? settingsByCwd.get(path.normalize(options.cwd ?? getProjectDir()));
225
- const activeDisabledProviders = new Set(activeSettings?.get("disabledProviders") ?? disabledProviders);
380
+ const activeSettings = options.isolatedHome
381
+ ? options.settings
382
+ : (options.settings ?? settingsByCwd.get(path.normalize(options.cwd ?? getProjectDir())));
383
+ const activeDisabledProviders = new Set(
384
+ activeSettings?.get("disabledProviders") ?? (options.isolatedHome ? [] : disabledProviders),
385
+ );
226
386
  let providers = (capability.providers as Provider<T>[]).filter(
227
387
  p => options.includeDisabledProviders === true || !activeDisabledProviders.has(p.id),
228
388
  );
@@ -247,19 +407,122 @@ export async function loadCapability<T>(capabilityId: string, options: LoadOptio
247
407
  throw new Error(`Unknown capability: "${capabilityId}"`);
248
408
  }
249
409
 
250
- const cwd = options.cwd ?? getProjectDir();
410
+ // `isolatedHome` is an internal option used only by loadCapabilityForHome.
411
+ // Public ordinary loads must always apply the active process settings and
412
+ // disabled-provider/extension policy, even if a caller supplies that field.
413
+ const ordinaryOptions: LoadOptions = options.isolatedHome ? { ...options, isolatedHome: false } : options;
414
+ const cwd = ordinaryOptions.cwd ?? getProjectDir();
251
415
  const home = getTrustedHomeDir();
252
- const userAgentDir = options.agentDir ? path.resolve(options.agentDir) : getAgentDir();
416
+ const userAgentDir = ordinaryOptions.agentDir ? path.resolve(ordinaryOptions.agentDir) : getAgentDir();
253
417
  const profileAuthority =
254
- options.profileAuthority ??
255
- (options.agentDir && normalizePathForComparison(userAgentDir) !== normalizePathForComparison(getAgentDir())
418
+ ordinaryOptions.profileAuthority ??
419
+ (ordinaryOptions.agentDir &&
420
+ normalizePathForComparison(userAgentDir) !== normalizePathForComparison(getAgentDir())
256
421
  ? "custom"
257
422
  : getAgentProfileAuthority());
258
423
  const repoRoot = await findRepoRoot(cwd);
259
- const ctx: LoadContext = { cwd, home, userAgentDir, profileAuthority, repoRoot, settings: options.settings };
260
- const providers = filterProviders(capability, options);
424
+ const ctx: LoadContext = {
425
+ cwd,
426
+ home,
427
+ userAgentDir,
428
+ profileAuthority,
429
+ repoRoot,
430
+ isolatedHome: false,
431
+ settings: ordinaryOptions.settings,
432
+ };
433
+ const providers = filterProviders(capability, ordinaryOptions);
434
+
435
+ return await loadImpl(capability, providers, ctx, ordinaryOptions);
436
+ }
437
+
438
+ /**
439
+ * Load a capability against an explicitly supplied home directory.
440
+ *
441
+ * Cross-profile authority contract: when an explicit `home` is supplied, the
442
+ * load must resolve every user-scope surface from that home and NEVER fall
443
+ * back to the process-global `getAgentDir()`. The agent directory is derived
444
+ * from the supplied home (`<home>/<configDir>/agent`) unless the caller
445
+ * supplies an explicit `agentDir`; there is no implicit process-profile
446
+ * fallback. Consumers that cannot derive a user scope must fail closed.
447
+ */
448
+ export async function loadCapabilityForHome<T>(
449
+ capabilityId: string,
450
+ home: string,
451
+ options: LoadOptions = {},
452
+ ): Promise<CapabilityResult<T>> {
453
+ const capability = capabilities.get(capabilityId) as Capability<T> | undefined;
454
+ if (!capability) {
455
+ throw new Error(`Unknown capability: "${capabilityId}"`);
456
+ }
457
+
458
+ const resolvedHome = path.resolve(home);
459
+ if (!path.isAbsolute(home)) {
460
+ throw new Error(
461
+ `loadCapabilityForHome requires an absolute home directory; received "${home}". Refusing to fall back to the process profile.`,
462
+ );
463
+ }
464
+ // Derive the user agent directory from the SUPPLIED home. An explicit
465
+ // options.agentDir is honored; otherwise `<home>/<configDirName>/agent`.
466
+ // getAgentDir() is deliberately not consulted: an explicit home must never
467
+ // read another profile's SYSTEM/RULES/AGENTS, skills, commands, hooks,
468
+ // settings, or executable descriptors.
469
+ const canonicalHome = await canonicalizeThroughExistingAncestor(resolvedHome);
470
+ const homeStats = await fs.stat(canonicalHome);
471
+ const homeIdentity = { dev: homeStats.dev, ino: homeStats.ino };
472
+ const trustedHome = await canonicalizeThroughExistingAncestor(getTrustedHomeDir());
473
+ const isolatedHome = canonicalHome !== trustedHome;
474
+ const lexicalCwd = options.cwd === undefined ? canonicalHome : path.resolve(options.cwd);
475
+ const cwd = await canonicalizeThroughExistingAncestor(lexicalCwd);
476
+ // Every explicit-home cwd must resolve inside the supplied physical home.
477
+ // Allowing an outside cwd would let repository and plugin-registry discovery
478
+ // walk unrelated ancestors before the isolated boundary can take effect.
479
+ if (isolatedHome && !isWithinOrEqual(canonicalHome, cwd)) {
480
+ throw new Error(
481
+ `loadCapabilityForHome cwd is outside the supplied home: "${lexicalCwd}" resolves to "${cwd}" outside "${canonicalHome}".`,
482
+ );
483
+ }
484
+
485
+ const isolatedOptions: LoadOptions = { ...options, isolatedHome };
486
+ const providers = filterProviders(capability, isolatedOptions);
487
+ const effectiveProviderIds = new Set(providers.map(provider => provider.id));
488
+ const usesAgentScope = effectiveProviderIds.has("native") || effectiveProviderIds.has("ssh-json");
489
+ const userAgentDir = options.agentDir
490
+ ? await canonicalizeThroughExistingAncestor(path.resolve(options.agentDir))
491
+ : usesAgentScope
492
+ ? await canonicalizeContainedPath(
493
+ canonicalHome,
494
+ path.join(canonicalHome, getConfigDirName(), "agent"),
495
+ "user agent directory",
496
+ )
497
+ : path.join(canonicalHome, getConfigDirName(), "agent");
498
+ const userAgentStats = await fs.stat(userAgentDir).catch(() => null);
499
+ const userAgentIdentity = userAgentStats ? { dev: userAgentStats.dev, ino: userAgentStats.ino } : null;
500
+ await assertExplicitHomeRoots(canonicalHome, cwd, Boolean(options.agentDir), effectiveProviderIds, isolatedHome);
501
+ const repoRootCandidate = await findRepoRoot(cwd, canonicalHome, {
502
+ isolatedHome,
503
+ homeIdentity,
504
+ home: canonicalHome,
505
+ bypassCache: true,
506
+ });
507
+ const canonicalRepoRoot = repoRootCandidate ? await canonicalizeThroughExistingAncestor(repoRootCandidate) : null;
508
+ const repoRoot =
509
+ canonicalRepoRoot &&
510
+ canonicalRepoRoot !== canonicalHome &&
511
+ (!isolatedHome || isWithinOrEqual(canonicalHome, canonicalRepoRoot))
512
+ ? canonicalRepoRoot
513
+ : null;
514
+ const ctx: LoadContext = {
515
+ cwd,
516
+ home: canonicalHome,
517
+ userAgentDir,
518
+ userAgentIdentity,
519
+ repoRoot,
520
+ isolatedHome,
521
+ homeIdentity,
522
+ settings: isolatedOptions.settings,
523
+ };
261
524
 
262
- return await loadImpl(capability, providers, ctx, options);
525
+ return await loadImpl(capability, providers, ctx, isolatedOptions);
263
526
  }
264
527
 
265
528
  // =============================================================================
@@ -7,6 +7,7 @@
7
7
  */
8
8
 
9
9
  import type { Settings } from "../config/settings";
10
+ import type { FileIdentity } from "./fs";
10
11
  /**
11
12
  * Context passed to every provider loader.
12
13
  */
@@ -31,8 +32,14 @@ export interface LoadContext {
31
32
  userAgentDir?: string;
32
33
  /** Resolver-owned classification for the selected user agent directory. */
33
34
  profileAuthority?: "default" | "custom";
35
+ /** Device/inode identity captured for the explicit home root. */
36
+ homeIdentity?: FileIdentity;
37
+ /** Device/inode identity captured for the isolated native agent root. */
38
+ userAgentIdentity?: FileIdentity | null;
34
39
  /** Git repository root (directory containing .git), or null if not in a repo */
35
40
  repoRoot: string | null;
41
+ /** Whether discovery must remain isolated to the supplied home boundary. */
42
+ isolatedHome?: boolean;
36
43
  /** Owning session settings for provider policy decisions. */
37
44
  settings?: Settings;
38
45
  }
@@ -107,6 +114,8 @@ export interface LoadOptions {
107
114
  disabledExtensions?: string[];
108
115
  /** Session settings whose provider policy applies to this load. */
109
116
  settings?: Settings;
117
+ /** Internal: prevent explicit-home loads from consulting process-global policy. */
118
+ isolatedHome?: boolean;
110
119
  }
111
120
 
112
121
  /**
@@ -297,10 +297,11 @@ const TRUST_BY_KIND: Record<CustomizeSurfaceKind, string> = {
297
297
  };
298
298
 
299
299
  /** Whether a restart/new session is required for changes to take effect. */
300
- function restartRequiredFor(kind: CustomizeSurfaceKind): boolean {
301
- // MCP servers are connectable on demand inside a running session
302
- // (`/mcp connect`); every other surface is fixed at session startup.
303
- return kind !== "mcp";
300
+ function restartRequiredFor(_kind: CustomizeSurfaceKind): boolean {
301
+ // Every customization projection describes startup-owned state. MCP is not
302
+ // an in-session public reload surface, so it follows the same contract as
303
+ // every other customization kind.
304
+ return true;
304
305
  }
305
306
 
306
307
  function sourceClassFor(provider: string): CustomizeSourceClass {
@@ -556,7 +557,7 @@ async function collectMcps(cwd: string, activeSettings: SettingsInstance): Promi
556
557
  const disabledExts = disabledExtensionIds(activeSettings);
557
558
  const disabledProviders = new Set(activeSettings.get("disabledProviders"));
558
559
  // The startup projection: `loadAllMCPConfigs` is what a session uses when
559
- // connecting MCP servers (`/mcp connect` or `--mcp-config`). A single
560
+ // connecting MCP servers at startup (`--mcp-config` names one exact file). A single
560
561
  // policy-violating endpoint (for example userinfo in a URL) fails the whole
561
562
  // projection closed at startup, so catch it and report instead of losing
562
563
  // the surface.
@@ -644,8 +645,10 @@ async function collectMcps(cwd: string, activeSettings: SettingsInstance): Promi
644
645
  base,
645
646
  "stored-only",
646
647
  "storage-only",
647
- "Discovered but never auto-connected by standalone sessions. Connect it explicitly when you need it.",
648
- ["Run /mcp connect inside a session", `gjc --mcp-config ${entry.path} to load one exact config file`],
648
+ "Discovered but never auto-connected by standalone sessions at startup. Enable it for startup loading when you need it.",
649
+ [
650
+ `gjc --mcp-config ${entry.path} to load one exact config file (autoload: false servers stay unloaded; flip autoload and start a new session)`,
651
+ ],
649
652
  { mcp: safeMcpSummary(server, connectable) },
650
653
  );
651
654
  });
@@ -962,8 +965,11 @@ async function collectExtensions(cwd: string, activeSettings: SettingsInstance):
962
965
  nameOf: ext => ext.name,
963
966
  pathOf: ext => ext.path,
964
967
  notExecutedDetail:
965
- "Discovered and shown in the extension dashboard, but session startup does not load filesystem extension modules. Runtime extensions come from validated GJC plugin bundles.",
966
- notExecutedRemediation: ["See `gjc plugin list` for plugin bundles that contribute extensions"],
968
+ "Trusted filesystem extension module discovered for session-start loading. Doctor does not execute the module, so its runtime input handlers, tools, commands, and flags remain opaque until a session loads it.",
969
+ notExecutedRemediation: [
970
+ "Review the trusted extension source before session startup",
971
+ "Disable one module with gjc config set disabledExtensions '[\"extension-module:<name>\"]', or set disableExtensionDiscovery in the SDK",
972
+ ],
967
973
  });
968
974
  }
969
975
 
@@ -1404,7 +1410,7 @@ function foreignMcpItem(
1404
1410
  scope: "project",
1405
1411
  path: server._source.path,
1406
1412
  trust: TRUST_BY_KIND.mcp,
1407
- restartRequired: false,
1413
+ restartRequired: restartRequiredFor("mcp"),
1408
1414
  precedence: { priority: 0, shadowedBy },
1409
1415
  };
1410
1416
  return {
@@ -1414,7 +1420,7 @@ function foreignMcpItem(
1414
1420
  detail: `${desc.providerName} project convention (${desc.dirName}/) is not part of the GJC load path; sessions never discover this MCP server.`,
1415
1421
  remediation: [
1416
1422
  `Move the server definition to .gjc/mcp.json to make it discoverable`,
1417
- "or connect it explicitly via /mcp connect with an exact config file",
1423
+ "then enable it for startup loading (autoload: false servers stay unloaded; flip autoload and start a new session)",
1418
1424
  ],
1419
1425
  mcp: safeMcpSummary(server, false),
1420
1426
  };
@@ -1598,7 +1604,7 @@ export async function runCustomizeDoctor(
1598
1604
  skillScopeNotes,
1599
1605
  disabledProviders: settings.get("disabledProviders"),
1600
1606
  mcpNote:
1601
- "Standalone sessions never auto-connect MCP servers. Discovered servers are connectable on demand via /mcp connect, or with gjc --mcp-config <path>.",
1607
+ "Standalone sessions never auto-connect undiscoverable MCP servers at startup. Discoverable servers load from GJC's own config scopes, or name one exact file with gjc --mcp-config <path> (autoload: false servers stay unloaded; flip autoload and start a new session).",
1602
1608
  conventionsNotLoaded: foreignPolicyNote ? [foreignPolicyNote] : [],
1603
1609
  globalImportCandidateDirs,
1604
1610
  sourceClasses: SOURCE_CLASS_DESCRIPTIONS,
@@ -0,0 +1,236 @@
1
+ import { isDoctorTargetId } from "./ids";
2
+ import type { DoctorMode, DoctorRepair, DoctorRiskClass, DoctorScope } from "./types";
3
+
4
+ export type DoctorAction =
5
+ | "config.set-validated"
6
+ | "mcp.set-startup-policy"
7
+ | "permissions.restrict-owned-config"
8
+ | "install.restore-binary"
9
+ | "install.repair-managed-link"
10
+ | "plugin.restore-known-artifact"
11
+ | "plugin.quarantine-selected"
12
+ | "service.restart-owned"
13
+ | "service.detach-owned-stale-artifact";
14
+ export interface DoctorCliOptions {
15
+ readonly mode: DoctorMode;
16
+ readonly json: boolean;
17
+ readonly help: boolean;
18
+ readonly checks: readonly string[];
19
+ readonly scope?: DoctorScope;
20
+ readonly timeoutMs?: number;
21
+ readonly repair?: DoctorAction;
22
+ readonly targetId?: string;
23
+ readonly setValue?: boolean;
24
+ readonly ref?: string;
25
+ readonly sha256?: string;
26
+ readonly allowRisks: readonly DoctorRiskClass[];
27
+ readonly yes: boolean;
28
+ readonly drainSeconds?: number;
29
+ }
30
+ export type DoctorConfirmationResult = "confirmed" | "confirmation_declined" | "confirmation_timeout" | "cancelled";
31
+ export interface DoctorOptions extends DoctorCliOptions {
32
+ readonly cwd?: string;
33
+ readonly tty?: boolean;
34
+ readonly signal?: AbortSignal;
35
+ readonly runId?: string;
36
+ readonly deadline?: number;
37
+ /** Internal supervisor channel; never populated from CLI data. */
38
+ readonly confirmRepair?: (repair: DoctorRepair) => Promise<DoctorConfirmationResult>;
39
+ /** Synchronous notification before entering an effect-capable transaction. */
40
+ readonly onRepairAdmitted?: () => void;
41
+ }
42
+ export interface DoctorParseResult {
43
+ readonly options?: DoctorOptions;
44
+ readonly error?: string;
45
+ }
46
+ const ACTION_TARGET_KINDS = {
47
+ "config.set-validated": "config",
48
+ "mcp.set-startup-policy": "mcp",
49
+ "permissions.restrict-owned-config": "permission",
50
+ "install.restore-binary": "binary",
51
+ "install.repair-managed-link": "link",
52
+ "plugin.restore-known-artifact": "plugin",
53
+ "plugin.quarantine-selected": "plugin",
54
+ "service.restart-owned": "service",
55
+ "service.detach-owned-stale-artifact": "artifact",
56
+ } as const satisfies Record<DoctorAction, string>;
57
+ const ACTIONS = new Set(Object.keys(ACTION_TARGET_KINDS));
58
+ export function isDoctorAction(value: unknown): value is DoctorAction {
59
+ return typeof value === "string" && ACTIONS.has(value);
60
+ }
61
+ const RISKS = new Set<DoctorRiskClass>([
62
+ "config-change",
63
+ "permission-change",
64
+ "install-replace",
65
+ "plugin-change",
66
+ "service-interruption",
67
+ "artifact-detach",
68
+ "network",
69
+ "external-execution",
70
+ ]);
71
+ function value(argv: readonly string[], i: number, _flag: string): [string | undefined, number] {
72
+ const v = argv[i + 1];
73
+ return !v || v.startsWith("-") ? [undefined, i] : [v, i + 1];
74
+ }
75
+ export function parseDoctorArgs(argv: readonly string[]): DoctorParseResult {
76
+ let mode: DoctorMode = "diagnose",
77
+ json = false,
78
+ help = false,
79
+ scope: DoctorScope | undefined,
80
+ timeoutMs: number | undefined;
81
+ let repair: DoctorAction | undefined,
82
+ targetId: string | undefined,
83
+ setValue: boolean | undefined,
84
+ ref: string | undefined,
85
+ sha256: string | undefined,
86
+ drainSeconds: number | undefined,
87
+ yes = false;
88
+ const checks = new Set<string>(),
89
+ allowRisks = new Set<DoctorRiskClass>();
90
+ const singleton = new Set([
91
+ "--repair",
92
+ "--target",
93
+ "--set-value-json",
94
+ "--ref",
95
+ "--sha256",
96
+ "--scope",
97
+ "--timeout-ms",
98
+ ]);
99
+ for (let i = 0; i < argv.length; i++) {
100
+ const arg = argv[i];
101
+ if (arg === "--json") {
102
+ json = true;
103
+ continue;
104
+ }
105
+ if (arg === "--help" || arg === "-h") {
106
+ help = true;
107
+ continue;
108
+ }
109
+ if (arg === "--fix") {
110
+ if (mode === "dry-run") continue;
111
+ mode = "fix";
112
+ continue;
113
+ }
114
+ if (arg === "--dry-run") {
115
+ mode = "dry-run";
116
+ continue;
117
+ }
118
+ if (arg === "--yes") {
119
+ yes = true;
120
+ continue;
121
+ }
122
+ const equals = arg.indexOf("=");
123
+ const flag = equals < 0 ? arg : arg.slice(0, equals);
124
+ const raw = equals < 0 ? undefined : arg.slice(equals + 1);
125
+ const hasInline = raw !== undefined;
126
+ if (flag === "--check" || flag === "--allow-risk") {
127
+ const v = hasInline ? raw : value(argv, i, flag)[0];
128
+ if (!v) return { error: `${flag} requires a value` };
129
+ if (!hasInline) i++;
130
+ if (flag === "--check") checks.add(v);
131
+ else {
132
+ if (!RISKS.has(v as DoctorRiskClass)) return { error: "unknown risk class" };
133
+ allowRisks.add(v as DoctorRiskClass);
134
+ }
135
+ continue;
136
+ }
137
+ if (singleton.has(flag)) {
138
+ if (
139
+ (flag === "--repair" && repair) ||
140
+ (flag === "--target" && targetId) ||
141
+ (flag === "--set-value-json" && setValue !== undefined) ||
142
+ (flag === "--ref" && ref) ||
143
+ (flag === "--sha256" && sha256) ||
144
+ (flag === "--scope" && scope) ||
145
+ (flag === "--timeout-ms" && timeoutMs !== undefined)
146
+ )
147
+ return { error: `${flag} may be specified only once` };
148
+ const v = hasInline ? raw : value(argv, i, flag)[0];
149
+ if (!v) return { error: `${flag} requires a value` };
150
+ if (!hasInline) i++;
151
+ if (flag === "--repair") {
152
+ if (!isDoctorAction(v)) return { error: "unknown repair action" };
153
+ repair = v;
154
+ } else if (flag === "--target") targetId = v;
155
+ else if (flag === "--set-value-json") {
156
+ if (v !== "true" && v !== "false") return { error: "--set-value-json accepts only true or false" };
157
+ setValue = v === "true";
158
+ } else if (flag === "--ref") ref = v;
159
+ else if (flag === "--sha256") {
160
+ if (!/^[a-fA-F0-9]{64}$/.test(v)) return { error: "--sha256 must be 64 hexadecimal characters" };
161
+ sha256 = v.toLowerCase();
162
+ } else if (flag === "--scope") {
163
+ if (v !== "user" && v !== "project") return { error: "--scope must be user or project" };
164
+ scope = v;
165
+ } else {
166
+ const n = Number(v);
167
+ if (!Number.isInteger(n) || n < 1000 || n > 120000)
168
+ return { error: "--timeout-ms must be an integer from 1000 to 120000" };
169
+ timeoutMs = n;
170
+ }
171
+ continue;
172
+ }
173
+ if (arg === "--drain") {
174
+ if (drainSeconds !== undefined) return { error: "--drain may be specified only once" };
175
+ drainSeconds = 30;
176
+ continue;
177
+ }
178
+ if (arg.startsWith("--")) return { error: "unknown option" };
179
+ return { error: "unexpected argument" };
180
+ }
181
+ if (mode === "fix" && (!repair || !targetId)) return { error: "--fix requires --repair and --target" };
182
+ if (mode === "dry-run" && repair !== undefined && targetId === undefined)
183
+ return { error: "--repair requires --target" };
184
+ if (drainSeconds !== undefined && repair !== "service.restart-owned")
185
+ return { error: "--drain is only valid for service.restart-owned" };
186
+ if (targetId !== undefined && !isDoctorTargetId(targetId))
187
+ return { error: "target must be a stable doctor target ID" };
188
+ if (repair && targetId && targetId.split(":")[1] !== ACTION_TARGET_KINDS[repair])
189
+ return { error: "repair action does not match target kind" };
190
+ if (scope && targetId) {
191
+ const parts = targetId.split(":");
192
+ const targetScope = parts.includes("user") ? "user" : parts.includes("project") ? "project" : undefined;
193
+ if (!targetScope) return { error: "--scope is unsupported for this target" };
194
+ if (targetScope !== scope) return { error: "--scope does not match target scope" };
195
+ }
196
+ if (repair === "config.set-validated" || repair === "mcp.set-startup-policy") {
197
+ if (setValue === undefined) return { error: "this action requires --set-value-json true|false" };
198
+ }
199
+ if (repair !== "config.set-validated" && repair !== "mcp.set-startup-policy" && setValue !== undefined)
200
+ return { error: "--set-value-json is only valid for config or MCP repairs" };
201
+ if (repair !== "install.restore-binary" && repair !== "plugin.restore-known-artifact" && sha256 !== undefined)
202
+ return { error: "--sha256 is only valid for binary or plugin restore" };
203
+ if (
204
+ repair !== "install.restore-binary" &&
205
+ repair !== "plugin.restore-known-artifact" &&
206
+ ref !== undefined &&
207
+ repair !== "install.repair-managed-link"
208
+ )
209
+ return { error: "--ref is not valid for this repair" };
210
+ if (repair === "install.repair-managed-link" && mode === "fix" && !ref)
211
+ return { error: "install.repair-managed-link requires --ref" };
212
+ if (
213
+ repair === "install.repair-managed-link" &&
214
+ ref !== undefined &&
215
+ !/^c1:link:r[0-9a-f]{64}:(source|wrapper|binary):p[0-9a-f]{64}$/.test(ref)
216
+ )
217
+ return { error: "--ref must be a c1 link candidate ID" };
218
+ return {
219
+ options: {
220
+ mode,
221
+ json,
222
+ help,
223
+ checks: [...checks],
224
+ scope,
225
+ timeoutMs,
226
+ repair,
227
+ targetId,
228
+ setValue,
229
+ ref,
230
+ sha256,
231
+ allowRisks: [...allowRisks],
232
+ yes,
233
+ drainSeconds,
234
+ },
235
+ };
236
+ }