@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
@@ -0,0 +1,571 @@
1
+ /**
2
+ * Doctor bootstrap isolation supervisor (parent side).
3
+ *
4
+ * `gjc doctor` must stay usable when the normal runtime is broken (malformed
5
+ * config, a faulting native addon, an unbounded filesystem stall). A
6
+ * `Promise.race` inside the in-process runner cannot bound synchronous or
7
+ * native work (fs realpath/read, native addon calls, fsync) — only an owned
8
+ * child-process boundary can. This module owns that boundary from the
9
+ * parent's side; the child worker entry (imports `./doctor/runner` and is
10
+ * re-entered via `cli.ts`'s `--internal-doctor-worker` admission branch) is
11
+ * implemented in `doctor-worker.ts`.
12
+ *
13
+ * Protocol: newline-delimited JSON over the child's stdin/stdout, each
14
+ * message authenticated by a random per-run token delivered only via
15
+ * `DOCTOR_WORKER_TOKEN_ENV` (never argv, never logged, never in the report).
16
+ * Token comparison is constant-time. The child's stdio is fully owned by
17
+ * this protocol, so the interactive confirmation prompt (default No, exact
18
+ * action/target/value, SIGINT- and deadline-aware) is rendered by THIS
19
+ * parent against the real terminal — the child never touches the TTY.
20
+ *
21
+ * "Repair admitted" is a permanent fence: it is latched synchronously the
22
+ * instant the `admitted` message is observed, before any further await, and
23
+ * is never cleared. Any abnormal ending after admission is reported as an
24
+ * uncertain post-effect outcome (exit 4); before admission it is "nothing
25
+ * started" (exit 3), and a runtime that itself was admitted-then-lost is
26
+ * evidenced with `worker_termination_unverified`, never a silently clean
27
+ * exit. SIGINT is forwarded to the child; a verified clean stop after SIGINT
28
+ * and before admission reports 130, never fabricated merely because a
29
+ * `Promise.race` resolved. No successor worker is ever spawned after an
30
+ * unverified termination.
31
+ */
32
+
33
+ import type { ChildProcessByStdio } from "node:child_process";
34
+ import * as childProcess from "node:child_process";
35
+ import { randomBytes, randomUUID, timingSafeEqual } from "node:crypto";
36
+ import * as path from "node:path";
37
+ import * as readline from "node:readline";
38
+ import { createInterface as createPromptInterface } from "node:readline/promises";
39
+ import type { Readable, Writable } from "node:stream";
40
+ import { fileURLToPath } from "node:url";
41
+ import { VERSION } from "@gajae-code/utils/dirs";
42
+ import type { DoctorAction, DoctorConfirmationResult, DoctorOptions } from "./doctor/args";
43
+ import { DOCTOR_EXIT_CODES, finalizeReport } from "./doctor/report";
44
+ import type { DoctorMode, DoctorRepair, DoctorReport } from "./doctor/types";
45
+
46
+ /** Re-entrant argv marker for the isolated doctor worker. Wiring into `cli.ts`'s admission chain is a coordinated follow-up. */
47
+ export const DOCTOR_WORKER_ARG = "--internal-doctor-worker";
48
+ /** Per-spawn random token; the worker must refuse to run the protocol route without an exact match. */
49
+ export const DOCTOR_WORKER_TOKEN_ENV = "GJC_DOCTOR_WORKER_TOKEN";
50
+
51
+ const DIAGNOSE_BUDGET_MS = 15_000;
52
+ const SHORT_REPAIR_BUDGET_MS = 10_000;
53
+ const LONG_REPAIR_BUDGET_MS = 120_000;
54
+ const SERVICE_RESTART_BASE_BUDGET_MS = 15_000 + 15_000;
55
+ const TERMINATION_GRACE_MS = 2_000;
56
+
57
+ const WORKER_ENV_KEYS = [
58
+ "PATH",
59
+ "HOME",
60
+ "USERPROFILE",
61
+ "SYSTEMROOT",
62
+ "WINDIR",
63
+ "TMPDIR",
64
+ "TEMP",
65
+ "TMP",
66
+ "LANG",
67
+ "LC_ALL",
68
+ "TZ",
69
+ "GJC_CONFIG_DIR",
70
+ "PI_CONFIG_DIR",
71
+ "GJC_CODING_AGENT_DIR",
72
+ "PI_CODING_AGENT_DIR",
73
+ "XDG_CONFIG_HOME",
74
+ "XDG_DATA_HOME",
75
+ "XDG_STATE_HOME",
76
+ "XDG_CACHE_HOME",
77
+ ] as const;
78
+
79
+ const COMPILED = /(?:\/\$bunfs\/|\/~BUN\/|\/%7EBUN\/)/i.test(import.meta.url);
80
+
81
+ export interface SupervisedDoctorInput {
82
+ /** Raw argv following `doctor` (unparsed). The worker re-derives options and re-runs full security admission itself. */
83
+ readonly argv: readonly string[];
84
+ /** Parsed options for local budget/cwd/tty decisions only; `confirmRepair`/`onRepairAdmitted` here are ignored — the parent supplies live protocol-backed equivalents. */
85
+ readonly options: DoctorOptions;
86
+ }
87
+
88
+ export type WorkerToSupervisorMessage =
89
+ | {
90
+ readonly type: "confirm";
91
+ readonly token: string;
92
+ readonly id: number;
93
+ readonly repair: DoctorRepair;
94
+ readonly setValue?: boolean;
95
+ }
96
+ | { readonly type: "admitted"; readonly token: string }
97
+ | { readonly type: "report"; readonly token: string; readonly report: DoctorReport }
98
+ | { readonly type: "error"; readonly token: string; readonly code: string; readonly message: string };
99
+ /** Distributes `Omit` over the message union so each variant keeps its own extra fields (plain `Omit<Union, K>` only keeps keys common to every member). */
100
+ export type WorkerToSupervisorPayload = WorkerToSupervisorMessage extends infer M
101
+ ? M extends { readonly token: string }
102
+ ? Omit<M, "token">
103
+ : never
104
+ : never;
105
+
106
+ export type SupervisorToWorkerMessage =
107
+ | {
108
+ readonly type: "init";
109
+ readonly token: string;
110
+ readonly argv: readonly string[];
111
+ readonly cwd: string;
112
+ readonly tty: boolean;
113
+ readonly runId: string;
114
+ readonly timeoutMs: number;
115
+ readonly deadlineAt: number;
116
+ }
117
+ | {
118
+ readonly type: "confirm-result";
119
+ readonly token: string;
120
+ readonly id: number;
121
+ readonly result: DoctorConfirmationResult;
122
+ };
123
+
124
+ function actionCeilingMs(action: DoctorAction, drainSeconds: number | undefined): number {
125
+ switch (action) {
126
+ case "config.set-validated":
127
+ case "mcp.set-startup-policy":
128
+ case "permissions.restrict-owned-config":
129
+ case "install.repair-managed-link":
130
+ case "service.detach-owned-stale-artifact":
131
+ return SHORT_REPAIR_BUDGET_MS;
132
+ case "install.restore-binary":
133
+ case "plugin.restore-known-artifact":
134
+ case "plugin.quarantine-selected":
135
+ return LONG_REPAIR_BUDGET_MS;
136
+ case "service.restart-owned":
137
+ return SERVICE_RESTART_BASE_BUDGET_MS + (drainSeconds ? drainSeconds * 1_000 : 0);
138
+ }
139
+ }
140
+
141
+ /** An explicit `--timeout-ms` may only shorten the action-specific ceiling, never raise it. */
142
+ export function resolveSupervisedBudgetMs(options: DoctorOptions): number {
143
+ const ceiling =
144
+ options.mode === "fix" && options.repair
145
+ ? actionCeilingMs(options.repair, options.drainSeconds)
146
+ : DIAGNOSE_BUDGET_MS;
147
+ return options.timeoutMs !== undefined ? Math.min(options.timeoutMs, ceiling) : ceiling;
148
+ }
149
+
150
+ function resolveWorkerSpawnArgv(): string[] | undefined {
151
+ if (COMPILED) return [process.execPath, DOCTOR_WORKER_ARG];
152
+ try {
153
+ const cliPath = fileURLToPath(new URL("../cli.ts", import.meta.url));
154
+ const configPath = fileURLToPath(new URL("../sdk/broker/internal-source.bunfig.toml", import.meta.url));
155
+ if (!path.isAbsolute(cliPath) || !path.isAbsolute(configPath)) return undefined;
156
+ return [process.execPath, "--no-env-file", `--config=${configPath}`, cliPath, DOCTOR_WORKER_ARG];
157
+ } catch {
158
+ return undefined;
159
+ }
160
+ }
161
+
162
+ function workerEnvironment(token: string): NodeJS.ProcessEnv {
163
+ const environment: NodeJS.ProcessEnv = {
164
+ NO_COLOR: "1",
165
+ BUN_RUNTIME_TRANSPILER_CACHE_PATH: "0",
166
+ [DOCTOR_WORKER_TOKEN_ENV]: token,
167
+ };
168
+ for (const key of WORKER_ENV_KEYS) {
169
+ const value = process.env[key];
170
+ if (value !== undefined) environment[key] = value;
171
+ }
172
+ return environment;
173
+ }
174
+
175
+ /** Length is compared before the constant-time byte compare so a length mismatch is not itself a timing side channel on the byte contents. */
176
+ function tokenMatches(received: string, expected: string): boolean {
177
+ const receivedBuffer = Buffer.from(received, "utf8");
178
+ const expectedBuffer = Buffer.from(expected, "utf8");
179
+ return receivedBuffer.length === expectedBuffer.length && timingSafeEqual(receivedBuffer, expectedBuffer);
180
+ }
181
+
182
+ /**
183
+ * Structural gate for everything the supervisor dereferences afterwards. The
184
+ * `selection` envelope is validated here rather than at the use site because
185
+ * the readline handler reads `selection.repair`/`selection.targetId` directly:
186
+ * a worker that emits a report without a well-formed selection must be
187
+ * classified `worker_report_invalid`, never throw inside the `line` listener
188
+ * (which would escape the protocol and take the parent down).
189
+ */
190
+ function isPlausibleDoctorReport(value: unknown): value is DoctorReport {
191
+ if (!value || typeof value !== "object") return false;
192
+ const report = value as Partial<DoctorReport>;
193
+ const summary = report.summary as { exitCode?: unknown } | undefined;
194
+ const selection = report.selection as { checks?: unknown; repair?: unknown; targetId?: unknown } | undefined;
195
+ return (
196
+ report.schemaVersion === 1 &&
197
+ report.command === "doctor" &&
198
+ typeof report.runId === "string" &&
199
+ typeof report.mode === "string" &&
200
+ typeof summary === "object" &&
201
+ summary !== null &&
202
+ typeof summary.exitCode === "number" &&
203
+ typeof selection === "object" &&
204
+ selection !== null &&
205
+ !Array.isArray(selection) &&
206
+ Array.isArray(selection.checks) &&
207
+ (selection.repair === undefined || typeof selection.repair === "string") &&
208
+ (selection.targetId === undefined || typeof selection.targetId === "string") &&
209
+ Array.isArray(report.checks) &&
210
+ Array.isArray(report.repairs)
211
+ );
212
+ }
213
+
214
+ /**
215
+ * Accept a worker `report` message only when it is both structurally complete
216
+ * and bound to this exact run. Returns the report on success so callers never
217
+ * re-narrow it, and `undefined` for every rejected shape — including a missing
218
+ * or malformed `selection`, which used to throw at the dereference.
219
+ */
220
+ export function acceptWorkerReport(
221
+ value: unknown,
222
+ expected: {
223
+ readonly runId: string;
224
+ readonly mode: DoctorMode;
225
+ readonly repair?: string;
226
+ readonly targetId?: string;
227
+ },
228
+ ): DoctorReport | undefined {
229
+ if (!isPlausibleDoctorReport(value)) return undefined;
230
+ if (value.runId !== expected.runId || value.mode !== expected.mode) return undefined;
231
+ if (value.selection.repair !== expected.repair || value.selection.targetId !== expected.targetId) return undefined;
232
+ return value;
233
+ }
234
+
235
+ /**
236
+ * Write side of the parent→worker protocol.
237
+ *
238
+ * The channel is a one-way latch: once the worker has settled (exit, close, or
239
+ * a pipe error) no further byte is written to its stdin. A confirmation prompt
240
+ * can resolve arbitrarily late — after the deadline fired, after SIGINT, after
241
+ * the worker crashed — and its callback would otherwise write to a closed pipe,
242
+ * surfacing as an EPIPE / ERR_STREAM_DESTROYED that crashes the parent. The
243
+ * latch check and the write happen in one synchronous step, so no stream event
244
+ * can interleave between them, and any residual race is contained: it becomes a
245
+ * worker-failure outcome instead of an exception.
246
+ */
247
+ export interface SupervisorChannel {
248
+ readonly settled: boolean;
249
+ /** Latch the channel closed without reporting a failure (normal termination). */
250
+ settle(): void;
251
+ send(message: SupervisorToWorkerMessage): void;
252
+ }
253
+
254
+ export function createSupervisorChannel(stdin: Writable, onChannelClosed: () => void): SupervisorChannel {
255
+ let settled = false;
256
+ const fail = (): void => {
257
+ settled = true;
258
+ onChannelClosed();
259
+ };
260
+ // An asynchronous pipe teardown arrives as a stream `error` event, not as a
261
+ // throw from `write`; without this listener the stream emits an unhandled
262
+ // 'error' and terminates the parent process.
263
+ stdin.on("error", fail);
264
+ return {
265
+ get settled() {
266
+ return settled;
267
+ },
268
+ settle() {
269
+ settled = true;
270
+ },
271
+ send(message) {
272
+ if (settled || stdin.destroyed || !stdin.writable) {
273
+ fail();
274
+ return;
275
+ }
276
+ try {
277
+ stdin.write(`${JSON.stringify(message)}\n`);
278
+ } catch {
279
+ fail();
280
+ }
281
+ },
282
+ };
283
+ }
284
+
285
+ function synthesizedReport(input: {
286
+ readonly options: DoctorOptions;
287
+ readonly runId: string;
288
+ readonly started: number;
289
+ readonly exitCode: number;
290
+ readonly invocationError: string;
291
+ readonly interrupted?: boolean;
292
+ }): DoctorReport {
293
+ const report = finalizeReport({
294
+ runId: input.runId,
295
+ mode: input.options.mode,
296
+ generatedAt: new Date().toISOString(),
297
+ durationMs: Math.round(performance.now() - input.started),
298
+ subject: {
299
+ gjcVersion: VERSION,
300
+ platform: process.platform,
301
+ arch: process.arch,
302
+ channel: COMPILED ? "standalone" : "source",
303
+ scope: input.options.scope,
304
+ rootIds: [],
305
+ },
306
+ selection: { checks: [], repair: input.options.repair, targetId: input.options.targetId },
307
+ coverage: { requested: 0, expanded: 0, attempted: 0, completed: 0, blocked: 0, timedOut: 0, unsupported: 0 },
308
+ checks: [],
309
+ repairs: [],
310
+ invocationError: input.invocationError,
311
+ interrupted: input.interrupted,
312
+ });
313
+ // The synthesized envelope always carries a concrete supervisor-decided exit code
314
+ // (admission-fenced, never a bare Promise.race artifact); finalizeReport's own
315
+ // exit derivation from empty coverage/checks is not authoritative here.
316
+ return { ...report, summary: { ...report.summary, exitCode: input.exitCode } };
317
+ }
318
+
319
+ /** Real terminal confirmation prompt, rendered by the parent against its own stdio (never the child's). */
320
+ async function promptForConfirmation(
321
+ repair: DoctorRepair,
322
+ setValue: boolean | undefined,
323
+ deadline: number,
324
+ sigintSignal: AbortSignal,
325
+ ): Promise<DoctorConfirmationResult> {
326
+ const terminal = createPromptInterface({ input: process.stdin, output: process.stderr });
327
+ const remaining = Math.max(1, Math.floor(deadline - performance.now()));
328
+ const timeoutSignal = AbortSignal.timeout(remaining);
329
+ try {
330
+ const answer = await terminal.question(
331
+ `Apply ${repair.id} to ${repair.targetId}${setValue === undefined ? "" : `, value ${setValue}`}? [y/N] `,
332
+ { signal: AbortSignal.any([sigintSignal, timeoutSignal]) },
333
+ );
334
+ return /^(y|yes)$/i.test(answer.trim()) ? "confirmed" : "confirmation_declined";
335
+ } catch (error) {
336
+ if (error instanceof Error && error.name === "AbortError")
337
+ return sigintSignal.aborted ? "cancelled" : "confirmation_timeout";
338
+ return "confirmation_declined";
339
+ } finally {
340
+ terminal.close();
341
+ }
342
+ }
343
+
344
+ /**
345
+ * Run doctor diagnostics/repair inside an owned, verified-terminated child.
346
+ * Never falls back to in-process execution and never spawns a successor
347
+ * worker after this attempt's child is gone.
348
+ */
349
+ export async function runSupervisedDoctor(input: SupervisedDoctorInput): Promise<DoctorReport> {
350
+ const started = performance.now();
351
+ const runId = randomUUID();
352
+ const options = input.options;
353
+ const budgetMs = resolveSupervisedBudgetMs(options);
354
+ const deadline = started + budgetMs;
355
+ const spawnArgv = resolveWorkerSpawnArgv();
356
+ if (!spawnArgv)
357
+ return synthesizedReport({
358
+ options,
359
+ runId,
360
+ started,
361
+ exitCode: DOCTOR_EXIT_CODES.incomplete,
362
+ invocationError: "worker_runtime_identity_unavailable",
363
+ });
364
+
365
+ const token = randomBytes(32).toString("hex");
366
+ const [command, ...args] = spawnArgv;
367
+ let child: ChildProcessByStdio<Writable, Readable, null>;
368
+ try {
369
+ child = childProcess.spawn(command!, args, {
370
+ cwd: options.cwd ?? process.cwd(),
371
+ env: workerEnvironment(token),
372
+ stdio: ["pipe", "pipe", "ignore"],
373
+ windowsHide: true,
374
+ });
375
+ } catch {
376
+ return synthesizedReport({
377
+ options,
378
+ runId,
379
+ started,
380
+ exitCode: DOCTOR_EXIT_CODES.incomplete,
381
+ invocationError: "worker_spawn_failed",
382
+ });
383
+ }
384
+
385
+ // Latched exactly once, synchronously, on first observation of "admitted" —
386
+ // never cleared afterward regardless of any later cancel/timeout/signal.
387
+ let admitted = false;
388
+ let childExited = false;
389
+ let settled = false;
390
+ let finalReport: DoctorReport | undefined;
391
+ let abnormalReasonCode: string | undefined;
392
+ const outcome = Promise.withResolvers<void>();
393
+
394
+ const finishOnce = (report: DoctorReport | undefined, reasonCode: string | undefined): void => {
395
+ if (settled) return;
396
+ settled = true;
397
+ finalReport = report;
398
+ abnormalReasonCode = reasonCode;
399
+ outcome.resolve();
400
+ };
401
+
402
+ const channel = createSupervisorChannel(child.stdin, () => finishOnce(undefined, "worker_channel_closed"));
403
+ const send = channel.send;
404
+
405
+ const exitEvent = Promise.withResolvers<void>();
406
+ child.once("exit", () => {
407
+ childExited = true;
408
+ channel.settle();
409
+ exitEvent.resolve();
410
+ });
411
+ const waitForExit = async (milliseconds: number): Promise<boolean> => {
412
+ if (childExited) return true;
413
+ const timeout = Promise.withResolvers<boolean>();
414
+ const timer = setTimeout(() => timeout.resolve(false), milliseconds);
415
+ try {
416
+ return await Promise.race([exitEvent.promise.then(() => true), timeout.promise]);
417
+ } finally {
418
+ clearTimeout(timer);
419
+ }
420
+ };
421
+
422
+ const sigintController = new AbortController();
423
+ const onSigint = (): void => {
424
+ sigintController.abort();
425
+ try {
426
+ child.kill("SIGINT");
427
+ } catch {
428
+ // Child already gone.
429
+ }
430
+ };
431
+ process.on("SIGINT", onSigint);
432
+
433
+ const lineReader = readline.createInterface({ input: child.stdout, crlfDelay: Infinity });
434
+ lineReader.on("line", line => {
435
+ let message: WorkerToSupervisorMessage;
436
+ try {
437
+ message = JSON.parse(line) as WorkerToSupervisorMessage;
438
+ } catch {
439
+ finishOnce(undefined, "worker_protocol_invalid");
440
+ return;
441
+ }
442
+ if (typeof message.token !== "string" || !tokenMatches(message.token, token)) {
443
+ finishOnce(undefined, "worker_protocol_unauthenticated");
444
+ return;
445
+ }
446
+ if (message.type === "admitted") {
447
+ admitted = true;
448
+ return;
449
+ }
450
+ if (message.type === "confirm") {
451
+ // A confirmation that arrives after admission is refused, never defaulted to yes:
452
+ // the repair has already crossed into effect-capable territory.
453
+ if (admitted) {
454
+ send({ type: "confirm-result", token, id: message.id, result: "cancelled" });
455
+ return;
456
+ }
457
+ void promptForConfirmation(message.repair, message.setValue, deadline, sigintController.signal).then(
458
+ result => {
459
+ send({ type: "confirm-result", token, id: message.id, result });
460
+ },
461
+ );
462
+ return;
463
+ }
464
+ if (message.type === "report") {
465
+ const accepted = acceptWorkerReport(message.report, {
466
+ runId,
467
+ mode: options.mode,
468
+ repair: options.repair,
469
+ targetId: options.targetId,
470
+ });
471
+ if (!accepted) {
472
+ finishOnce(undefined, "worker_report_invalid");
473
+ return;
474
+ }
475
+ finishOnce(accepted, undefined);
476
+ return;
477
+ }
478
+ if (message.type === "error") {
479
+ finishOnce(undefined, "worker_reported_error");
480
+ }
481
+ });
482
+
483
+ send({
484
+ type: "init",
485
+ token,
486
+ argv: input.argv,
487
+ cwd: options.cwd ?? process.cwd(),
488
+ tty: Boolean(options.tty),
489
+ runId,
490
+ timeoutMs: budgetMs,
491
+ deadlineAt: Date.now() + Math.max(1, deadline - performance.now()),
492
+ });
493
+
494
+ // `close` fires only after stdout/stderr are fully drained, so a report/error
495
+ // line the worker flushed right before exiting is always processed by the
496
+ // line reader above BEFORE this fallback can fire (matches the bash-shell
497
+ // supervisor's own close-vs-exit ordering rationale).
498
+ child.once("close", () => {
499
+ channel.settle();
500
+ finishOnce(undefined, "worker_exited_without_report");
501
+ });
502
+ child.once("error", () => {
503
+ finishOnce(undefined, "worker_spawn_failed");
504
+ });
505
+
506
+ const timeoutHandle = setTimeout(
507
+ () => finishOnce(undefined, "worker_deadline_exceeded"),
508
+ Math.max(0, deadline - performance.now()),
509
+ );
510
+ try {
511
+ await outcome.promise;
512
+ } finally {
513
+ clearTimeout(timeoutHandle);
514
+ process.off("SIGINT", onSigint);
515
+ }
516
+
517
+ // Verified-termination sequence: never leave the worker running, never spawn a
518
+ // successor, and never report success/interruption without confirming the exit.
519
+ if (finalReport) child.stdin.end();
520
+ let terminationVerified = childExited || (finalReport !== undefined && (await waitForExit(200)));
521
+ if (!terminationVerified) {
522
+ try {
523
+ child.kill("SIGTERM");
524
+ terminationVerified = await waitForExit(TERMINATION_GRACE_MS);
525
+ } catch {
526
+ terminationVerified = false;
527
+ }
528
+ }
529
+ if (!terminationVerified) {
530
+ try {
531
+ child.kill("SIGKILL");
532
+ terminationVerified = await waitForExit(TERMINATION_GRACE_MS);
533
+ } catch {
534
+ terminationVerified = false;
535
+ }
536
+ }
537
+ try {
538
+ child.stdin.destroy();
539
+ } catch {
540
+ // Already closed.
541
+ }
542
+
543
+ if (finalReport && terminationVerified && !abnormalReasonCode) return finalReport;
544
+
545
+ if (!terminationVerified) {
546
+ return synthesizedReport({
547
+ options,
548
+ runId,
549
+ started,
550
+ exitCode: admitted ? DOCTOR_EXIT_CODES.mutationFailure : DOCTOR_EXIT_CODES.incomplete,
551
+ invocationError: "worker_termination_unverified",
552
+ });
553
+ }
554
+ if (sigintController.signal.aborted && !admitted) {
555
+ return synthesizedReport({
556
+ options,
557
+ runId,
558
+ started,
559
+ exitCode: DOCTOR_EXIT_CODES.interrupted,
560
+ invocationError: "worker_interrupted",
561
+ interrupted: true,
562
+ });
563
+ }
564
+ return synthesizedReport({
565
+ options,
566
+ runId,
567
+ started,
568
+ exitCode: admitted ? DOCTOR_EXIT_CODES.mutationFailure : DOCTOR_EXIT_CODES.incomplete,
569
+ invocationError: abnormalReasonCode ?? "worker_terminated_without_report",
570
+ });
571
+ }