@gajae-code/coding-agent 0.18.11 → 0.19.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 (462) hide show
  1. package/CHANGELOG.md +760 -0
  2. package/dist/types/async/job-manager.d.ts +7 -3
  3. package/dist/types/async/result-formatting.d.ts +2 -0
  4. package/dist/types/autoresearch/git.d.ts +1 -1
  5. package/dist/types/capability/fs.d.ts +2 -2
  6. package/dist/types/capability/rule.d.ts +7 -2
  7. package/dist/types/cli/accounts-cli.d.ts +2 -2
  8. package/dist/types/cli/append-system-prompt.d.ts +6 -0
  9. package/dist/types/cli/args.d.ts +7 -2
  10. package/dist/types/cli/customize-doctor-precedence.d.ts +16 -0
  11. package/dist/types/cli/customize-doctor.d.ts +4 -3
  12. package/dist/types/cli/doctor/install-repairs.d.ts +4 -1
  13. package/dist/types/cli/install-activation.d.ts +10 -4
  14. package/dist/types/cli/public-command-errors.d.ts +2 -0
  15. package/dist/types/cli/root-flags.d.ts +0 -12
  16. package/dist/types/cli/update-cli.d.ts +21 -3
  17. package/dist/types/cli-main.d.ts +0 -6
  18. package/dist/types/cli-ordinary.d.ts +2 -2
  19. package/dist/types/commands/accounts.d.ts +2 -1
  20. package/dist/types/commands/launch.d.ts +0 -6
  21. package/dist/types/commands/migrate.d.ts +1 -1
  22. package/dist/types/commands/sdk.d.ts +13 -0
  23. package/dist/types/commit/changelog/index.d.ts +1 -0
  24. package/dist/types/commit/map-reduce/index.d.ts +4 -4
  25. package/dist/types/config/autorouting-tier-map.d.ts +53 -1
  26. package/dist/types/config/config-hot-reload-trust.d.ts +12 -0
  27. package/dist/types/config/config-hot-reload.d.ts +44 -0
  28. package/dist/types/config/file-lock.d.ts +52 -3
  29. package/dist/types/config/model-discovery-manager.d.ts +2 -0
  30. package/dist/types/config/model-profile-activation.d.ts +79 -9
  31. package/dist/types/config/model-registry.d.ts +72 -8
  32. package/dist/types/config/model-resolver.d.ts +7 -4
  33. package/dist/types/config/models-config-schema.d.ts +10 -0
  34. package/dist/types/config/settings-schema.d.ts +57 -4
  35. package/dist/types/config/settings.d.ts +33 -1
  36. package/dist/types/coordinator-mcp/server.d.ts +4 -0
  37. package/dist/types/customization/import.d.ts +2 -0
  38. package/dist/types/customization/types.d.ts +1 -1
  39. package/dist/types/dap/config.d.ts +8 -4
  40. package/dist/types/dap/session.d.ts +11 -2
  41. package/dist/types/debug/system-info.d.ts +6 -1
  42. package/dist/types/discovery/helpers.d.ts +7 -2
  43. package/dist/types/discovery/mcp-compat.d.ts +3 -1
  44. package/dist/types/edit/index.d.ts +5 -0
  45. package/dist/types/edit/modes/patch.d.ts +14 -0
  46. package/dist/types/edit/modes/replace.d.ts +3 -0
  47. package/dist/types/eval/py/runtime.d.ts +9 -0
  48. package/dist/types/exec/bash-executor.d.ts +12 -0
  49. package/dist/types/export/ttsr.d.ts +19 -0
  50. package/dist/types/extensibility/extensions/types.d.ts +5 -1
  51. package/dist/types/extensibility/slash-commands.d.ts +1 -0
  52. package/dist/types/gjc-runtime/managed-owner-admission.d.ts +3 -0
  53. package/dist/types/gjc-runtime/ralplan-current-final.d.ts +54 -0
  54. package/dist/types/gjc-runtime/ralplan-runtime.d.ts +5 -0
  55. package/dist/types/gjc-runtime/session-state-lock.d.ts +2 -0
  56. package/dist/types/gjc-runtime/state-runtime.d.ts +0 -2
  57. package/dist/types/gjc-runtime/tmux-sessions.d.ts +24 -0
  58. package/dist/types/gjc-runtime/ultragoal-change-set.d.ts +6 -0
  59. package/dist/types/gjc-runtime/ultragoal-runtime.d.ts +1 -0
  60. package/dist/types/harness-control-plane/receipt-spool.d.ts +4 -0
  61. package/dist/types/hindsight/content.d.ts +5 -6
  62. package/dist/types/internal-urls/types.d.ts +2 -0
  63. package/dist/types/lsp/clients/swiftlint-client.d.ts +0 -4
  64. package/dist/types/main.d.ts +23 -18
  65. package/dist/types/migrate/action-planner.d.ts +10 -1
  66. package/dist/types/migrate/adapters/codex.d.ts +2 -1
  67. package/dist/types/migrate/adapters/index.d.ts +42 -1
  68. package/dist/types/migrate/executor.d.ts +3 -1
  69. package/dist/types/migrate/hook-module.d.ts +23 -0
  70. package/dist/types/migrate/report.d.ts +2 -1
  71. package/dist/types/migrate/types.d.ts +82 -3
  72. package/dist/types/modes/components/agent-dashboard.d.ts +1 -1
  73. package/dist/types/modes/components/model-selector.d.ts +43 -0
  74. package/dist/types/modes/components/settings-selector.d.ts +6 -0
  75. package/dist/types/modes/controllers/selector-controller.d.ts +2 -0
  76. package/dist/types/modes/prompt-suggestion-controller.d.ts +1 -1
  77. package/dist/types/modes/theme/theme.d.ts +1 -0
  78. package/dist/types/modes/utils/host-status.d.ts +15 -11
  79. package/dist/types/runtime-mcp/content-limits.d.ts +4 -0
  80. package/dist/types/runtime-mcp/manager.d.ts +7 -0
  81. package/dist/types/runtime-mcp/plugin-mcp-name-filter.d.ts +22 -0
  82. package/dist/types/runtime-mcp/project-host-values.d.ts +11 -5
  83. package/dist/types/runtime-mcp/tool-bridge.d.ts +4 -0
  84. package/dist/types/sdk/broker/broker-exit.d.ts +3 -3
  85. package/dist/types/sdk/broker/broker.d.ts +29 -2
  86. package/dist/types/sdk/broker/daemon-entry.d.ts +11 -1
  87. package/dist/types/sdk/broker/doctor-restart.d.ts +1 -1
  88. package/dist/types/sdk/broker/ensure.d.ts +141 -9
  89. package/dist/types/sdk/broker/hop.d.ts +45 -0
  90. package/dist/types/sdk/broker/lifecycle-ledger.d.ts +18 -10
  91. package/dist/types/sdk/broker/lifecycle.d.ts +22 -0
  92. package/dist/types/sdk/broker/lock-host-identity.d.ts +5 -0
  93. package/dist/types/sdk/broker/session-index.d.ts +6 -1
  94. package/dist/types/sdk/broker/transport.d.ts +2 -0
  95. package/dist/types/sdk/bus/chat-command-policy.d.ts +2 -1
  96. package/dist/types/sdk/bus/index.d.ts +44 -0
  97. package/dist/types/sdk/bus/reconciliation-store.d.ts +10 -0
  98. package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +9 -1
  99. package/dist/types/sdk/bus/telegram-daemon.d.ts +2 -0
  100. package/dist/types/sdk/bus/threaded-render.d.ts +2 -1
  101. package/dist/types/sdk/cli/session-cli.d.ts +0 -1
  102. package/dist/types/sdk/host/control/operations.d.ts +5 -1
  103. package/dist/types/sdk/host/events.d.ts +4 -0
  104. package/dist/types/sdk/host/host.d.ts +2 -1
  105. package/dist/types/sdk/host/prompt-image-upload.d.ts +43 -0
  106. package/dist/types/sdk/host/session-runtime.d.ts +40 -8
  107. package/dist/types/sdk/host/surface-policy.d.ts +2 -1
  108. package/dist/types/sdk/lifecycle/service.d.ts +12 -0
  109. package/dist/types/sdk/prompt-deadline-flush.d.ts +42 -4
  110. package/dist/types/sdk/prompt-deadline-manager.d.ts +2 -0
  111. package/dist/types/sdk/prompt-status.d.ts +11 -4
  112. package/dist/types/sdk/protocol/adapter-validation.d.ts +10 -4
  113. package/dist/types/sdk/reconciliation-extensions.d.ts +1 -1
  114. package/dist/types/sdk/session-list.d.ts +1 -1
  115. package/dist/types/session/agent-session.d.ts +107 -9
  116. package/dist/types/session/internal/managed-session-scope.d.ts +2 -2
  117. package/dist/types/session/internal/managed-session-storage.d.ts +1 -1
  118. package/dist/types/session/internal/native-publish-outcome.d.ts +1 -1
  119. package/dist/types/session/messages.d.ts +5 -1
  120. package/dist/types/session/session-manager.d.ts +29 -3
  121. package/dist/types/session/startup-auth-config.d.ts +24 -2
  122. package/dist/types/session/streaming-output.d.ts +11 -0
  123. package/dist/types/session/terminal-abort.d.ts +3 -1
  124. package/dist/types/session-import/service.d.ts +1 -1
  125. package/dist/types/skill-state/workflow-mutation-guard.d.ts +20 -4
  126. package/dist/types/slash-commands/helpers/mcp.d.ts +5 -0
  127. package/dist/types/slash-commands/helpers/provider-auth-status.d.ts +8 -0
  128. package/dist/types/ssh/ssh-executor.d.ts +8 -1
  129. package/dist/types/system-prompt.d.ts +3 -2
  130. package/dist/types/task/executor.d.ts +19 -2
  131. package/dist/types/task/role-body.d.ts +11 -0
  132. package/dist/types/tools/ast-edit-write-path.d.ts +10 -0
  133. package/dist/types/tools/atomic-file-write.d.ts +9 -7
  134. package/dist/types/tools/awaiting-input.d.ts +28 -0
  135. package/dist/types/tools/bash-allowed-prefixes.d.ts +17 -0
  136. package/dist/types/tools/bash-skill-urls.d.ts +4 -0
  137. package/dist/types/tools/bash.d.ts +7 -0
  138. package/dist/types/tools/descriptors.d.ts +15 -2
  139. package/dist/types/tools/fetch.d.ts +7 -1
  140. package/dist/types/tools/index.d.ts +2 -0
  141. package/dist/types/tools/irc.d.ts +5 -3
  142. package/dist/types/tools/python.d.ts +1 -1
  143. package/dist/types/tools/report-issue.d.ts +41 -0
  144. package/dist/types/tools/subagent.d.ts +23 -0
  145. package/dist/types/tools/tmux-self-injection-guard.d.ts +2 -0
  146. package/dist/types/tools/ultragoal-ask-guard.d.ts +1 -1
  147. package/dist/types/tools/write.d.ts +2 -0
  148. package/dist/types/utils/file-mentions.d.ts +5 -0
  149. package/dist/types/utils/image-limits.d.ts +7 -0
  150. package/dist/types/utils/image-loading.d.ts +0 -1
  151. package/dist/types/utils/open.d.ts +0 -1
  152. package/dist/types/utils/pasted-image-loading.d.ts +1 -5
  153. package/dist/types/utils/prompt-suggestion.d.ts +2 -2
  154. package/dist/types/utils/shell-snapshot.d.ts +2 -0
  155. package/dist/types/utils/title-generator.d.ts +13 -3
  156. package/dist/types/web/scrapers/types.d.ts +8 -1
  157. package/dist/types/web/search/providers/codex.d.ts +6 -7
  158. package/dist/types/web/search/providers/insane.d.ts +7 -1
  159. package/dist/types/web/search/providers/searxng.d.ts +5 -0
  160. package/package.json +10 -9
  161. package/scripts/acp-conformance-agent.ts +12 -2
  162. package/scripts/build-binary.ts +2 -1
  163. package/scripts/capture-subagent-compact-panel-showcase.ts +105 -0
  164. package/scripts/generate-docs-index.ts +8 -1
  165. package/scripts/generate-sdk-operation-inventory.ts +38 -4
  166. package/src/async/job-manager.ts +356 -56
  167. package/src/async/result-formatting.ts +40 -0
  168. package/src/autoresearch/git.ts +9 -2
  169. package/src/autoresearch/prompts/prompt-setup.md +1 -1
  170. package/src/capability/fs.ts +58 -22
  171. package/src/capability/index.ts +2 -2
  172. package/src/capability/rule.ts +11 -2
  173. package/src/cli/accounts-cli.ts +176 -8
  174. package/src/cli/append-system-prompt.ts +12 -0
  175. package/src/cli/args.ts +11 -7
  176. package/src/cli/auth-broker-cli.ts +21 -11
  177. package/src/cli/customize-doctor-precedence.ts +31 -0
  178. package/src/cli/customize-doctor.ts +49 -39
  179. package/src/cli/doctor/install-repairs.ts +35 -5
  180. package/src/cli/fast-help.ts +1 -3
  181. package/src/cli/install-activation.ts +47 -4
  182. package/src/cli/migrate-cli.ts +19 -6
  183. package/src/cli/public-command-errors.ts +6 -0
  184. package/src/cli/root-flags.ts +2 -4
  185. package/src/cli/stats-cli.ts +9 -5
  186. package/src/cli/update-cli.ts +471 -13
  187. package/src/cli-ordinary.ts +7 -2
  188. package/src/commands/accounts.ts +10 -5
  189. package/src/commands/harness.ts +25 -15
  190. package/src/commands/migrate.ts +3 -3
  191. package/src/commands/sdk.ts +277 -50
  192. package/src/commands/ultragoal.ts +16 -8
  193. package/src/commit/agentic/agent.ts +6 -6
  194. package/src/commit/analysis/conventional.ts +14 -5
  195. package/src/commit/changelog/index.ts +39 -1
  196. package/src/commit/map-reduce/index.ts +7 -7
  197. package/src/commit/message.ts +10 -3
  198. package/src/commit/pipeline.ts +12 -12
  199. package/src/config/autorouting-tier-map.ts +50 -1
  200. package/src/config/config-file.ts +106 -3
  201. package/src/config/config-hot-reload-trust.ts +148 -0
  202. package/src/config/config-hot-reload.ts +742 -0
  203. package/src/config/file-lock.ts +1615 -328
  204. package/src/config/keybindings.ts +154 -7
  205. package/src/config/model-bindings-applier.ts +85 -47
  206. package/src/config/model-discovery-manager.ts +10 -0
  207. package/src/config/model-profile-activation.ts +889 -148
  208. package/src/config/model-registry.ts +1706 -205
  209. package/src/config/model-resolver.ts +52 -22
  210. package/src/config/models-config-schema.ts +6 -0
  211. package/src/config/settings-schema.ts +73 -4
  212. package/src/config/settings.ts +460 -24
  213. package/src/coordinator-mcp/policy.ts +26 -1
  214. package/src/coordinator-mcp/server.ts +37 -2
  215. package/src/crash/sanitize.ts +7 -1
  216. package/src/customization/import.ts +279 -8
  217. package/src/customization/inventory.ts +24 -8
  218. package/src/customization/types.ts +1 -0
  219. package/src/dap/client.ts +3 -2
  220. package/src/dap/config.ts +31 -11
  221. package/src/dap/defaults.json +1 -1
  222. package/src/dap/session.ts +161 -36
  223. package/src/debug/crash-diagnostics.ts +89 -6
  224. package/src/debug/report-bundle.ts +90 -2
  225. package/src/debug/system-info.ts +8 -3
  226. package/src/defaults/gjc/extensions/grok-cli-vendor/src/shared/base-url.ts +21 -7
  227. package/src/discovery/agents-md.ts +1 -1
  228. package/src/discovery/builtin.ts +4 -3
  229. package/src/discovery/helpers.ts +25 -16
  230. package/src/discovery/mcp-compat.ts +120 -17
  231. package/src/edit/apply-patch/index.ts +12 -1
  232. package/src/edit/index.ts +204 -22
  233. package/src/edit/modes/patch.ts +214 -8
  234. package/src/edit/modes/replace.ts +136 -0
  235. package/src/eval/js/context-manager.ts +29 -3
  236. package/src/eval/js/worker-entry.ts +51 -18
  237. package/src/eval/py/executor.ts +14 -4
  238. package/src/eval/py/runner.py +144 -5
  239. package/src/eval/py/runtime.ts +44 -4
  240. package/src/exec/bash-executor.ts +81 -4
  241. package/src/exec/bash-shell-guardian.ts +4 -1
  242. package/src/exec/bash-shell-supervisor.ts +8 -4
  243. package/src/exec/isolated-shell.ts +5 -1
  244. package/src/exec/non-interactive-env.ts +0 -1
  245. package/src/export/html/index.ts +86 -6
  246. package/src/export/ttsr.ts +176 -1
  247. package/src/extensibility/custom-tools/wrapper.ts +3 -1
  248. package/src/extensibility/extensions/loader.ts +10 -1
  249. package/src/extensibility/extensions/ouroboros-ooo-bridge.ts +63 -1
  250. package/src/extensibility/extensions/types.ts +5 -1
  251. package/src/extensibility/hooks/loader.ts +10 -2
  252. package/src/extensibility/plugins/marketplace/fetcher.ts +2 -2
  253. package/src/extensibility/plugins/marketplace/manager.ts +36 -10
  254. package/src/extensibility/slash-commands.ts +5 -0
  255. package/src/gjc-runtime/gc-runtime.ts +37 -7
  256. package/src/gjc-runtime/launch-tmux.ts +30 -15
  257. package/src/gjc-runtime/launch-worktree.test.ts +148 -0
  258. package/src/gjc-runtime/launch-worktree.ts +101 -14
  259. package/src/gjc-runtime/managed-owner-admission.ts +46 -2
  260. package/src/gjc-runtime/ralplan-current-final.ts +294 -0
  261. package/src/gjc-runtime/ralplan-runtime.ts +95 -1
  262. package/src/gjc-runtime/session-resolution.ts +38 -6
  263. package/src/gjc-runtime/session-state-lock.ts +59 -14
  264. package/src/gjc-runtime/state-runtime.ts +23 -19
  265. package/src/gjc-runtime/state-writer.ts +109 -5
  266. package/src/gjc-runtime/tmux-sessions.ts +93 -13
  267. package/src/gjc-runtime/ultragoal-change-set.ts +90 -11
  268. package/src/gjc-runtime/ultragoal-guard.ts +9 -1
  269. package/src/gjc-runtime/ultragoal-runtime.ts +15 -5
  270. package/src/harness-control-plane/finalize.ts +11 -0
  271. package/src/harness-control-plane/receipt-spool.ts +105 -28
  272. package/src/harness-control-plane/storage.ts +124 -6
  273. package/src/hindsight/content.ts +116 -16
  274. package/src/hindsight/mental-models.ts +22 -10
  275. package/src/hindsight/state.ts +7 -4
  276. package/src/hooks/native-skill-hook.ts +17 -12
  277. package/src/internal-urls/artifact-protocol.ts +3 -1
  278. package/src/internal-urls/docs-index.generated.ts +23 -22
  279. package/src/internal-urls/local-protocol.ts +167 -8
  280. package/src/internal-urls/types.ts +2 -0
  281. package/src/lsp/client.ts +2 -1
  282. package/src/lsp/clients/biome-client.ts +2 -0
  283. package/src/lsp/clients/swiftlint-client.ts +2 -0
  284. package/src/lsp/index.ts +29 -0
  285. package/src/lsp/workspace-path.ts +23 -0
  286. package/src/main.ts +330 -92
  287. package/src/memories/index.ts +25 -2
  288. package/src/migrate/action-planner.ts +574 -2
  289. package/src/migrate/adapters/claude-code.ts +162 -5
  290. package/src/migrate/adapters/codex.ts +77 -7
  291. package/src/migrate/adapters/index.ts +258 -5
  292. package/src/migrate/adapters/opencode.ts +10 -1
  293. package/src/migrate/executor.ts +222 -7
  294. package/src/migrate/hook-module.ts +193 -0
  295. package/src/migrate/report.ts +63 -2
  296. package/src/migrate/types.ts +84 -4
  297. package/src/modes/DESIGN.md +51 -0
  298. package/src/modes/acp/acp-agent.ts +10 -1
  299. package/src/modes/acp/permission-mode.ts +18 -1
  300. package/src/modes/components/agent-dashboard.ts +20 -4
  301. package/src/modes/components/assistant-message.ts +55 -1
  302. package/src/modes/components/customization/customization-dashboard.ts +7 -0
  303. package/src/modes/components/customization/import-wizard.ts +29 -3
  304. package/src/modes/components/model-selector.ts +157 -103
  305. package/src/modes/components/settings-selector.ts +103 -37
  306. package/src/modes/components/welcome.ts +3 -1
  307. package/src/modes/controllers/command-controller.ts +300 -9
  308. package/src/modes/controllers/event-controller.ts +1 -1
  309. package/src/modes/controllers/extension-ui-controller.ts +31 -1
  310. package/src/modes/controllers/goal-mode-controller.ts +79 -10
  311. package/src/modes/controllers/input-controller.ts +2 -0
  312. package/src/modes/controllers/plan-mode-controller.ts +66 -11
  313. package/src/modes/controllers/runtime-mcp-command-controller.ts +7 -6
  314. package/src/modes/controllers/selector-controller.ts +76 -7
  315. package/src/modes/interactive-mode.ts +123 -4
  316. package/src/modes/prompt-suggestion-controller.ts +1 -1
  317. package/src/modes/runtime-init.ts +5 -1
  318. package/src/modes/theme/theme.ts +15 -0
  319. package/src/modes/utils/host-status.ts +21 -10
  320. package/src/prompts/agent-fragments/browser-aside.md +2 -2
  321. package/src/prompts/system/irc-incoming.md +3 -1
  322. package/src/prompts/tools/debug.md +1 -1
  323. package/src/prompts/tools/irc.md +1 -1
  324. package/src/prompts/tools/read.md +1 -1
  325. package/src/prompts/tools/report-issue.md +13 -0
  326. package/src/prompts/tools/task.md +1 -1
  327. package/src/runtime-credential-selector.ts +2 -2
  328. package/src/runtime-mcp/content-limits.ts +7 -0
  329. package/src/runtime-mcp/manager-pool.test.ts +5 -4
  330. package/src/runtime-mcp/manager.ts +40 -3
  331. package/src/runtime-mcp/oauth-flow.ts +32 -6
  332. package/src/runtime-mcp/plugin-mcp-name-filter.ts +62 -0
  333. package/src/runtime-mcp/project-host-values.ts +12 -8
  334. package/src/runtime-mcp/render.ts +16 -1
  335. package/src/runtime-mcp/tool-bridge.ts +194 -21
  336. package/src/runtime-mcp/transports/stdio.ts +2 -1
  337. package/src/sdk/broker/broker-exit.ts +10 -5
  338. package/src/sdk/broker/broker.ts +484 -85
  339. package/src/sdk/broker/daemon-entry.ts +338 -116
  340. package/src/sdk/broker/doctor-restart.ts +2 -1
  341. package/src/sdk/broker/ensure.ts +1214 -268
  342. package/src/sdk/broker/hop.ts +286 -0
  343. package/src/sdk/broker/lifecycle-ledger.ts +38 -11
  344. package/src/sdk/broker/lifecycle.ts +102 -6
  345. package/src/sdk/broker/lock-host-identity.ts +29 -0
  346. package/src/sdk/broker/session-index.ts +91 -16
  347. package/src/sdk/broker/transport.ts +17 -1
  348. package/src/sdk/bus/chat-command-policy.ts +16 -3
  349. package/src/sdk/bus/chat-daemon-control.ts +2 -2
  350. package/src/sdk/bus/index.ts +460 -35
  351. package/src/sdk/bus/reconciliation-store.ts +201 -8
  352. package/src/sdk/bus/session-control.ts +2 -0
  353. package/src/sdk/bus/slack-daemon.ts +105 -9
  354. package/src/sdk/bus/telegram-daemon-contract.ts +9 -1
  355. package/src/sdk/bus/telegram-daemon.ts +10 -2
  356. package/src/sdk/bus/threaded-render.ts +20 -1
  357. package/src/sdk/cli/session-cli.ts +23 -6
  358. package/src/sdk/host/control/dispatch.ts +13 -1
  359. package/src/sdk/host/control/operations.ts +10 -1
  360. package/src/sdk/host/events.ts +67 -4
  361. package/src/sdk/host/host.ts +8 -2
  362. package/src/sdk/host/prompt-image-upload.ts +438 -0
  363. package/src/sdk/host/sdk-surface-parity.test.ts +15 -0
  364. package/src/sdk/host/session-runtime.test.ts +1737 -129
  365. package/src/sdk/host/session-runtime.ts +684 -83
  366. package/src/sdk/host/surface-policy.ts +4 -0
  367. package/src/sdk/host/websocket-transport.lifecycle.test.ts +90 -2
  368. package/src/sdk/host/websocket-transport.ts +19 -7
  369. package/src/sdk/lifecycle/service.ts +15 -1
  370. package/src/sdk/prompt-deadline-flush.ts +78 -5
  371. package/src/sdk/prompt-deadline-manager.ts +68 -14
  372. package/src/sdk/prompt-status.ts +6 -4
  373. package/src/sdk/protocol/adapter-validation.ts +81 -12
  374. package/src/sdk/protocol/operation-inventory.generated.json +261 -2
  375. package/src/sdk/protocol/operation-registry.ts +33 -3
  376. package/src/sdk/reconciliation-extensions.ts +1 -0
  377. package/src/sdk/session-list.ts +1 -0
  378. package/src/sdk/session.ts +237 -97
  379. package/src/session/agent-session.ts +2726 -479
  380. package/src/session/history-storage.ts +182 -12
  381. package/src/session/internal/managed-session-scope.ts +173 -57
  382. package/src/session/internal/managed-session-storage.ts +85 -12
  383. package/src/session/internal/native-publish-outcome.ts +5 -1
  384. package/src/session/messages.ts +47 -6
  385. package/src/session/session-manager.ts +326 -19
  386. package/src/session/startup-auth-config.ts +116 -8
  387. package/src/session/streaming-output.ts +262 -4
  388. package/src/session/terminal-abort.ts +14 -2
  389. package/src/session-import/redact.ts +53 -2
  390. package/src/session-import/service.ts +1 -0
  391. package/src/skill-state/workflow-mutation-guard.ts +2145 -206
  392. package/src/slash-commands/builtin-registry.ts +21 -6
  393. package/src/slash-commands/helpers/credential-switch.ts +10 -5
  394. package/src/slash-commands/helpers/mcp.ts +11 -3
  395. package/src/slash-commands/helpers/provider-auth-status.ts +456 -0
  396. package/src/ssh/ssh-executor.ts +9 -2
  397. package/src/stt/recorder.ts +26 -9
  398. package/src/system-prompt.ts +61 -16
  399. package/src/task/executor.ts +115 -20
  400. package/src/task/index.ts +389 -175
  401. package/src/task/role-body.ts +84 -0
  402. package/src/task/token-log-mode.test.ts +162 -0
  403. package/src/task/token-log.ts +91 -2
  404. package/src/tools/archive-reader.ts +650 -37
  405. package/src/tools/ask.ts +1 -0
  406. package/src/tools/ast-edit-write-path.ts +64 -0
  407. package/src/tools/ast-edit.ts +18 -1
  408. package/src/tools/ast-grep.ts +7 -2
  409. package/src/tools/atomic-file-write.ts +46 -15
  410. package/src/tools/auto-generated-guard.ts +15 -3
  411. package/src/tools/awaiting-input.ts +114 -0
  412. package/src/tools/bash-allowed-prefixes.ts +214 -6
  413. package/src/tools/bash-interactive.ts +14 -2
  414. package/src/tools/bash-skill-urls.ts +58 -5
  415. package/src/tools/bash.ts +113 -26
  416. package/src/tools/browser/tab-worker.ts +39 -13
  417. package/src/tools/browser.ts +17 -7
  418. package/src/tools/cron.ts +10 -1
  419. package/src/tools/debug.ts +16 -8
  420. package/src/tools/descriptors.test.ts +44 -0
  421. package/src/tools/descriptors.ts +50 -6
  422. package/src/tools/eval.ts +15 -0
  423. package/src/tools/fetch.ts +93 -4
  424. package/src/tools/gh.ts +27 -12
  425. package/src/tools/image-gen.ts +20 -6
  426. package/src/tools/index.ts +2 -0
  427. package/src/tools/irc.ts +5 -3
  428. package/src/tools/monitor.ts +9 -1
  429. package/src/tools/output-meta.ts +74 -5
  430. package/src/tools/python.ts +1 -1
  431. package/src/tools/read.ts +26 -37
  432. package/src/tools/recipe/runner.ts +23 -12
  433. package/src/tools/render-utils.ts +2 -2
  434. package/src/tools/report-issue.ts +184 -0
  435. package/src/tools/search.ts +3 -2
  436. package/src/tools/ssh.ts +274 -8
  437. package/src/tools/subagent-render.ts +114 -12
  438. package/src/tools/subagent.ts +118 -5
  439. package/src/tools/tmux-self-injection-guard.ts +3301 -122
  440. package/src/tools/todo-write.ts +41 -3
  441. package/src/tools/tool-catalog.generated.ts +32 -4
  442. package/src/tools/ultragoal-ask-guard.ts +13 -1
  443. package/src/tools/write.ts +42 -3
  444. package/src/tui/tree-list.ts +15 -1
  445. package/src/utils/commit-message-generator.ts +12 -4
  446. package/src/utils/external-editor.ts +21 -7
  447. package/src/utils/file-mentions.ts +75 -7
  448. package/src/utils/git.ts +38 -7
  449. package/src/utils/image-limits.ts +8 -0
  450. package/src/utils/image-loading.ts +1 -1
  451. package/src/utils/open.ts +73 -4
  452. package/src/utils/pasted-image-loading.ts +9 -8
  453. package/src/utils/prompt-suggestion.ts +5 -4
  454. package/src/utils/shell-snapshot.ts +9 -1
  455. package/src/utils/title-generator.ts +72 -22
  456. package/src/web/scrapers/index.ts +3 -1
  457. package/src/web/scrapers/types.ts +8 -0
  458. package/src/web/scrapers/youtube.ts +3 -2
  459. package/src/web/search/index.ts +2 -1
  460. package/src/web/search/providers/codex.ts +29 -32
  461. package/src/web/search/providers/insane.ts +91 -28
  462. package/src/web/search/providers/searxng.ts +62 -10
@@ -1,6 +1,8 @@
1
+ import * as fs from "node:fs/promises";
1
2
  import * as path from "node:path";
2
3
  import * as timers from "node:timers/promises";
3
4
  import { logger, untilAborted } from "@gajae-code/utils";
5
+ import { projectSafeSpawnEnv } from "@gajae-code/utils/shell-config";
4
6
  import { NON_INTERACTIVE_ENV } from "../exec/non-interactive-env";
5
7
  import { type OwnedProcess, spawnOwnedProcess } from "../runtime/process-lifecycle";
6
8
  import { DapClient } from "./client";
@@ -106,9 +108,19 @@ interface DapSession {
106
108
  initializedSeen: boolean;
107
109
  needsConfigurationDone: boolean;
108
110
  configurationDoneSent: boolean;
111
+ /** Containment root. Distinct from `cwd`, which is the caller execution directory. */
112
+ workspace: string;
113
+ closing: boolean;
114
+ disposed: boolean;
115
+ inflightTerminal: number;
116
+ terminalIdle?: Promise<void>;
117
+ resolveTerminalIdle?: () => void;
109
118
  runInTerminalProcesses: Set<OwnedProcess>;
110
119
  }
111
120
 
121
+ /** Test seam: pauses after cwd containment and before the debuggee spawn. */
122
+ export const runInTerminalTestHooks: { beforeSpawn?: () => Promise<void> } = {};
123
+
112
124
  export interface DapOutputSnapshot {
113
125
  snapshot: DapSessionSummary;
114
126
  output: string;
@@ -210,6 +222,53 @@ function buildSummary(session: DapSession): DapSessionSummary {
210
222
  };
211
223
  }
212
224
 
225
+ function isEnoent(error: unknown): boolean {
226
+ return (error as NodeJS.ErrnoException | undefined)?.code === "ENOENT";
227
+ }
228
+
229
+ /** Walk missing leaves so a not-yet-created debug cwd can still be contained. */
230
+ async function canonicalizeRunInTerminalCwd(absolute: string): Promise<string> {
231
+ try {
232
+ return await fs.realpath(absolute);
233
+ } catch (error) {
234
+ if (!isEnoent(error)) throw error;
235
+ }
236
+ const missing: string[] = [];
237
+ let cursor = absolute;
238
+ for (;;) {
239
+ const parent = path.dirname(cursor);
240
+ if (parent === cursor) throw new Error("runInTerminal cwd escapes the workspace");
241
+ missing.unshift(path.basename(cursor));
242
+ cursor = parent;
243
+ try {
244
+ return path.join(await fs.realpath(cursor), ...missing);
245
+ } catch (error) {
246
+ if (!isEnoent(error)) throw error;
247
+ }
248
+ }
249
+ }
250
+
251
+ export function cwdEscapesWorkspace(root: string, candidate: string): boolean {
252
+ const relative = path.relative(root, candidate);
253
+ return relative === ".." || relative.startsWith(`..${path.sep}`) || path.isAbsolute(relative);
254
+ }
255
+
256
+ /**
257
+ * The validated cwd is the cwd that is spawned. Relative paths resolve against
258
+ * the session workspace, then both sides are canonicalized before containment.
259
+ */
260
+ async function resolveRunInTerminalCwd(workspace: string, requested: string | undefined): Promise<string> {
261
+ if (typeof requested === "string" && requested.trim() === "") {
262
+ throw new Error("runInTerminal cwd escapes the workspace");
263
+ }
264
+ const root = await fs.realpath(workspace);
265
+ const candidate = await canonicalizeRunInTerminalCwd(path.resolve(workspace, requested ?? workspace));
266
+ if (cwdEscapesWorkspace(root, candidate)) {
267
+ throw new Error("runInTerminal cwd escapes the workspace");
268
+ }
269
+ return candidate;
270
+ }
271
+
213
272
  export class DapSessionManager {
214
273
  #sessions = new Map<string, DapSession>();
215
274
  #activeSessionId: string | null = null;
@@ -234,13 +293,19 @@ export class DapSessionManager {
234
293
  }
235
294
 
236
295
  async launch(
237
- options: DapLaunchSessionOptions,
296
+ options: DapLaunchSessionOptions & { workspace?: string },
238
297
  signal?: AbortSignal,
239
298
  timeoutMs: number = 30_000,
240
299
  ): Promise<DapSessionSummary> {
241
300
  await this.#ensureLaunchSlot();
242
301
  const client = await DapClient.spawn({ adapter: options.adapter, cwd: options.cwd });
243
- const session = this.#registerSession(client, options.adapter, options.cwd, options.program);
302
+ const session = this.#registerSession(
303
+ client,
304
+ options.adapter,
305
+ options.cwd,
306
+ options.workspace ?? options.cwd,
307
+ options.program,
308
+ );
244
309
  try {
245
310
  session.capabilities = await client.initialize(
246
311
  this.#buildInitializeArguments(options.adapter),
@@ -299,13 +364,13 @@ export class DapSessionManager {
299
364
  }
300
365
 
301
366
  async attach(
302
- options: DapAttachSessionOptions,
367
+ options: DapAttachSessionOptions & { workspace?: string },
303
368
  signal?: AbortSignal,
304
369
  timeoutMs: number = 30_000,
305
370
  ): Promise<DapSessionSummary> {
306
371
  await this.#ensureLaunchSlot();
307
372
  const client = await DapClient.spawn({ adapter: options.adapter, cwd: options.cwd });
308
- const session = this.#registerSession(client, options.adapter, options.cwd);
373
+ const session = this.#registerSession(client, options.adapter, options.cwd, options.workspace ?? options.cwd);
309
374
  try {
310
375
  session.capabilities = await client.initialize(
311
376
  this.#buildInitializeArguments(options.adapter),
@@ -886,24 +951,29 @@ export class DapSessionManager {
886
951
  const session = this.#getActiveSessionOrNull();
887
952
  if (!session) return null;
888
953
  session.lastUsedAt = Date.now();
889
- if (session.status !== "terminated") {
890
- if (session.capabilities?.supportsTerminateRequest) {
954
+ // Close before the first await. A runInTerminal paused on cwd resolution
955
+ // must not spawn while terminate or disconnect is still outstanding.
956
+ session.closing = true;
957
+ try {
958
+ if (session.status !== "terminated") {
959
+ if (session.capabilities?.supportsTerminateRequest) {
960
+ await untilAborted(
961
+ signal,
962
+ session.client.sendRequest("terminate", undefined, signal, timeoutMs).catch(() => undefined),
963
+ );
964
+ }
891
965
  await untilAborted(
892
966
  signal,
893
- session.client.sendRequest("terminate", undefined, signal, timeoutMs).catch(() => undefined),
967
+ session.client
968
+ .sendRequest("disconnect", { terminateDebuggee: true }, signal, timeoutMs)
969
+ .catch(() => undefined),
894
970
  );
895
971
  }
896
- await untilAborted(
897
- signal,
898
- session.client
899
- .sendRequest("disconnect", { terminateDebuggee: true }, signal, timeoutMs)
900
- .catch(() => undefined),
901
- );
972
+ session.status = "terminated";
973
+ return buildSummary(session);
974
+ } finally {
975
+ await this.#disposeSession(session);
902
976
  }
903
- session.status = "terminated";
904
- const summary = buildSummary(session);
905
- await this.#disposeSession(session);
906
- return summary;
907
977
  }
908
978
 
909
979
  #startCleanupTimer(): void {
@@ -945,12 +1015,22 @@ export class DapSessionManager {
945
1015
  throw new Error(`Debug session ${active.id} is still active. Terminate it before launching another.`);
946
1016
  }
947
1017
 
948
- #registerSession(client: DapClient, adapter: DapResolvedAdapter, cwd: string, program?: string): DapSession {
1018
+ #registerSession(
1019
+ client: DapClient,
1020
+ adapter: DapResolvedAdapter,
1021
+ cwd: string,
1022
+ workspace: string,
1023
+ program?: string,
1024
+ ): DapSession {
949
1025
  const session: DapSession = {
950
1026
  id: `debug-${++this.#nextId}`,
951
1027
  adapter,
952
1028
  cwd,
1029
+ workspace,
953
1030
  program,
1031
+ closing: false,
1032
+ disposed: false,
1033
+ inflightTerminal: 0,
954
1034
  client,
955
1035
  status: "launching",
956
1036
  launchedAt: Date.now(),
@@ -975,24 +1055,40 @@ export class DapSessionManager {
975
1055
  if (!Array.isArray(args.args) || args.args.length === 0) {
976
1056
  throw new Error("runInTerminal request did not include a command");
977
1057
  }
978
- const env = Object.fromEntries(
979
- Object.entries(args.env ?? {}).filter((entry): entry is [string, string] => entry[1] !== null),
980
- );
981
- const owner = spawnOwnedProcess(args.args, {
982
- cwd: args.cwd ?? session.cwd,
983
- stdin: "ignore",
984
- env: {
985
- ...Bun.env,
986
- ...NON_INTERACTIVE_ENV,
987
- ...env,
988
- },
989
- name: `dap:${session.id}:runInTerminal`,
990
- });
991
- drainStream(owner.child.stdout);
992
- drainStream(owner.child.stderr);
993
- session.runInTerminalProcesses.add(owner);
994
- owner.exited.finally(() => session.runInTerminalProcesses.delete(owner));
995
- return { processId: owner.pid } satisfies DapRunInTerminalResponse;
1058
+ if (session.closing) throw new Error("runInTerminal session is closed");
1059
+ this.#beginTerminal(session);
1060
+ try {
1061
+ if (session.closing) throw new Error("runInTerminal session is closed");
1062
+ const env = Object.fromEntries(
1063
+ Object.entries(args.env ?? {}).filter((entry): entry is [string, string] => entry[1] !== null),
1064
+ );
1065
+ const cwd = await resolveRunInTerminalCwd(session.workspace, args.cwd ?? session.cwd);
1066
+ await runInTerminalTestHooks.beforeSpawn?.();
1067
+ if (session.closing || !this.#sessions.has(session.id)) {
1068
+ throw new Error("runInTerminal session is closed");
1069
+ }
1070
+ const owner = spawnOwnedProcess(args.args, {
1071
+ cwd,
1072
+ stdin: "ignore",
1073
+ env: {
1074
+ ...projectSafeSpawnEnv(),
1075
+ ...NON_INTERACTIVE_ENV,
1076
+ ...env,
1077
+ },
1078
+ name: `dap:${session.id}:runInTerminal`,
1079
+ });
1080
+ if (session.closing || !this.#sessions.has(session.id)) {
1081
+ await owner.dispose();
1082
+ throw new Error("runInTerminal session is closed");
1083
+ }
1084
+ drainStream(owner.child.stdout);
1085
+ drainStream(owner.child.stderr);
1086
+ session.runInTerminalProcesses.add(owner);
1087
+ owner.exited.finally(() => session.runInTerminalProcesses.delete(owner));
1088
+ return { processId: owner.pid } satisfies DapRunInTerminalResponse;
1089
+ } finally {
1090
+ this.#endTerminal(session);
1091
+ }
996
1092
  });
997
1093
  client.onReverseRequest("startDebugging", async rawArgs => {
998
1094
  const startArgs = (rawArgs ?? {}) as Partial<DapStartDebuggingArguments>;
@@ -1319,7 +1415,36 @@ export class DapSessionManager {
1319
1415
  return session;
1320
1416
  }
1321
1417
 
1418
+ #beginTerminal(session: DapSession): void {
1419
+ session.inflightTerminal++;
1420
+ if (session.inflightTerminal !== 1) return;
1421
+ session.terminalIdle = new Promise(resolve => {
1422
+ session.resolveTerminalIdle = resolve;
1423
+ });
1424
+ }
1425
+
1426
+ #endTerminal(session: DapSession): void {
1427
+ session.inflightTerminal = Math.max(0, session.inflightTerminal - 1);
1428
+ if (session.inflightTerminal !== 0) return;
1429
+ session.resolveTerminalIdle?.();
1430
+ session.resolveTerminalIdle = undefined;
1431
+ session.terminalIdle = undefined;
1432
+ }
1433
+
1434
+ async #waitTerminalIdle(session: DapSession): Promise<void> {
1435
+ while (session.inflightTerminal > 0 && session.terminalIdle) {
1436
+ await session.terminalIdle;
1437
+ }
1438
+ }
1439
+
1322
1440
  async #disposeSession(session: DapSession) {
1441
+ session.closing = true;
1442
+ if (session.disposed) {
1443
+ await this.#waitTerminalIdle(session);
1444
+ return;
1445
+ }
1446
+ session.disposed = true;
1447
+ await this.#waitTerminalIdle(session);
1323
1448
  if (this.#activeSessionId === session.id) {
1324
1449
  this.#activeSessionId = null;
1325
1450
  }
@@ -1,6 +1,8 @@
1
+ import { constants as fsConstants } from "node:fs";
1
2
  import * as fs from "node:fs/promises";
2
3
  import * as os from "node:os";
3
4
  import * as path from "node:path";
5
+ import { redactCrashSecrets } from "@gajae-code/utils";
4
6
 
5
7
  const CRASH_DIAGNOSTICS_ENV = "GJC_CRASH_DIAGNOSTICS";
6
8
  const CRASH_DIAGNOSTICS_DIR_ENV = "GJC_CRASH_DIAGNOSTICS_DIR";
@@ -169,7 +171,7 @@ export async function writeCrashReport(
169
171
  pid: process.pid,
170
172
  cwd: options.cwd ?? process.cwd(),
171
173
  ...classification,
172
- stderrPreview: input.stderr ? trimStartBytes(input.stderr, STDERR_PREVIEW_BYTES) : undefined,
174
+ stderrPreview: input.stderr ? trimStartBytes(redactCrashSecrets(input.stderr), STDERR_PREVIEW_BYTES) : undefined,
173
175
  spawnError: input.spawnError === undefined ? undefined : stringifyError(input.spawnError),
174
176
  };
175
177
  const enabled = crashDiagnosticsEnabled(options.env);
@@ -179,10 +181,10 @@ export async function writeCrashReport(
179
181
  }
180
182
 
181
183
  try {
182
- const dir = getCrashDiagnosticsDirectory(options.env);
183
- await ensurePrivateDiagnosticsDirectory(dir);
184
+ const directory = diagnosticsDirectoryPath(getCrashDiagnosticsDirectory(options.env));
185
+ await ensurePrivateDiagnosticsDirectory(directory);
184
186
  const filename = `${report.createdAt.replace(/[:.]/g, "-")}-${report.kind}-${report.class}-${process.pid}.json`;
185
- const reportPath = path.join(dir, filename);
187
+ const reportPath = path.join(directory, filename);
186
188
  await writePrivateCrashReport(reportPath, `${JSON.stringify(report, null, 2)}\n`);
187
189
  return { report, path: reportPath, enabled };
188
190
  } catch {
@@ -196,19 +198,100 @@ export function formatCrashDiagnosticNotice(result: CrashReportWriteResult): str
196
198
  return `[crash:${result.report.kind}:${result.report.class}] ${result.report.reason}${location}`;
197
199
  }
198
200
 
201
+ const POSIX_NOFOLLOW = process.platform === "win32" ? 0 : (fsConstants.O_NOFOLLOW ?? 0);
202
+
203
+ function isFilesystemRoot(dir: string): boolean {
204
+ const root = path.parse(dir).root;
205
+ return root.length > 0 && dir === root;
206
+ }
207
+
208
+ /** Drop trailing platform separators and "." segments. A backslash stays in the name on POSIX. ".." is unchanged. */
209
+ function diagnosticsDirectoryPath(dir: string): string {
210
+ const sep = path.sep;
211
+ let current = dir;
212
+ while (!isFilesystemRoot(current)) {
213
+ let end = current.length;
214
+ while (end > 0 && current[end - 1] === sep) end -= 1;
215
+ if (end !== current.length) {
216
+ const stripped = current.slice(0, end);
217
+ if (stripped === "" || isFilesystemRoot(stripped)) return path.parse(current).root || current;
218
+ if (process.platform === "win32" && /^[A-Za-z]:$/.test(stripped)) return `${stripped}\\`;
219
+ current = stripped;
220
+ continue;
221
+ }
222
+ const dotSuffix = `${sep}.`;
223
+ if (current.endsWith(dotSuffix) && !current.endsWith(`${sep}..`)) {
224
+ const parent = current.slice(0, -dotSuffix.length);
225
+ if (parent === "" || isFilesystemRoot(parent)) {
226
+ return parent === "" ? path.parse(current).root || current : parent;
227
+ }
228
+ if (process.platform === "win32" && /^[A-Za-z]:$/.test(parent)) return `${parent}\\`;
229
+ current = parent;
230
+ continue;
231
+ }
232
+ return current;
233
+ }
234
+ return current;
235
+ }
236
+
199
237
  async function ensurePrivateDiagnosticsDirectory(dir: string): Promise<void> {
238
+ await assertRealDiagnosticsDirectory(dir);
200
239
  await fs.mkdir(dir, { recursive: true, mode: DIRECTORY_MODE });
240
+ await assertRealDiagnosticsDirectory(dir);
241
+ await chmodRealDiagnosticsDirectory(dir);
242
+ }
243
+
244
+ async function assertRealDiagnosticsDirectory(dir: string): Promise<void> {
245
+ let stat: Awaited<ReturnType<typeof fs.lstat>>;
246
+ try {
247
+ stat = await fs.lstat(dir);
248
+ } catch (error) {
249
+ if (typeof error === "object" && error !== null && "code" in error && error.code === "ENOENT") return;
250
+ throw error;
251
+ }
252
+ if (stat.isSymbolicLink() || !stat.isDirectory()) {
253
+ throw new Error("crash diagnostics path is not a private directory");
254
+ }
255
+ }
256
+
257
+ async function chmodRealDiagnosticsDirectory(dir: string): Promise<void> {
258
+ const stat = await fs.lstat(dir);
259
+ if (stat.isSymbolicLink() || !stat.isDirectory()) {
260
+ throw new Error("crash diagnostics path is not a private directory");
261
+ }
262
+ // macOS lchmod sets the mode without read permission and without following a symlink.
263
+ // Linux has no equivalent here, so an unreadable directory is left unchanged. A path chmod would follow a swapped symlink.
264
+ if (process.platform === "darwin" && typeof fs.lchmod === "function") {
265
+ await fs.lchmod(dir, DIRECTORY_MODE);
266
+ return;
267
+ }
268
+ const directoryFlag = fsConstants.O_DIRECTORY ?? 0;
269
+ const pathFlag = (fsConstants as { O_PATH?: number }).O_PATH ?? 0;
270
+ if (POSIX_NOFOLLOW !== 0 && directoryFlag !== 0) {
271
+ const openFlag = pathFlag !== 0 ? pathFlag : fsConstants.O_RDONLY;
272
+ const handle = await fs.open(dir, openFlag | directoryFlag | POSIX_NOFOLLOW);
273
+ try {
274
+ await handle.chmod(DIRECTORY_MODE);
275
+ } finally {
276
+ await handle.close();
277
+ }
278
+ return;
279
+ }
201
280
  await fs.chmod(dir, DIRECTORY_MODE);
202
281
  }
203
282
 
204
283
  async function writePrivateCrashReport(reportPath: string, contents: string): Promise<void> {
205
- const file = await fs.open(reportPath, "wx", REPORT_FILE_MODE);
284
+ const file = await fs.open(
285
+ reportPath,
286
+ fsConstants.O_WRONLY | fsConstants.O_CREAT | fsConstants.O_EXCL | POSIX_NOFOLLOW,
287
+ REPORT_FILE_MODE,
288
+ );
206
289
  try {
207
290
  await file.writeFile(contents);
291
+ await file.chmod(REPORT_FILE_MODE);
208
292
  } finally {
209
293
  await file.close();
210
294
  }
211
- await fs.chmod(reportPath, REPORT_FILE_MODE);
212
295
  }
213
296
 
214
297
  function stringifyError(error: unknown): string {
@@ -3,6 +3,7 @@
3
3
  *
4
4
  * Creates a .tar.gz archive with session data, logs, system info, and optional profiling data.
5
5
  */
6
+ import { constants as fsConstants } from "node:fs";
6
7
  import * as fs from "node:fs/promises";
7
8
  import * as path from "node:path";
8
9
  import type { WorkProfile } from "@gajae-code/natives";
@@ -13,7 +14,9 @@ import {
13
14
  getLogPath,
14
15
  getLogsDir,
15
16
  getReportsDir,
17
+ hasFsCode,
16
18
  isEnoent,
19
+ redactCrashSecrets,
17
20
  resolveEquivalentPath,
18
21
  } from "@gajae-code/utils";
19
22
  import type { CpuProfile, HeapSnapshot } from "./profiler";
@@ -132,6 +135,72 @@ export interface DebugLogSource {
132
135
  loadOlderLogs(limitDays?: number): Promise<string>;
133
136
  }
134
137
 
138
+ /** Owner-only mode. umask cannot widen this, and fchmod repeats it after open. */
139
+ const REPORT_BUNDLE_MODE = 0o600;
140
+
141
+ /**
142
+ * Open the archive path without following a final symlink.
143
+ * A missing path is created with O_EXCL. An existing regular file is replaced.
144
+ * A symlink fails the open (ELOOP, or EEXIST on macOS with O_EXCL) and is left in place.
145
+ */
146
+ async function openReportBundleForWrite(outputPath: string): Promise<fs.FileHandle> {
147
+ const noFollow = process.platform === "win32" ? 0 : (fsConstants.O_NOFOLLOW ?? 0);
148
+ const createFlags = fsConstants.O_WRONLY | fsConstants.O_CREAT | fsConstants.O_EXCL | noFollow;
149
+ const replaceFlags = fsConstants.O_WRONLY | noFollow;
150
+
151
+ const openRegular = async (): Promise<fs.FileHandle> => {
152
+ const current = await fs.lstat(outputPath);
153
+ if (current.isSymbolicLink() || !current.isFile()) {
154
+ throw new Error("report bundle output is not a regular file");
155
+ }
156
+ try {
157
+ return await fs.open(outputPath, replaceFlags, REPORT_BUNDLE_MODE);
158
+ } catch (error) {
159
+ if (hasFsCode(error, "ELOOP")) throw new Error("report bundle output is not a regular file");
160
+ throw error;
161
+ }
162
+ };
163
+
164
+ let missing = false;
165
+ try {
166
+ await fs.lstat(outputPath);
167
+ } catch (error) {
168
+ if (!isEnoent(error)) throw error;
169
+ missing = true;
170
+ }
171
+ if (!missing) return openRegular();
172
+
173
+ try {
174
+ return await fs.open(outputPath, createFlags, REPORT_BUNDLE_MODE);
175
+ } catch (error) {
176
+ if (hasFsCode(error, "ELOOP")) throw new Error("report bundle output is not a regular file");
177
+ if (!hasFsCode(error, "EEXIST")) throw error;
178
+ }
179
+ return openRegular();
180
+ }
181
+
182
+ async function writeReportBundleFile(outputPath: string, bytes: Uint8Array): Promise<void> {
183
+ const handle = await openReportBundleForWrite(outputPath);
184
+ try {
185
+ const opened = await handle.stat();
186
+ const current = await fs.lstat(outputPath);
187
+ if (
188
+ current.isSymbolicLink() ||
189
+ !opened.isFile() ||
190
+ !current.isFile() ||
191
+ opened.dev !== current.dev ||
192
+ opened.ino !== current.ino
193
+ ) {
194
+ throw new Error("report bundle output is not a regular file");
195
+ }
196
+ if (process.platform !== "win32") await handle.chmod(REPORT_BUNDLE_MODE);
197
+ await handle.truncate(0);
198
+ await handle.writeFile(bytes);
199
+ } finally {
200
+ await handle.close();
201
+ }
202
+ }
203
+
135
204
  /**
136
205
  * Create a debug report bundle.
137
206
  *
@@ -228,12 +297,31 @@ export async function createReportBundle(options: ReportBundleOptions): Promise<
228
297
  }
229
298
  }
230
299
 
231
- // Write archive
232
- await Bun.Archive.write(outputPath, data, { compress: "gzip" });
300
+ // Scrub the transcript copies only. Settings, system info, and profiles are
301
+ // separate payloads and stay as collected. Sources on disk are not rewritten.
302
+ for (const archivePath of Object.keys(data)) {
303
+ const content = data[archivePath];
304
+ if (content === undefined || !reportEntryCarriesUserText(archivePath)) continue;
305
+ data[archivePath] = redactCrashSecrets(content);
306
+ }
307
+
308
+ // Write archive. Bytes are built in memory so the path open can refuse a symlink.
309
+ const archiveBytes = await new Bun.Archive(data, { compress: "gzip" }).bytes();
310
+ await writeReportBundleFile(outputPath, archiveBytes);
233
311
 
234
312
  return { path: outputPath, files };
235
313
  }
236
314
 
315
+ /** Session, log, artifact, and subagent copies are the transcript the user may attach. */
316
+ function reportEntryCarriesUserText(archivePath: string): boolean {
317
+ return (
318
+ archivePath === "logs.txt" ||
319
+ archivePath === "session.jsonl" ||
320
+ archivePath.startsWith("artifacts/") ||
321
+ archivePath.startsWith("subagents/")
322
+ );
323
+ }
324
+
237
325
  /** Add all files from a directory to the archive */
238
326
  async function addDirectoryToArchive(
239
327
  data: Record<string, string>,
@@ -3,7 +3,7 @@
3
3
  */
4
4
 
5
5
  import * as os from "node:os";
6
- import { APP_NAME, formatBytes, getProjectDir, VERSION } from "@gajae-code/utils";
6
+ import { APP_NAME, formatBytes, getProjectDir, redactCrashSecrets, VERSION } from "@gajae-code/utils";
7
7
 
8
8
  export interface SystemInfo {
9
9
  os: string;
@@ -97,7 +97,12 @@ export function formatSystemInfo(info: SystemInfo): string {
97
97
  return lines.join("\n");
98
98
  }
99
99
 
100
- /** Sanitize environment variables by redacting sensitive values */
100
+ /**
101
+ * Sanitize environment variables for a debug report.
102
+ * Sensitive names stay fully replaced. Other values keep their text, with the
103
+ * same credential shapes removed that crash records already scrub before they
104
+ * are written to disk.
105
+ */
101
106
  export function sanitizeEnv(env: Record<string, string | undefined>): Record<string, string> {
102
107
  const SENSITIVE_PATTERNS = [
103
108
  /key/i,
@@ -117,7 +122,7 @@ export function sanitizeEnv(env: Record<string, string | undefined>): Record<str
117
122
  for (const [k, v] of Object.entries(env)) {
118
123
  if (v === undefined) continue;
119
124
  const isSensitive = SENSITIVE_PATTERNS.some(p => p.test(k));
120
- result[k] = isSensitive ? "[REDACTED]" : v;
125
+ result[k] = isSensitive ? "[REDACTED]" : redactCrashSecrets(v);
121
126
  }
122
127
  return result;
123
128
  }
@@ -1,5 +1,8 @@
1
+ import { $credentialEnv, $pickCredentialEnv } from '@gajae-code/utils';
2
+
1
3
  const DEFAULT_GROK_BUILD_BASE_URL = 'https://cli-chat-proxy.grok.com/v1';
2
4
  const ALLOWED_GROK_BUILD_HOSTS = new Set(['cli-chat-proxy.grok.com']);
5
+ const UNSAFE_BASE_URL_ENV = 'GJC_GROK_CLI_ALLOW_UNSAFE_BASE_URL';
3
6
 
4
7
  function normalizeBaseUrl(baseUrl: string): string {
5
8
  return baseUrl.trim().replace(/\/+$/, '');
@@ -14,23 +17,34 @@ function isAllowedCredentialHost(baseUrl: string): boolean {
14
17
  }
15
18
  }
16
19
 
20
+ /**
21
+ * Operator environment only. `$credentialEnv` already drops a cwd dotenv value,
22
+ * including a dynamic declaration, using the snapshot taken when the env module
23
+ * loaded. A repository `.env` therefore cannot name the Grok Build base URL or
24
+ * approve that URL with the unsafe opt-in.
25
+ */
26
+ function trustedConfiguredBaseUrl(): string | undefined {
27
+ return $pickCredentialEnv('GJC_GROK_CLI_BASE_URL', 'GROK_CLI_BASE_URL');
28
+ }
29
+
30
+ function unsafeOverrideAllowed(): boolean {
31
+ return $credentialEnv(UNSAFE_BASE_URL_ENV) === '1';
32
+ }
33
+
17
34
  export function getBaseUrl(): string {
18
- const configured = process.env.GJC_GROK_CLI_BASE_URL || process.env.GROK_CLI_BASE_URL;
35
+ const configured = trustedConfiguredBaseUrl();
19
36
  if (!configured) return DEFAULT_GROK_BUILD_BASE_URL;
20
37
 
21
38
  const normalized = normalizeBaseUrl(configured);
22
39
  if (isAllowedCredentialHost(normalized)) return normalized;
23
-
24
- if (process.env.GJC_GROK_CLI_ALLOW_UNSAFE_BASE_URL === '1') return normalized;
40
+ if (unsafeOverrideAllowed()) return normalized;
25
41
 
26
42
  return DEFAULT_GROK_BUILD_BASE_URL;
27
43
  }
28
44
 
29
45
  export function isGrokBuildBaseUrlOverrideIgnored(): boolean {
30
- const configured = process.env.GJC_GROK_CLI_BASE_URL || process.env.GROK_CLI_BASE_URL;
46
+ const configured = trustedConfiguredBaseUrl();
31
47
  if (!configured) return false;
32
48
  const normalized = normalizeBaseUrl(configured);
33
- return (
34
- !isAllowedCredentialHost(normalized) && process.env.GJC_GROK_CLI_ALLOW_UNSAFE_BASE_URL !== '1'
35
- );
49
+ return !isAllowedCredentialHost(normalized) && !unsafeOverrideAllowed();
36
50
  }
@@ -43,7 +43,7 @@ async function readBoundedAgentsMdFile(
43
43
  const homeIsStable = async (): Promise<boolean> => {
44
44
  if (!options?.home || !options.homeIdentity) return true;
45
45
  try {
46
- const current = await fs.stat(options.home);
46
+ const current = await fs.stat(options.home, { bigint: true });
47
47
  return current.dev === options.homeIdentity.dev && current.ino === options.homeIdentity.ino;
48
48
  } catch {
49
49
  return false;
@@ -399,10 +399,11 @@ async function loadSlashCommands(ctx: LoadContext): Promise<LoadResult<SlashComm
399
399
  const commandsDir = path.join(dir, "commands");
400
400
  const result = await loadFilesFromDir<SlashCommand>(ctx, commandsDir, PROVIDER_ID, level, {
401
401
  extensions: ["md"],
402
+ recursive: true,
402
403
  scope: readScopeForLevel(level),
403
- transform: (name, content, path, source) => ({
404
- name: name.replace(/\.md$/, ""),
405
- path,
404
+ transform: (_name, content, filePath, source) => ({
405
+ name: path.relative(commandsDir, filePath).replace(/\.md$/, "").split(path.sep).join(":"),
406
+ path: filePath,
406
407
  content,
407
408
  level,
408
409
  _source: source,