@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
@@ -13,6 +13,7 @@ import {
13
13
  MANAGED_OWNER_PREDECESSOR_RUN_ID_ENV,
14
14
  MANAGED_OWNER_PREDECESSOR_TOKEN_ENV,
15
15
  MANAGED_OWNER_TRANSCRIPT_PATH_ENV,
16
+ trustedManagedOwnerLaunchSessionFile,
16
17
  } from "./managed-owner-admission";
17
18
  import {
18
19
  MANAGED_OWNER_INCARNATION_ENV,
@@ -70,11 +71,13 @@ import {
70
71
  } from "./tmux-owner-isolation";
71
72
  import { assertGjcTmuxStagedMutationAuthoritySync } from "./tmux-provider-context";
72
73
  import {
74
+ childGjcSessionIdAssignment,
73
75
  findGjcTmuxSessionByName,
74
76
  findGjcTmuxSessionByScope,
75
77
  type GjcTmuxSessionStatus,
76
78
  type ProvenTmuxSessionIdentity,
77
79
  proveGjcTmuxSessionMutationTarget,
80
+ tmuxLaunchIdentityEnv,
78
81
  } from "./tmux-sessions";
79
82
  import {
80
83
  buildWindowsPowerShellInnerCommand,
@@ -432,6 +435,8 @@ function stripRootTmuxFlag(rawArgs: string[]): string[] {
432
435
 
433
436
  export function applyGjcTmuxProfile(context: GjcTmuxProfileContext): GjcTmuxProfileResult {
434
437
  const env = context.env ?? process.env;
438
+ const cwd = context.cwd ?? process.cwd();
439
+ const identityEnv = tmuxLaunchIdentityEnv(env, cwd);
435
440
  const branchSlug = context.branch ? buildGjcTmuxSessionSlug(context.branch) : (context.branchSlug ?? null);
436
441
  // The psmux UX filter (mouse / set-clipboard / mode-style /
437
442
  // set-window-option) now lives in buildGjcTmuxProfileCommands so every
@@ -447,8 +452,8 @@ export function applyGjcTmuxProfile(context: GjcTmuxProfileContext): GjcTmuxProf
447
452
  branch: context.branch ?? null,
448
453
  branchSlug,
449
454
  project: context.project ?? null,
450
- sessionId: context.sessionId ?? env[GJC_COORDINATOR_SESSION_ID_ENV] ?? null,
451
- sessionStateFile: context.sessionStateFile ?? env[GJC_COORDINATOR_SESSION_STATE_FILE_ENV] ?? null,
455
+ sessionId: context.sessionId ?? identityEnv[GJC_COORDINATOR_SESSION_ID_ENV] ?? null,
456
+ sessionStateFile: context.sessionStateFile ?? identityEnv[GJC_COORDINATOR_SESSION_STATE_FILE_ENV] ?? null,
452
457
  ownerGeneration: context.ownerGeneration ?? null,
453
458
  ownerServerKey: context.ownerServerKey ?? null,
454
459
  version: context.version ?? null,
@@ -462,7 +467,6 @@ export function applyGjcTmuxProfile(context: GjcTmuxProfileContext): GjcTmuxProf
462
467
  });
463
468
  if (commands.length === 0) return { skipped: true, commands: [], failures: [] };
464
469
  const spawnSync = context.spawnSync ?? defaultSpawnSync;
465
- const cwd = context.cwd ?? process.cwd();
466
470
  const options: TmuxSpawnOptions = {
467
471
  cwd,
468
472
  env,
@@ -1105,22 +1109,24 @@ export function buildDefaultTmuxLaunchPlan(context: TmuxLaunchContext): TmuxLaun
1105
1109
  if (policy === "tmux" && !isInteractiveRootLaunch(context.parsed, tty)) return undefined;
1106
1110
 
1107
1111
  const cwd = context.cwd ?? process.cwd();
1112
+ const identityEnv = tmuxLaunchIdentityEnv(env, cwd);
1108
1113
  const branch = context.worktreeBranch ?? context.currentBranch ?? readCurrentBranch(cwd);
1109
1114
  const project = context.project ?? cwd;
1110
- const sessionName = buildGjcTmuxSessionName(env, { branch });
1115
+ const sessionName = buildGjcTmuxSessionName(identityEnv, { branch });
1111
1116
  // Pick the most appropriate tmux binary for this platform. On native Windows
1112
1117
  // the resolver walks psmux / pmux / tmux and uses the first one present on
1113
1118
  // PATH, so the default `gjc --tmux` flow lands on a real multiplexer even
1114
1119
  // without an explicit GJC_TMUX_COMMAND override.
1115
1120
  const resolvedBinary = resolveGjcTmuxBinary({ platform, env });
1116
1121
  const tmuxCommand = resolvedBinary.command;
1117
- const sessionId = env[GJC_COORDINATOR_SESSION_ID_ENV]?.trim() || sessionName;
1122
+ const sessionId = identityEnv[GJC_COORDINATOR_SESSION_ID_ENV]?.trim() || sessionName;
1118
1123
  // The session ROOT is keyed by the active GJC session (GJC_SESSION_ID), NOT the
1119
1124
  // coordinator/tmux identity. Fall back to the coordinator id only for standalone
1120
- // tmux launches with no GJC session context.
1121
- const gjcSessionId = env.GJC_SESSION_ID?.trim() || sessionId;
1125
+ // tmux launches with no GJC session context. Project dotenv pins are already
1126
+ // removed from identityEnv.
1127
+ const gjcSessionId = identityEnv.GJC_SESSION_ID?.trim() || sessionId;
1122
1128
  const sessionStateFile =
1123
- env[GJC_COORDINATOR_SESSION_STATE_FILE_ENV]?.trim() ||
1129
+ identityEnv[GJC_COORDINATOR_SESSION_STATE_FILE_ENV]?.trim() ||
1124
1130
  tmuxRuntimeSessionPath(cwd, gjcSessionId, buildGjcTmuxSessionSlug(sessionName));
1125
1131
  const signingEnv = coordinatorSidecarSigningEnv(env);
1126
1132
  // Stage only the one-shot channel descriptor (path + key id). The PKCS#8 key
@@ -1135,13 +1141,13 @@ export function buildDefaultTmuxLaunchPlan(context: TmuxLaunchContext): TmuxLaun
1135
1141
  (context.diagnosticWriter ?? safeStderrWrite)(formatTmuxUnavailableDiagnostic(platform));
1136
1142
  return undefined;
1137
1143
  }
1138
- const existingSessionName = allowsExistingTmuxAttach(context.parsed, env)
1144
+ const existingSessionName = allowsExistingTmuxAttach(context.parsed, identityEnv)
1139
1145
  ? "existingBranchSessionName" in context
1140
1146
  ? (context.existingBranchSessionName ?? undefined)
1141
1147
  : resolvedBinary.isPsmux && platform === "win32"
1142
- ? explicitTmuxSessionName(env)
1148
+ ? explicitTmuxSessionName(identityEnv)
1143
1149
  : findExistingSessionForLaunch({
1144
- env,
1150
+ env: identityEnv,
1145
1151
  project,
1146
1152
  branch,
1147
1153
  })
@@ -1153,6 +1159,7 @@ export function buildDefaultTmuxLaunchPlan(context: TmuxLaunchContext): TmuxLaun
1153
1159
  execPath: context.execPath ?? process.execPath,
1154
1160
  extraEnv: {
1155
1161
  ...childSigningEnv,
1162
+ ...childGjcSessionIdAssignment(env, identityEnv, cwd, gjcSessionId),
1156
1163
  [GJC_COORDINATOR_SESSION_ID_ENV]: sessionId,
1157
1164
  [GJC_COORDINATOR_SESSION_STATE_FILE_ENV]: sessionStateFile,
1158
1165
  // Carry the GJC-managed session name into the child so tmux-backed
@@ -1209,6 +1216,9 @@ function trustedReplacementAuthority(
1209
1216
  function prepareManagedOwnerLifecycle(plan: TmuxLaunchPlan, context: TmuxLaunchContext): void {
1210
1217
  if (plan.ownerGeneration) return;
1211
1218
  const sessionId = plan.sessionId ?? plan.sessionName;
1219
+ const launchEnv = context.env ?? process.env;
1220
+ const identityEnv = tmuxLaunchIdentityEnv(launchEnv, plan.cwd);
1221
+ const gjcSessionId = identityEnv.GJC_SESSION_ID?.trim() || sessionId;
1212
1222
  const stateDir = path.dirname(plan.sessionStateFile ?? path.join(plan.cwd, ".gjc", "runtime"));
1213
1223
  const baseline = captureOwnerGenerationBaselineSync(stateDir, sessionId);
1214
1224
  const replacement = trustedReplacementAuthority(stateDir, sessionId, baseline);
@@ -1233,6 +1243,7 @@ function prepareManagedOwnerLifecycle(plan: TmuxLaunchPlan, context: TmuxLaunchC
1233
1243
  [GJC_COORDINATOR_SIDECAR_KEY_ID_ENV]: plan.coordinatorSidecarBootstrap.keyId,
1234
1244
  }
1235
1245
  : {}),
1246
+ ...childGjcSessionIdAssignment(launchEnv, identityEnv, plan.cwd, gjcSessionId),
1236
1247
  [GJC_COORDINATOR_SESSION_ID_ENV]: sessionId,
1237
1248
  [GJC_COORDINATOR_SESSION_STATE_FILE_ENV]: plan.sessionStateFile ?? "",
1238
1249
  // The tmux server inherits the bootstrap secret as child environment;
@@ -1249,8 +1260,12 @@ function prepareManagedOwnerLifecycle(plan: TmuxLaunchPlan, context: TmuxLaunchC
1249
1260
  [MANAGED_OWNER_PREDECESSOR_GENERATION_ENV]: replacement.generation,
1250
1261
  [MANAGED_OWNER_PREDECESSOR_RUN_ID_ENV]: replacement.runId,
1251
1262
  [MANAGED_OWNER_PREDECESSOR_INCARNATION_ENV]: replacement.incarnation,
1252
- [MANAGED_OWNER_TRANSCRIPT_PATH_ENV]:
1253
- context.env?.GJC_SESSION_FILE ?? process.env.GJC_SESSION_FILE ?? "",
1263
+ [MANAGED_OWNER_TRANSCRIPT_PATH_ENV]: trustedManagedOwnerLaunchSessionFile(
1264
+ {
1265
+ GJC_SESSION_FILE: context.env?.GJC_SESSION_FILE ?? process.env.GJC_SESSION_FILE,
1266
+ },
1267
+ plan.cwd,
1268
+ ),
1254
1269
  }
1255
1270
  : {}),
1256
1271
  },
@@ -1522,7 +1537,7 @@ export function launchDefaultTmuxIfNeeded(context: TmuxLaunchContext): boolean {
1522
1537
  try {
1523
1538
  if (plan.attachSessionName) {
1524
1539
  const stateDir = env[GJC_TMUX_OWNER_STATE_DIR_ENV]?.trim();
1525
- const sessionId = env[GJC_COORDINATOR_SESSION_ID_ENV]?.trim();
1540
+ const sessionId = tmuxLaunchIdentityEnv(env, plan.cwd)[GJC_COORDINATOR_SESSION_ID_ENV]?.trim();
1526
1541
  const generation = env[GJC_TMUX_OWNER_GENERATION_ENV]?.trim();
1527
1542
  if (!stateDir || !sessionId || !generation) throw new Error("gjc_tmux_provider_authority_unavailable");
1528
1543
  plan.authority = readGjcTmuxProviderAuthoritySync({ stateDir, sessionId, generation });
@@ -1591,7 +1606,7 @@ export function launchDefaultTmuxIfNeeded(context: TmuxLaunchContext): boolean {
1591
1606
  }
1592
1607
  };
1593
1608
  const creationSpawn = plan.authority ? spawnSync : rawSpawnSync;
1594
- const spawnEnv = { ...env };
1609
+ const spawnEnv = { ...tmuxLaunchIdentityEnv(env, plan.cwd) };
1595
1610
  delete spawnEnv[GJC_COORDINATOR_SIDECAR_SIGNING_KEY_ENV];
1596
1611
  const signingEnv = coordinatorSidecarSigningEnv(env);
1597
1612
  Object.assign(spawnEnv, signingEnv);
@@ -9,6 +9,7 @@ import {
9
9
  ensureLaunchWorktree,
10
10
  evacuatePreWorktreeTarget,
11
11
  isReplaceableWorktreeTarget,
12
+ removeOwnedLaunchWorktree,
12
13
  restorePreWorktreeGjc,
13
14
  } from "./launch-worktree";
14
15
 
@@ -246,3 +247,150 @@ describe("acquireTargetLock", () => {
246
247
  expect(fs.existsSync(path.join(worktreePath, ".git"))).toBe(true);
247
248
  });
248
249
  });
250
+
251
+ function ownedReceipt(): { created: true; reused: false; createdBranch: false } {
252
+ return { created: true, reused: false, createdBranch: false };
253
+ }
254
+
255
+ function detachedPlan(repoRoot: string, worktreePath: string): GjcLaunchWorktreePlan {
256
+ return {
257
+ enabled: true,
258
+ repoRoot,
259
+ worktreePath,
260
+ detached: true,
261
+ baseRef: "HEAD",
262
+ branchName: null,
263
+ };
264
+ }
265
+
266
+ /** Replace a real worktree directory with a symlink to a directory that copies its `.git` file. */
267
+ function swapWorktreeForForgedGitdir(worktreePath: string, forgeDir: string): void {
268
+ const gitFile = fs.readFileSync(path.join(worktreePath, ".git"), "utf8");
269
+ fs.mkdirSync(forgeDir, { recursive: true });
270
+ fs.writeFileSync(path.join(forgeDir, ".git"), gitFile.endsWith("\n") ? gitFile : `${gitFile}\n`);
271
+ fs.writeFileSync(path.join(forgeDir, "needle.txt"), "KEEP-NEEDLE\n");
272
+ fs.rmSync(worktreePath, { recursive: true, force: true });
273
+ fs.symlinkSync(forgeDir, worktreePath, "dir");
274
+ }
275
+
276
+ describe("removeOwnedLaunchWorktree", () => {
277
+ test("still removes a real directory worktree", () => {
278
+ const repo = initRepo("gjc-rm-real-repo-");
279
+ const bucket = makeTempDir("gjc-rm-real-bucket-");
280
+ const worktreePath = path.join(bucket, "wt");
281
+ git(repo, ["worktree", "add", "-q", "--detach", worktreePath, "HEAD"]);
282
+ fs.writeFileSync(path.join(worktreePath, "needle.txt"), "inside\n");
283
+
284
+ removeOwnedLaunchWorktree(detachedPlan(repo, worktreePath), ownedReceipt());
285
+
286
+ expect(fs.existsSync(worktreePath)).toBe(false);
287
+ expect(countRegisteredWorktrees(repo)).toBe(1);
288
+ });
289
+
290
+ test("still removes a worktree reached through an in-repo directory symlink", () => {
291
+ const repo = initRepo("gjc-rm-inside-link-repo-");
292
+ const inside = path.join(repo, "inside");
293
+ fs.mkdirSync(inside);
294
+ fs.symlinkSync(inside, path.join(repo, ".worktrees"), "dir");
295
+ const worktreePath = path.join(repo, ".worktrees", "wt");
296
+ git(repo, ["worktree", "add", "-q", "--detach", worktreePath, "HEAD"]);
297
+
298
+ removeOwnedLaunchWorktree(detachedPlan(repo, worktreePath), ownedReceipt());
299
+
300
+ expect(fs.existsSync(path.join(inside, "wt"))).toBe(false);
301
+ expect(fs.lstatSync(path.join(repo, ".worktrees")).isSymbolicLink()).toBe(true);
302
+ expect(countRegisteredWorktrees(repo)).toBe(1);
303
+ });
304
+
305
+ test("does not delete the referent when the worktree path is a symlink to a forged gitdir", () => {
306
+ const repo = initRepo("gjc-rm-forge-repo-");
307
+ const bucket = makeTempDir("gjc-rm-forge-bucket-");
308
+ const worktreePath = path.join(bucket, "wt");
309
+ git(repo, ["worktree", "add", "-q", "--detach", worktreePath, "HEAD"]);
310
+ const forge = makeTempDir("gjc-rm-forge-target-");
311
+ swapWorktreeForForgedGitdir(worktreePath, forge);
312
+
313
+ removeOwnedLaunchWorktree(detachedPlan(repo, worktreePath), ownedReceipt());
314
+
315
+ expect(fs.readFileSync(path.join(forge, "needle.txt"), "utf8")).toBe("KEEP-NEEDLE\n");
316
+ expect(fs.lstatSync(path.join(forge, ".git")).isFile()).toBe(true);
317
+ expect(fs.existsSync(worktreePath)).toBe(false);
318
+ });
319
+
320
+ test("still leaves an outside symlink when an in-repo bucket is retargeted", () => {
321
+ const repo = initRepo("gjc-rm-outlink-repo-");
322
+ const bucket = path.join(repo, ".worktrees");
323
+ fs.mkdirSync(bucket);
324
+ const worktreePath = path.join(bucket, "wt");
325
+ git(repo, ["worktree", "add", "-q", "--detach", worktreePath, "HEAD"]);
326
+ fs.rmSync(bucket, { recursive: true, force: true });
327
+ const outside = makeTempDir("gjc-rm-outlink-outside-");
328
+ const vault = makeTempDir("gjc-rm-outlink-vault-");
329
+ fs.writeFileSync(path.join(vault, "needle.txt"), "KEEP-NEEDLE\n");
330
+ const outsideLink = path.join(outside, "wt");
331
+ fs.symlinkSync(vault, outsideLink, "dir");
332
+ fs.symlinkSync(outside, bucket, "dir");
333
+
334
+ removeOwnedLaunchWorktree(detachedPlan(repo, worktreePath), ownedReceipt());
335
+
336
+ expect(fs.lstatSync(outsideLink).isSymbolicLink()).toBe(true);
337
+ expect(fs.readlinkSync(outsideLink)).toBe(vault);
338
+ expect(fs.readFileSync(path.join(vault, "needle.txt"), "utf8")).toBe("KEEP-NEEDLE\n");
339
+ expect(fs.lstatSync(bucket).isSymbolicLink()).toBe(true);
340
+ });
341
+
342
+ test("does not delete a directory outside the repo when an in-repo bucket symlink is retargeted", () => {
343
+ const repo = initRepo("gjc-rm-retarget-repo-");
344
+ const bucket = path.join(repo, ".worktrees");
345
+ fs.mkdirSync(bucket);
346
+ const worktreePath = path.join(bucket, "wt");
347
+ git(repo, ["worktree", "add", "-q", "--detach", worktreePath, "HEAD"]);
348
+ const gitFile = fs.readFileSync(path.join(worktreePath, ".git"), "utf8");
349
+ fs.rmSync(bucket, { recursive: true, force: true });
350
+ const outside = makeTempDir("gjc-rm-retarget-outside-");
351
+ const outsideWt = path.join(outside, "wt");
352
+ fs.mkdirSync(outsideWt);
353
+ fs.writeFileSync(path.join(outsideWt, ".git"), gitFile.endsWith("\n") ? gitFile : `${gitFile}\n`);
354
+ fs.writeFileSync(path.join(outsideWt, "needle.txt"), "KEEP-NEEDLE\n");
355
+ fs.symlinkSync(outside, bucket, "dir");
356
+
357
+ removeOwnedLaunchWorktree(detachedPlan(repo, worktreePath), ownedReceipt());
358
+
359
+ expect(fs.readFileSync(path.join(outsideWt, "needle.txt"), "utf8")).toBe("KEEP-NEEDLE\n");
360
+ expect(fs.lstatSync(bucket).isSymbolicLink()).toBe(true);
361
+ });
362
+ });
363
+
364
+ describe("commitPreWorktreeRestore symlink rollback", () => {
365
+ test("rolls back a symlinked worktree without deleting the forged referent", () => {
366
+ const repo = initRepo("gjc-rb-forge-repo-");
367
+ const bucket = makeTempDir("gjc-rb-forge-bucket-");
368
+ const worktreePath = path.join(bucket, "wt");
369
+ git(repo, ["worktree", "add", "-q", "--detach", worktreePath, "HEAD"]);
370
+ const forge = makeTempDir("gjc-rb-forge-target-");
371
+ swapWorktreeForForgedGitdir(worktreePath, forge);
372
+ const stashPath = path.join(bucket, ".gjc-pre-wt-wt");
373
+ fs.mkdirSync(stashPath, { recursive: true });
374
+ fs.writeFileSync(path.join(stashPath, "launcher.txt"), "config");
375
+
376
+ let message = "";
377
+ try {
378
+ commitPreWorktreeRestore(
379
+ {
380
+ restore: () => {
381
+ throw new Error("overlay exploded");
382
+ },
383
+ stashPath,
384
+ },
385
+ detachedPlan(repo, worktreePath),
386
+ );
387
+ throw new Error("expected commitPreWorktreeRestore to throw");
388
+ } catch (error) {
389
+ message = error instanceof Error ? error.message : String(error);
390
+ }
391
+
392
+ expect(message).toMatch(/worktree_gjc_restore_failed/);
393
+ expect(fs.readFileSync(path.join(forge, "needle.txt"), "utf8")).toBe("KEEP-NEEDLE\n");
394
+ expect(fs.existsSync(path.join(stashPath, "launcher.txt"))).toBe(true);
395
+ });
396
+ });
@@ -244,9 +244,34 @@ function resolveWorktreeBucket(repoRoot: string): string {
244
244
  return resolveWorktreeBucketForPath(repoRoot, trustedWorktreeBucketEnv(process.env), os.homedir(), path);
245
245
  }
246
246
 
247
+ function throwIfBucketRealpathEscapesRepository(repoRoot: string, bucketPath: string): void {
248
+ let realBucket: string;
249
+ try {
250
+ realBucket = fs.realpathSync(bucketPath);
251
+ } catch (error) {
252
+ // A missing bucket is created later. A dangling link is classified by inspectBucketDir.
253
+ if (fileSystemErrorCode(error) === "ENOENT") return;
254
+ throw error;
255
+ }
256
+ const relativeReal = path.relative(fs.realpathSync(repoRoot), realBucket);
257
+ if (relativeReal.startsWith(`..${path.sep}`) || relativeReal === ".." || path.isAbsolute(relativeReal)) {
258
+ throw new Error(
259
+ [
260
+ "worktree_bucket_escapes_repository",
261
+ "The GJC launch worktree bucket resolves outside the repository.",
262
+ `Path: ${formatBucketPath(bucketPath)}`,
263
+ "Safe remediation: replace the path with a real directory inside the repository, or set GJC_WORKTREE_DIR to a directory outside the repository. GJC did not create a worktree.",
264
+ ].join("\n"),
265
+ );
266
+ }
267
+ }
268
+
247
269
  function ensureRepositoryBucketIgnored(repoRoot: string, bucketPath: string): void {
248
270
  const relativeBucket = path.relative(repoRoot, bucketPath);
271
+ // Lexical paths outside the repo are the operator override and stay allowed.
249
272
  if (relativeBucket.startsWith(`..${path.sep}`) || relativeBucket === ".." || path.isAbsolute(relativeBucket)) return;
273
+ // git check-ignore accepts a gitignored symlink without looking at its target.
274
+ throwIfBucketRealpathEscapesRepository(repoRoot, bucketPath);
250
275
  const ignoreProbe = path.join(relativeBucket, ".gjc-worktree-probe");
251
276
  for (const candidate of [relativeBucket, ignoreProbe]) {
252
277
  const result = Bun.spawnSync(["git", "check-ignore", "--quiet", "--", candidate], {
@@ -702,17 +727,87 @@ function tryRestore(restore: () => void): void {
702
727
  }
703
728
  }
704
729
 
705
- /** Best-effort removal of a worktree GJC just created, used when a later step must roll it back. */
706
- function rollbackCreatedWorktree(plan: GjcLaunchWorktreePlan): void {
730
+ function lexicalPathInsideRepository(repoRoot: string, targetPath: string): boolean {
731
+ const relative = path.relative(path.resolve(repoRoot), path.resolve(targetPath));
732
+ return !(relative.startsWith(`..${path.sep}`) || relative === ".." || path.isAbsolute(relative));
733
+ }
734
+
735
+ function realpathEscapesRepository(repoRoot: string, targetPath: string): boolean {
736
+ let realTarget: string;
737
+ try {
738
+ realTarget = fs.realpathSync(targetPath);
739
+ } catch (error) {
740
+ // A missing path is not an escape. Anything else is not safe to hand to remove --force.
741
+ if (fileSystemErrorCode(error) === "ENOENT") return false;
742
+ return true;
743
+ }
744
+ const relativeReal = path.relative(fs.realpathSync(repoRoot), realTarget);
745
+ return relativeReal.startsWith(`..${path.sep}`) || relativeReal === ".." || path.isAbsolute(relativeReal);
746
+ }
747
+
748
+ function pruneWorktreeRegistration(repoRoot: string): void {
707
749
  try {
708
- runGit(plan.repoRoot, ["worktree", "remove", "--force", plan.worktreePath]);
750
+ runGit(repoRoot, ["worktree", "prune"]);
709
751
  } catch {
752
+ // Best-effort. Callers report their own failure.
753
+ }
754
+ }
755
+
756
+ /**
757
+ * Remove a launch worktree this process just created.
758
+ *
759
+ * `git worktree remove --force` follows a symlink and deletes the referent when a
760
+ * copied `.git` gitdir file passes validation. Unlink that link only when its
761
+ * parent stays inside the repository, or when the path is an explicit external
762
+ * worktree. An in-repo path whose parent resolves outside the repository is
763
+ * left untouched. A real directory is still removed with `--force`.
764
+ */
765
+ function removeCreatedWorktree(repoRoot: string, worktreePath: string): void {
766
+ let stat: fs.Stats | undefined;
767
+ try {
768
+ stat = fs.lstatSync(worktreePath);
769
+ } catch (error) {
770
+ if (fileSystemErrorCode(error) !== "ENOENT") {
771
+ pruneWorktreeRegistration(repoRoot);
772
+ return;
773
+ }
774
+ }
775
+ // A retargeted in-repo bucket can place this path's parent outside the repository.
776
+ // Do not unlink that outside entry. An explicit external worktree is not this case.
777
+ if (
778
+ lexicalPathInsideRepository(repoRoot, worktreePath) &&
779
+ realpathEscapesRepository(repoRoot, path.dirname(worktreePath))
780
+ ) {
781
+ pruneWorktreeRegistration(repoRoot);
782
+ return;
783
+ }
784
+ if (stat?.isSymbolicLink()) {
710
785
  try {
711
- runGit(plan.repoRoot, ["worktree", "prune"]);
786
+ fs.unlinkSync(worktreePath);
712
787
  } catch {
713
- // Best-effort rollback; the restore failure below is the actionable error.
788
+ // The link may already be gone.
714
789
  }
790
+ pruneWorktreeRegistration(repoRoot);
791
+ return;
792
+ }
793
+ if (
794
+ stat?.isDirectory() &&
795
+ lexicalPathInsideRepository(repoRoot, worktreePath) &&
796
+ realpathEscapesRepository(repoRoot, worktreePath)
797
+ ) {
798
+ pruneWorktreeRegistration(repoRoot);
799
+ return;
715
800
  }
801
+ try {
802
+ runGit(repoRoot, ["worktree", "remove", "--force", worktreePath]);
803
+ } catch {
804
+ pruneWorktreeRegistration(repoRoot);
805
+ }
806
+ }
807
+
808
+ /** Best-effort removal of a worktree GJC just created, used when a later step must roll it back. */
809
+ function rollbackCreatedWorktree(plan: GjcLaunchWorktreePlan): void {
810
+ removeCreatedWorktree(plan.repoRoot, plan.worktreePath);
716
811
  }
717
812
 
718
813
  /**
@@ -1205,15 +1300,7 @@ export function removeOwnedLaunchWorktree(
1205
1300
  receipt: { created: boolean; reused: boolean; createdBranch: boolean },
1206
1301
  ): void {
1207
1302
  if (!receipt.created || receipt.reused) return;
1208
- try {
1209
- runGit(plan.repoRoot, ["worktree", "remove", "--force", plan.worktreePath]);
1210
- } catch {
1211
- try {
1212
- runGit(plan.repoRoot, ["worktree", "prune"]);
1213
- } catch {
1214
- // Proof stays on the ledger; leftover classify is best-effort after abort.
1215
- }
1216
- }
1303
+ removeCreatedWorktree(plan.repoRoot, plan.worktreePath);
1217
1304
  if (receipt.createdBranch && plan.branchName) {
1218
1305
  try {
1219
1306
  runGit(plan.repoRoot, ["branch", "-D", plan.branchName]);
@@ -2,8 +2,10 @@ import { Buffer } from "node:buffer";
2
2
  import * as crypto from "node:crypto";
3
3
  import * as fs from "node:fs/promises";
4
4
  import * as path from "node:path";
5
+ import { canonicalEnvKey, type ProjectEnvSnapshot, projectEnvSnapshot } from "@gajae-code/utils/env-file";
5
6
  import type { ManagedOwnerBinding, ManagedOwnerSigabrtReceipt } from "./managed-owner-supervisor";
6
7
  import { assertSafePathComponent } from "./session-layout";
8
+ import { trustedCoordinatorEnv } from "./sidecar-bootstrap-env";
7
9
  import { lifecyclePaths } from "./tmux-owner-isolation";
8
10
  import {
9
11
  persistUltragoalRecoveryDecision,
@@ -23,6 +25,48 @@ export const MANAGED_OWNER_PREDECESSOR_GENERATION_ENV = "GJC_MANAGED_OWNER_PREDE
23
25
  export const MANAGED_OWNER_PREDECESSOR_RUN_ID_ENV = "GJC_MANAGED_OWNER_PREDECESSOR_RUN_ID";
24
26
  export const MANAGED_OWNER_PREDECESSOR_INCARNATION_ENV = "GJC_MANAGED_OWNER_PREDECESSOR_INCARNATION";
25
27
  export const MANAGED_OWNER_TRANSCRIPT_PATH_ENV = "GJC_MANAGED_OWNER_TRANSCRIPT_PATH";
28
+ const MANAGED_OWNER_SESSION_FILE_ENV = "GJC_SESSION_FILE";
29
+
30
+ /**
31
+ * Operator environment only. A project dotenv value, including a dynamic
32
+ * declaration, must not choose the recovery transcript. A static
33
+ * `GJC_SESSION_FILE` declaration is the same source: the launcher copies it
34
+ * into the transcript variable. The operator bytes are kept, including
35
+ * surrounding whitespace; provenance is not path normalization.
36
+ */
37
+ function operatorOwnedEnvValue(name: string, raw: string | undefined, snapshot: ProjectEnvSnapshot): string {
38
+ if (!raw) return "";
39
+ const key = canonicalEnvKey(name);
40
+ const declared = snapshot.values[key];
41
+ if (declared !== undefined && (snapshot.dynamic.has(key) || declared === raw)) return "";
42
+ const verdict = trustedCoordinatorEnv(name, { [name]: raw }, snapshot);
43
+ if (verdict === undefined && raw.trim().length > 0) return "";
44
+ return raw;
45
+ }
46
+
47
+ export function trustedManagedOwnerTranscriptPath(env: NodeJS.ProcessEnv = process.env, cwd = process.cwd()): string {
48
+ const snapshot = projectEnvSnapshot(cwd);
49
+ const raw = operatorOwnedEnvValue(
50
+ MANAGED_OWNER_TRANSCRIPT_PATH_ENV,
51
+ env[MANAGED_OWNER_TRANSCRIPT_PATH_ENV],
52
+ snapshot,
53
+ );
54
+ if (!raw) return "";
55
+ const sessionKey = canonicalEnvKey(MANAGED_OWNER_SESSION_FILE_ENV);
56
+ if (snapshot.dynamic.has(sessionKey)) return raw;
57
+ const declared = snapshot.values[sessionKey];
58
+ if (declared !== undefined && raw === declared) return "";
59
+ return raw;
60
+ }
61
+
62
+ /** Session file forwarded to a replacement child. Same provenance rule. */
63
+ export function trustedManagedOwnerLaunchSessionFile(env: NodeJS.ProcessEnv, cwd: string): string {
64
+ return operatorOwnedEnvValue(
65
+ MANAGED_OWNER_SESSION_FILE_ENV,
66
+ env[MANAGED_OWNER_SESSION_FILE_ENV],
67
+ projectEnvSnapshot(cwd),
68
+ );
69
+ }
26
70
 
27
71
  export interface ManagedOwnerRecoveryContext {
28
72
  root: string;
@@ -295,7 +339,7 @@ export async function admitManagedOwnerBeforeCli(): Promise<ManagedOwnerAdmissio
295
339
  },
296
340
  receipt,
297
341
  admission,
298
- transcriptPath: process.env[MANAGED_OWNER_TRANSCRIPT_PATH_ENV] ?? "",
342
+ transcriptPath: trustedManagedOwnerTranscriptPath(),
299
343
  });
300
344
  await persistUltragoalRecoveryDecision({
301
345
  cwd: process.cwd(),
@@ -330,7 +374,7 @@ export async function completeManagedOwnerRecovery(
330
374
  binding: recoveryBinding,
331
375
  receipt: context.receipt,
332
376
  admission: context.admission,
333
- transcriptPath: process.env[MANAGED_OWNER_TRANSCRIPT_PATH_ENV] ?? "",
377
+ transcriptPath: trustedManagedOwnerTranscriptPath(),
334
378
  protectedPaths: context.decision.snapshot?.protectedPaths,
335
379
  sanctionedDeltas: context.decision.snapshot?.sanctionedDeltas,
336
380
  absentArtifacts: context.decision.snapshot?.absentArtifacts,