@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
@@ -1896,10 +1896,10 @@ const EXECUTION_APPROVAL_TRANSCRIPT_MAX_BYTES = 128 * 1024 * 1024;
1896
1896
  const DEEP_INTERVIEW_EXECUTION_APPROVAL_RECORD_MAX_BYTES = 16 * 1024;
1897
1897
  const DEEP_INTERVIEW_EXECUTION_APPROVAL_ID_MAX_LENGTH = 256;
1898
1898
 
1899
+ // The boundary binds approval to transcript content (prefix length/sha256, leaf, Ask call), not to
1900
+ // the file's inode: SessionManager persists full rewrites by atomic rename, which replaces the inode.
1899
1901
  export interface ExecutionApprovalTranscriptBoundary {
1900
1902
  byte_length: number;
1901
- device: string;
1902
- inode: string;
1903
1903
  leaf_id: string | null;
1904
1904
  approval_tool_call_id: string;
1905
1905
  }
@@ -1909,10 +1909,6 @@ function isExecutionApprovalTranscriptBoundary(value: unknown): value is Executi
1909
1909
  isPlainObject(value) &&
1910
1910
  Number.isSafeInteger(value.byte_length) &&
1911
1911
  (value.byte_length as number) > 0 &&
1912
- typeof value.device === "string" &&
1913
- /^\d+$/.test(value.device) &&
1914
- typeof value.inode === "string" &&
1915
- /^\d+$/.test(value.inode) &&
1916
1912
  (value.leaf_id === null || isExecutionApprovalId(value.leaf_id)) &&
1917
1913
  isExecutionApprovalId(value.approval_tool_call_id)
1918
1914
  );
@@ -2037,8 +2033,6 @@ export async function captureExecutionApprovalTranscriptBoundary(
2037
2033
  throw new StateCommandError(2, "execution approval transcript boundary lacks the current Ask call");
2038
2034
  return {
2039
2035
  byte_length: Buffer.byteLength(text),
2040
- device: after.dev.toString(),
2041
- inode: after.ino.toString(),
2042
2036
  leaf_id: prefix.leaf,
2043
2037
  approval_tool_call_id: approvalToolCallId,
2044
2038
  };
@@ -2062,13 +2056,8 @@ export async function assertExecutionApprovalTranscriptBoundary(
2062
2056
  const after = await fs.lstat(transcriptPath, { bigint: true });
2063
2057
  if ((await fs.realpath(transcriptPath)) !== path.resolve(transcriptPath))
2064
2058
  throw new StateCommandError(2, "execution approval transcript path is not canonical");
2065
- if (
2066
- !sameBoundedFileIdentity(before, after) ||
2067
- after.dev.toString() !== boundary.device ||
2068
- after.ino.toString() !== boundary.inode ||
2069
- text === undefined
2070
- )
2071
- throw new StateCommandError(2, "execution approval transcript identity changed");
2059
+ if (!sameBoundedFileIdentity(before, after) || text === undefined)
2060
+ throw new StateCommandError(2, "execution approval transcript changed while it was read");
2072
2061
  const bytes = Buffer.from(text);
2073
2062
  const prefix = bytes.subarray(0, boundary.byte_length);
2074
2063
  if (prefix.length !== boundary.byte_length || createHash("sha256").update(prefix).digest("hex") !== transcriptSha256)
@@ -3222,6 +3211,21 @@ async function assertSanctionedExecutionApprovalAudit(
3222
3211
  )
3223
3212
  throw new StateCommandError(2, "deep-interview execution approval lacks sanctioned transition provenance");
3224
3213
 
3214
+ // The approval index is written only after the append-only ledger entry.
3215
+ // A pending journal means that append did not finish, so the index must
3216
+ // not authorize execution handoff.
3217
+ let approvalJournal: WorkflowTransactionJournal | undefined;
3218
+ try {
3219
+ approvalJournal = await readWorkflowTransactionJournal(cwd, sessionId, mutationId);
3220
+ } catch (error) {
3221
+ throw new StateCommandError(
3222
+ 2,
3223
+ `failed to read execution approval journal: ${error instanceof Error ? error.message : String(error)}`,
3224
+ );
3225
+ }
3226
+ if (approvalJournal?.status === "pending")
3227
+ throw new StateCommandError(2, "deep-interview execution approval journal is incomplete");
3228
+
3225
3229
  const indexedApprovalPath = path.join(sessionStateDir(cwd, sessionId), "deep-interview-approval-audit.json");
3226
3230
  let indexedRaw = "";
3227
3231
  try {
@@ -5067,8 +5071,6 @@ async function appendExecutionApprovalAudit(
5067
5071
  } = {},
5068
5072
  ): Promise<void> {
5069
5073
  const entry = buildExecutionApprovalAuditEntry(options);
5070
- await writeExecutionApprovalIndex(options, entry);
5071
- await hooks.afterIndex?.();
5072
5074
  const filePath = auditPath(options.cwd, options.sessionId);
5073
5075
  await withWorkflowStateLock(
5074
5076
  filePath,
@@ -5089,6 +5091,8 @@ async function appendExecutionApprovalAudit(
5089
5091
  },
5090
5092
  { cwd: options.cwd },
5091
5093
  );
5094
+ await writeExecutionApprovalIndex(options, entry);
5095
+ await hooks.afterIndex?.();
5092
5096
  await hooks.afterAudit?.();
5093
5097
  }
5094
5098
 
@@ -5296,8 +5300,6 @@ async function handleApproveExecutionRecordLocked(
5296
5300
  indexedMatches = isPlainObject(parsed) && JSON.stringify(parsed) === JSON.stringify(expectedApprovalEntry);
5297
5301
  } catch {}
5298
5302
  }
5299
- if (!indexedMatches) await writeExecutionApprovalIndex(approvalOptions, expectedApprovalEntry);
5300
- recoverySteps.add("approval-index");
5301
5303
  let approvalAuditOffset = pendingJournal.approval_audit_offset;
5302
5304
  if (!recoverySteps.has("approval-audit")) {
5303
5305
  approvalAuditOffset = await appendExecutionApprovalAuditIdempotent(
@@ -5312,6 +5314,8 @@ async function handleApproveExecutionRecordLocked(
5312
5314
  );
5313
5315
  recoverySteps.add("approval-audit");
5314
5316
  }
5317
+ if (!indexedMatches) await writeExecutionApprovalIndex(approvalOptions, expectedApprovalEntry);
5318
+ recoverySteps.add("approval-index");
5315
5319
  await updateWorkflowTransactionJournal(cwd, selectors.gjcSessionId, existingReceipt.mutation_id, {
5316
5320
  steps: [...recoverySteps],
5317
5321
  approval_audit_offset: approvalAuditOffset,
@@ -1158,10 +1158,113 @@ export async function updateJsonAtomic<T = unknown>(
1158
1158
  );
1159
1159
  }
1160
1160
 
1161
+ async function assertAppendRegularFile(filePath: string): Promise<void> {
1162
+ try {
1163
+ const stat = await fs.lstat(filePath);
1164
+ if (stat.isSymbolicLink() || !stat.isFile()) throw new Error("state append path is not a regular file");
1165
+ } catch (error) {
1166
+ if (!isErrno(error, "ENOENT")) throw error;
1167
+ }
1168
+ }
1169
+
1170
+ /**
1171
+ * Append bytes the same way `appendAuditEntry` opens its log: mode 0600, `O_NOFOLLOW`
1172
+ * when the path already exists, and `O_EXCL` when it does not. An existing mode 0666
1173
+ * file, and any other group or other permission bits, is tightened to 0600 before the write.
1174
+ */
1175
+ async function tightenReadOnlyAppendMode(filePath: string): Promise<void> {
1176
+ if (process.platform === "win32") return;
1177
+ const noFollow = fsConstants.O_NOFOLLOW ?? 0;
1178
+ const initialStat = await fs.lstat(filePath, { bigint: true });
1179
+ if (initialStat.isSymbolicLink() || !initialStat.isFile())
1180
+ throw new Error("state append path is not a regular file");
1181
+ // A duplicate idempotent append must not require a writable file. Owner-only
1182
+ // modes such as 0400 stay as they are; only group or other bits are tightened.
1183
+ const handle = await fs.open(filePath, fsConstants.O_RDONLY | noFollow);
1184
+ try {
1185
+ const openedStat = await handle.stat({ bigint: true });
1186
+ const pathStat = await fs.lstat(filePath, { bigint: true });
1187
+ const sameObject = (left: nodeFs.BigIntStats, right: nodeFs.BigIntStats) =>
1188
+ left.dev === right.dev && left.ino === right.ino && left.nlink === right.nlink;
1189
+ if (
1190
+ openedStat.isSymbolicLink() ||
1191
+ !openedStat.isFile() ||
1192
+ pathStat.isSymbolicLink() ||
1193
+ !pathStat.isFile() ||
1194
+ !sameObject(initialStat, openedStat) ||
1195
+ !sameObject(openedStat, pathStat)
1196
+ )
1197
+ throw new Error("state append path identity changed before append");
1198
+ if ((Number(openedStat.mode) & 0o077) === 0) return;
1199
+ await handle.chmod(0o600);
1200
+ const tightened = await handle.stat({ bigint: true });
1201
+ if ((Number(tightened.mode) & 0o777) !== 0o600)
1202
+ throw new Error("state append path could not be tightened to mode 0600");
1203
+ } finally {
1204
+ await handle.close();
1205
+ }
1206
+ }
1207
+
1208
+ async function appendOwnerOnlyText(filePath: string, text: string): Promise<void> {
1209
+ await fs.mkdir(path.dirname(filePath), { recursive: true });
1210
+ await assertAppendRegularFile(filePath);
1211
+ const noFollow = process.platform === "win32" ? 0 : (fsConstants.O_NOFOLLOW ?? 0);
1212
+ let handle: fs.FileHandle | undefined;
1213
+ let initialStat: nodeFs.BigIntStats | undefined;
1214
+ // O_EXCL matches appendAuditEntry for a missing path. Concurrent appends can
1215
+ // create the same file together, so an EEXIST loser retries the existing-file
1216
+ // open, which carries O_NOFOLLOW and will not follow a symlink.
1217
+ for (let attempt = 0; attempt < 2; attempt++) {
1218
+ initialStat = undefined;
1219
+ try {
1220
+ initialStat = await fs.lstat(filePath, { bigint: true });
1221
+ if (initialStat.isSymbolicLink() || !initialStat.isFile())
1222
+ throw new Error("state append path is not a regular file");
1223
+ } catch (error) {
1224
+ if (!isErrno(error, "ENOENT")) throw error;
1225
+ }
1226
+ const flags = initialStat
1227
+ ? fsConstants.O_WRONLY | fsConstants.O_APPEND | noFollow
1228
+ : fsConstants.O_WRONLY | fsConstants.O_APPEND | fsConstants.O_CREAT | fsConstants.O_EXCL;
1229
+ try {
1230
+ handle = await fs.open(filePath, flags, 0o600);
1231
+ break;
1232
+ } catch (error) {
1233
+ if (isErrno(error, "ELOOP")) throw new Error("state append path is not a regular file");
1234
+ if (initialStat === undefined && isErrno(error, "EEXIST") && attempt === 0) continue;
1235
+ throw error;
1236
+ }
1237
+ }
1238
+ if (!handle) throw new Error("state append path identity changed before append");
1239
+ try {
1240
+ const openedStat = await handle.stat({ bigint: true });
1241
+ const pathStat = await fs.lstat(filePath, { bigint: true });
1242
+ const sameObject = (left: nodeFs.BigIntStats, right: nodeFs.BigIntStats) =>
1243
+ left.dev === right.dev && left.ino === right.ino && left.nlink === right.nlink;
1244
+ if (
1245
+ openedStat.isSymbolicLink() ||
1246
+ !openedStat.isFile() ||
1247
+ pathStat.isSymbolicLink() ||
1248
+ !pathStat.isFile() ||
1249
+ (initialStat !== undefined && !sameObject(initialStat, openedStat)) ||
1250
+ !sameObject(openedStat, pathStat)
1251
+ )
1252
+ throw new Error("state append path identity changed before append");
1253
+ if (process.platform !== "win32" && (Number(openedStat.mode) & 0o077) !== 0) {
1254
+ await handle.chmod(0o600);
1255
+ const tightened = await handle.stat({ bigint: true });
1256
+ if ((Number(tightened.mode) & 0o777) !== 0o600)
1257
+ throw new Error("state append path could not be tightened to mode 0600");
1258
+ }
1259
+ await handle.writeFile(text, "utf-8");
1260
+ } finally {
1261
+ await handle?.close();
1262
+ }
1263
+ }
1264
+
1161
1265
  export async function appendJsonl(targetPath: string, entry: unknown, options?: StateWriterOptions): Promise<string> {
1162
1266
  const filePath = resolveGjcTarget(targetPath, cwdForOptions(options));
1163
- await fs.mkdir(path.dirname(filePath), { recursive: true });
1164
- await fs.appendFile(filePath, `${JSON.stringify(entry)}\n`, "utf-8");
1267
+ await appendOwnerOnlyText(filePath, `${JSON.stringify(entry)}\n`);
1165
1268
  await maybeAudit(filePath, options);
1166
1269
  return filePath;
1167
1270
  }
@@ -1259,12 +1362,14 @@ export async function appendJsonlIdempotent(
1259
1362
  return lockResolvedWorkflowTarget(
1260
1363
  filePath,
1261
1364
  async () => {
1365
+ await assertAppendRegularFile(filePath);
1262
1366
  const existing = await readJsonlEntries(filePath);
1263
1367
  const duplicate = findJsonlDuplicate(existing, entry, options);
1264
1368
  if (duplicate !== undefined) {
1369
+ await tightenReadOnlyAppendMode(filePath);
1265
1370
  return { path: filePath, appended: false, duplicate };
1266
1371
  }
1267
- await fs.appendFile(filePath, `${JSON.stringify(entry)}\n`, "utf-8");
1372
+ await appendOwnerOnlyText(filePath, `${JSON.stringify(entry)}\n`);
1268
1373
  await maybeAudit(filePath, options);
1269
1374
  return { path: filePath, appended: true };
1270
1375
  },
@@ -1274,8 +1379,7 @@ export async function appendJsonlIdempotent(
1274
1379
 
1275
1380
  export async function appendText(targetPath: string, text: string, options?: StateWriterOptions): Promise<string> {
1276
1381
  const filePath = resolveGjcTarget(targetPath, cwdForOptions(options));
1277
- await fs.mkdir(path.dirname(filePath), { recursive: true });
1278
- await fs.appendFile(filePath, text, "utf-8");
1382
+ await appendOwnerOnlyText(filePath, text);
1279
1383
  await maybeAudit(filePath, options);
1280
1384
  return filePath;
1281
1385
  }
@@ -4,6 +4,8 @@ import * as fsSync from "node:fs";
4
4
  import * as fs from "node:fs/promises";
5
5
  import * as path from "node:path";
6
6
  import type { Process } from "@gajae-code/natives";
7
+ import { startupProjectEnvSnapshot } from "@gajae-code/utils/env";
8
+ import { canonicalEnvKey, type ProjectEnvSnapshot, projectEnvSnapshot } from "@gajae-code/utils/env-file";
7
9
  import { nativeProcessBindings } from "@gajae-code/utils/native-process";
8
10
  import { managedSecurityFailureClassification } from "../session/internal/managed-session-storage";
9
11
  import { readLinuxProcStartTime, readLinuxProcStartTimeSync } from "./linux-proc";
@@ -23,6 +25,7 @@ import {
23
25
  GJC_TMUX_OWNER_SERVER_KEY_ENV,
24
26
  GJC_TMUX_OWNER_STATE_DIR_ENV,
25
27
  } from "./session-state-sidecar";
28
+ import { trustedCoordinatorEnv } from "./sidecar-bootstrap-env";
26
29
  import {
27
30
  assertGjcTmuxMutationAuthoritySync,
28
31
  bindGjcTmuxProviderAuthority,
@@ -247,6 +250,81 @@ export function __setMutationServerProofForTests(
247
250
  mutationServerProofTestDependency = dependency;
248
251
  }
249
252
 
253
+ const TMUX_LAUNCH_IDENTITY_ENV_NAMES = [
254
+ "GJC_SESSION_ID",
255
+ GJC_COORDINATOR_SESSION_ID_ENV,
256
+ GJC_COORDINATOR_SESSION_STATE_FILE_ENV,
257
+ "GJC_TMUX_SESSION",
258
+ ] as const;
259
+
260
+ /**
261
+ * Drop project-dotenv copies of the tmux session name, coordinator id, and
262
+ * state-file path. A pin is kept only when every snapshot accepts it, so a
263
+ * declaration Bun loaded before `chdir` is refused even if the launch cwd no
264
+ * longer contains that file. Acceptance uses the shared helper, but the
265
+ * original string stays: callers trim at use, and a later check must see the
266
+ * same value that was accepted.
267
+ */
268
+ export function trustedTmuxLaunchIdentityEnv(
269
+ env: NodeJS.ProcessEnv,
270
+ snapshots: readonly ProjectEnvSnapshot[],
271
+ ): NodeJS.ProcessEnv {
272
+ const denied: string[] = [];
273
+ for (const name of TMUX_LAUNCH_IDENTITY_ENV_NAMES) {
274
+ if (!env[name]) continue;
275
+ for (const snapshot of snapshots) {
276
+ if (trustedCoordinatorEnv(name, env, snapshot) === undefined) {
277
+ denied.push(name);
278
+ break;
279
+ }
280
+ }
281
+ }
282
+ if (denied.length === 0) return env;
283
+ const next: NodeJS.ProcessEnv = { ...env };
284
+ for (const name of denied) delete next[name];
285
+ return next;
286
+ }
287
+
288
+ export function tmuxLaunchIdentityEnv(env: NodeJS.ProcessEnv, cwd: string): NodeJS.ProcessEnv {
289
+ return trustedTmuxLaunchIdentityEnv(env, [projectEnvSnapshot(cwd), startupProjectEnvSnapshot()]);
290
+ }
291
+
292
+ /**
293
+ * Session id the launched child must see. A trusted pin is passed through.
294
+ * A rejected project pin is replaced by the same fallback the launch plan uses
295
+ * for the session root, which also stops Bun from reloading that dotenv value
296
+ * (`GJC_TMUX_LAUNCHED=1` skips the planner filter). An omitted id stays omitted.
297
+ */
298
+ export function resolvedChildGjcSessionId(input: {
299
+ trusted: string | undefined;
300
+ inherited: string | undefined;
301
+ projectDeclared: boolean;
302
+ fallback: string;
303
+ }): string | undefined {
304
+ const trusted = input.trusted?.trim();
305
+ if (trusted) return trusted;
306
+ if (!input.inherited?.trim() && !input.projectDeclared) return undefined;
307
+ const fallback = input.fallback.trim();
308
+ return fallback.length > 0 ? fallback : undefined;
309
+ }
310
+
311
+ export function childGjcSessionIdAssignment(
312
+ env: NodeJS.ProcessEnv,
313
+ identityEnv: NodeJS.ProcessEnv,
314
+ cwd: string,
315
+ fallbackSessionId: string,
316
+ ): Record<string, string> {
317
+ const snapshot = projectEnvSnapshot(cwd);
318
+ const key = canonicalEnvKey("GJC_SESSION_ID");
319
+ const value = resolvedChildGjcSessionId({
320
+ trusted: identityEnv.GJC_SESSION_ID,
321
+ inherited: env.GJC_SESSION_ID,
322
+ projectDeclared: snapshot.values[key] !== undefined || snapshot.dynamic.has(key),
323
+ fallback: fallbackSessionId,
324
+ });
325
+ return value === undefined ? {} : { GJC_SESSION_ID: value };
326
+ }
327
+
250
328
  function psmuxAuthorityFromEnv(env: NodeJS.ProcessEnv): ProviderAuthority | null {
251
329
  const stateDir = env[GJC_TMUX_OWNER_STATE_DIR_ENV]?.trim();
252
330
  const sessionId = env[GJC_COORDINATOR_SESSION_ID_ENV]?.trim();
@@ -515,11 +593,14 @@ function canonicalProviderStateDirs(cwd: string): string[] {
515
593
  }
516
594
 
517
595
  function psmuxAuthorityEnvironments(env: NodeJS.ProcessEnv): NodeJS.ProcessEnv[] {
518
- const explicitAuthority = psmuxAuthorityFromEnv(env);
596
+ const identityEnv = tmuxLaunchIdentityEnv(env, process.cwd());
597
+ const explicitAuthority = psmuxAuthorityFromEnv(identityEnv);
519
598
  if (explicitAuthority) return [environmentForProviderAuthority(env, explicitAuthority)];
520
599
  const explicitStateDir =
521
- env[GJC_TMUX_OWNER_STATE_DIR_ENV]?.trim() ??
522
- (env[GJC_COORDINATOR_SESSION_STATE_FILE_ENV] ? path.dirname(env[GJC_COORDINATOR_SESSION_STATE_FILE_ENV]) : "");
600
+ identityEnv[GJC_TMUX_OWNER_STATE_DIR_ENV]?.trim() ??
601
+ (identityEnv[GJC_COORDINATOR_SESSION_STATE_FILE_ENV]
602
+ ? path.dirname(identityEnv[GJC_COORDINATOR_SESSION_STATE_FILE_ENV])
603
+ : "");
523
604
  const ambient = resolveGjcTmuxBinary({ env });
524
605
  if (gjcTmuxAuthorityPlatform() === "win32") {
525
606
  const ambientAvailable = path.isAbsolute(ambient.command)
@@ -669,17 +750,15 @@ export function createGjcTmuxSession(
669
750
  if (launch && !validManagedLaunchSpec(launch, platform)) throw new Error("gjc_tmux_managed_launch_spec_invalid");
670
751
  const provider = resolveGjcTmuxProviderContext({ env, platform });
671
752
  const tmuxCommand = provider.command;
672
- const launchEnvironment = launch ? { ...env, GJC_TMUX_SESSION: launch.childSessionId } : env;
673
- const sessionName = buildGjcTmuxSessionName(launchEnvironment);
674
753
  const cwd = launch?.cwd ?? process.cwd();
675
- const sessionId = launch?.childSessionId ?? (env[GJC_COORDINATOR_SESSION_ID_ENV]?.trim() || sessionName);
754
+ const identityEnv = tmuxLaunchIdentityEnv(env, cwd);
755
+ const launchEnvironment = launch ? { ...identityEnv, GJC_TMUX_SESSION: launch.childSessionId } : identityEnv;
756
+ const sessionName = buildGjcTmuxSessionName(launchEnvironment);
757
+ const sessionId = launch?.childSessionId ?? (identityEnv[GJC_COORDINATOR_SESSION_ID_ENV]?.trim() || sessionName);
758
+ const filteredGjcSessionId = identityEnv.GJC_SESSION_ID?.trim() || sessionId;
676
759
  const stateFile =
677
- (!launch ? env[GJC_COORDINATOR_SESSION_STATE_FILE_ENV]?.trim() : undefined) ||
678
- tmuxRuntimeSessionPath(
679
- cwd,
680
- launch?.childSessionId ?? (env.GJC_SESSION_ID?.trim() || sessionId),
681
- buildGjcTmuxSessionSlug(sessionName),
682
- );
760
+ (!launch ? identityEnv[GJC_COORDINATOR_SESSION_STATE_FILE_ENV]?.trim() : undefined) ||
761
+ tmuxRuntimeSessionPath(cwd, launch?.childSessionId ?? filteredGjcSessionId, buildGjcTmuxSessionSlug(sessionName));
683
762
  const stateDir = (platform === "win32" ? path.win32 : path).dirname(stateFile);
684
763
  const generation = crypto.randomUUID();
685
764
  const authority = bindGjcTmuxProviderAuthority(provider, { stateDir, sessionId, generation });
@@ -690,6 +769,7 @@ export function createGjcTmuxSession(
690
769
  [GJC_TMUX_OWNER_SERVER_KEY_ENV]: tmuxCommand,
691
770
  [GJC_COORDINATOR_SESSION_ID_ENV]: sessionId,
692
771
  [GJC_COORDINATOR_SESSION_STATE_FILE_ENV]: stateFile,
772
+ ...childGjcSessionIdAssignment(env, identityEnv, cwd, filteredGjcSessionId),
693
773
  ...(launch
694
774
  ? {
695
775
  [MANAGED_OWNER_RUN_ID_ENV]: crypto.randomUUID(),
@@ -699,7 +779,7 @@ export function createGjcTmuxSession(
699
779
  : {}),
700
780
  };
701
781
  const childEnvironment: Record<string, string> = { ...(launch?.env ?? {}), ...managedEnvironment };
702
- const executionEnv = { ...env, ...managedEnvironment };
782
+ const executionEnv = { ...identityEnv, ...managedEnvironment };
703
783
  const shellQuote = (value: string): string => `'${value.replace(/'/g, `'\\''`)}'`;
704
784
  const command =
705
785
  platform === "win32"
@@ -128,15 +128,30 @@ async function hashAddedFiles(cwd: string, paths: readonly UltragoalChangeSetPat
128
128
 
129
129
  export async function spawnText(
130
130
  command: string[],
131
- options: { cwd: string; timeoutMs?: number },
131
+ options: { cwd: string; timeoutMs?: number; stdin?: string },
132
132
  ): Promise<{ ok: boolean; stdout: string; stderr: string }> {
133
133
  try {
134
- const proc = Bun.spawn(command, { cwd: options.cwd, stdout: "pipe", stderr: "pipe" });
134
+ const proc = Bun.spawn(command, { cwd: options.cwd, stdin: "pipe", stdout: "pipe", stderr: "pipe" });
135
135
  const timeout = setTimeout(() => proc.kill(), options.timeoutMs ?? 5000);
136
+ // Drain both pipes while the stdin write runs: a child that never reads
137
+ // stdin blocks the write, and a child that fills stdout before draining
138
+ // stdin blocks the read.
139
+ const stdinWrite = (async () => {
140
+ const stdin = proc.stdin;
141
+ if (!stdin || options.stdin === undefined) return;
142
+ try {
143
+ stdin.write(options.stdin);
144
+ await stdin.end();
145
+ } catch {
146
+ // The child closed stdin or the timer killed it; the exit code
147
+ // already reports the failure.
148
+ }
149
+ })();
136
150
  const [stdoutBytes, stderrBytes, exitCode] = await Promise.all([
137
151
  new Response(proc.stdout).arrayBuffer(),
138
152
  new Response(proc.stderr).arrayBuffer(),
139
153
  proc.exited,
154
+ stdinWrite,
140
155
  ]);
141
156
  clearTimeout(timeout);
142
157
  let stdout: string;
@@ -152,6 +167,37 @@ export async function spawnText(
152
167
  }
153
168
  }
154
169
 
170
+ /**
171
+ * Derive the empty tree object ID for the given repository, which depends on the
172
+ * repository's object format (SHA-1 vs SHA-256). A repository with no commits has
173
+ * no refs and no merge base, so the empty tree is the only authoritative baseline:
174
+ * every tracked, staged, and untracked file is an addition on top of it.
175
+ *
176
+ * @param cwd The repository root directory
177
+ * @returns The empty tree object ID (format-specific), or the SHA-1 ID if derivation fails
178
+ */
179
+ async function getEmptyTreeId(cwd: string): Promise<string> {
180
+ // Compute the empty tree object ID by hashing an empty tree.
181
+ // This respects the repository's object format (SHA-1 or SHA-256).
182
+ const result = await spawnText(["git", "hash-object", "-t", "tree", "--stdin"], {
183
+ cwd,
184
+ timeoutMs: 3000,
185
+ stdin: "", // Empty input for empty tree
186
+ });
187
+ if (result.ok && result.stdout.trim()) {
188
+ return result.stdout.trim();
189
+ }
190
+ // Fallback to SHA-1 empty tree ID if derivation fails
191
+ // (e.g., git command not available, permission denied, etc.)
192
+ return "4b825dc642cb6eb9a060e54bf8d69288fbee4904";
193
+ }
194
+
195
+ /**
196
+ * The canonical empty tree object for SHA-1 repositories. Use getEmptyTreeId()
197
+ * to derive the format-specific ID for a given repository.
198
+ */
199
+ export const GIT_EMPTY_TREE_SHA = "4b825dc642cb6eb9a060e54bf8d69288fbee4904";
200
+
155
201
  export async function resolveGitBase(cwd: string, branch?: string): Promise<string> {
156
202
  if (branch) {
157
203
  const exists = await spawnText(["git", "rev-parse", "--verify", branch], { cwd, timeoutMs: 3000 });
@@ -163,11 +209,31 @@ export async function resolveGitBase(cwd: string, branch?: string): Promise<stri
163
209
  // and mis-attributes other people's changes to this story (e.g. falsely
164
210
  // tripping change-scoped gates). Among existing candidates, pick the one
165
211
  // whose merge-base with HEAD is closest to HEAD (fewest commits ahead).
166
- const candidates = ["origin/dev", "dev", "origin/main", "origin/master", "main", "master"];
212
+ const candidates = [
213
+ "origin/dev",
214
+ "dev",
215
+ "origin/develop",
216
+ "develop",
217
+ "origin/main",
218
+ "origin/master",
219
+ "main",
220
+ "master",
221
+ ];
222
+ const currentBranch = await spawnText(["git", "symbolic-ref", "--quiet", "HEAD"], { cwd, timeoutMs: 3000 });
223
+ const currentRef = currentBranch.ok ? currentBranch.stdout.trim() : undefined;
167
224
  let best: { ref: string; ahead: number } | undefined;
225
+
168
226
  for (const candidate of candidates) {
169
- const exists = await spawnText(["git", "rev-parse", "--verify", candidate], { cwd, timeoutMs: 3000 });
227
+ const exists = await spawnText(["git", "rev-parse", "--symbolic-full-name", "--verify", candidate], {
228
+ cwd,
229
+ timeoutMs: 3000,
230
+ });
170
231
  if (!exists.ok) continue;
232
+ // Exclude only the checked-out local ref, not distinct refs at the same
233
+ // commit or remote-tracking refs with the same branch name. Comparing
234
+ // the checked-out ref to HEAD hides committed work as a complete empty
235
+ // capture; without an independent baseline, resolution must fail closed.
236
+ if (currentRef && exists.stdout.trim() === currentRef) continue;
171
237
  const mergeBase = await spawnText(["git", "merge-base", "HEAD", candidate], { cwd, timeoutMs: 3000 });
172
238
  if (!mergeBase.ok || !mergeBase.stdout.trim()) continue;
173
239
  const count = await spawnText(["git", "rev-list", "--count", `${mergeBase.stdout.trim()}..HEAD`], {
@@ -285,16 +351,26 @@ export async function computeCheckpointChangeSet(cwd: string): Promise<Ultragoal
285
351
  }
286
352
  const captureWitness = await repositoryStateWitness(cwd);
287
353
  await repositoryStateWitnessTestHook?.("after-initial", cwd);
288
- const baseRef = await resolveGitBase(cwd);
354
+ // A freshly initialized repository has no commits, so `HEAD`, every branch
355
+ // candidate, and any merge base are unresolvable. The empty tree is the only
356
+ // authoritative baseline there; refusing outright would strand a run whose
357
+ // implementation is entirely untracked. Everything on disk then counts as an
358
+ // addition, which is hashed like any other added file and never committed.
359
+ const headPresent = await spawnText(["git", "rev-parse", "--verify", "HEAD"], { cwd, timeoutMs: 3000 });
360
+ const unborn = !headPresent.ok;
361
+ const baseRef = unborn ? await getEmptyTreeId(cwd) : await resolveGitBase(cwd);
289
362
  const base = baseRef;
290
- const mergeBase = await spawnText(["git", "merge-base", "HEAD", baseRef], { cwd, timeoutMs: 3000 });
363
+ const mergeBase = unborn
364
+ ? { ok: false, stdout: "", stderr: "repository has no commits" }
365
+ : await spawnText(["git", "merge-base", "HEAD", baseRef], { cwd, timeoutMs: 3000 });
366
+ const diffRange = unborn ? baseRef : `${base}...HEAD`;
291
367
  const [committed, unstaged, staged, untracked, stat, committedDiff, unstagedDiff, stagedDiff] = await Promise.all([
292
- spawnText(["git", "diff", "--name-status", "-z", `${base}...HEAD`], { cwd, timeoutMs: 5000 }),
368
+ spawnText(["git", "diff", "--name-status", "-z", unborn ? baseRef : diffRange], { cwd, timeoutMs: 5000 }),
293
369
  spawnText(["git", "diff", "--name-status", "-z"], { cwd, timeoutMs: 5000 }),
294
370
  spawnText(["git", "diff", "--cached", "--name-status", "-z"], { cwd, timeoutMs: 5000 }),
295
371
  spawnText(["git", "ls-files", "--others", "--exclude-standard", "-z"], { cwd, timeoutMs: 5000 }),
296
- spawnText(["git", "diff", "--stat", `${base}...HEAD`], { cwd, timeoutMs: 5000 }),
297
- spawnText(["git", "diff", `${base}...HEAD`], { cwd, timeoutMs: 5000 }),
372
+ spawnText(["git", "diff", "--stat", diffRange], { cwd, timeoutMs: 5000 }),
373
+ spawnText(["git", "diff", diffRange], { cwd, timeoutMs: 5000 }),
298
374
  spawnText(["git", "diff"], { cwd, timeoutMs: 5000 }),
299
375
  spawnText(["git", "diff", "--cached"], { cwd, timeoutMs: 5000 }),
300
376
  ]);
@@ -382,7 +458,10 @@ async function repositoryStateWitness(cwd: string): Promise<string | undefined>
382
458
  spawnText(["git", "ls-files", "-z"], { cwd, timeoutMs: 5000 }),
383
459
  spawnText(["git", "ls-files", "--others", "--exclude-standard", "-z"], { cwd, timeoutMs: 5000 }),
384
460
  ]);
385
- if (!head.ok || !status.ok || !tracked.ok || !untracked.ok) return undefined;
461
+ // An unborn repository has no HEAD yet; the remaining reads still describe its
462
+ // full state, so witness it as an empty head rather than failing the capture.
463
+ if (!status.ok || !tracked.ok || !untracked.ok) return undefined;
464
+ const headToken = head.ok ? head.stdout.trim() : "unborn";
386
465
  const paths = [
387
466
  ...new Set([
388
467
  ...parseGitUntrackedPaths(tracked.stdout).map(row => row.path),
@@ -417,7 +496,7 @@ async function repositoryStateWitness(cwd: string): Promise<string | undefined>
417
496
  } catch {
418
497
  return undefined;
419
498
  }
420
- return `${head.stdout.trim()}\u0000${status.stdout}\u0000sha256:${content.digest("hex")}`;
499
+ return `${headToken}\u0000${status.stdout}\u0000sha256:${content.digest("hex")}`;
421
500
  }
422
501
 
423
502
  export function parseUnifiedDiffPaths(diff: string): UltragoalChangeSetPath[] {
@@ -25,6 +25,7 @@ import {
25
25
  type UltragoalPlan,
26
26
  type UltragoalReceiptKind,
27
27
  } from "./ultragoal-runtime";
28
+ import { readUltragoalSuccessionFence } from "./ultragoal-succession";
28
29
 
29
30
  export type UltragoalGuardState =
30
31
  | "inactive"
@@ -823,7 +824,14 @@ async function consumeUltragoalNudge(input: {
823
824
  if (!sessionId) return { nudged: false };
824
825
  const plan = await readUltragoalPlan(input.cwd, sessionId);
825
826
  if (!plan) return { nudged: false };
826
- const target = selectUltragoalNudgeTarget(plan, { currentGoalObjective: input.currentGoal?.objective });
827
+ const fence = await readUltragoalSuccessionFence(input.cwd, sessionId);
828
+ // A goal handed off to a successor run is not this run's resolvable work. Skip
829
+ // fenced goals while selecting, so a retained source-owned goal can still receive
830
+ // the nudge (and keep aggregate drop guarded) without spending budget on the handoff.
831
+ const target = selectUltragoalNudgeTarget(plan, {
832
+ currentGoalObjective: input.currentGoal?.objective,
833
+ excludedGoalIds: fence?.selectedGoalIds,
834
+ });
827
835
  if (!target) return { nudged: false };
828
836
  const { budget } = await resolveUltragoalNudgeBudget(input.cwd, input.agentDir);
829
837
  const outcome = await recordUltragoalNudgeIfBudgetRemaining({
@@ -82,6 +82,7 @@ import {
82
82
  import {
83
83
  assertUltragoalAdoptionPublished,
84
84
  assertUltragoalGoalNotFenced,
85
+ readUltragoalSuccessionFence,
85
86
  runUltragoalSuccessionCommand,
86
87
  } from "./ultragoal-succession";
87
88
  import {
@@ -472,22 +473,27 @@ export async function resolveUltragoalNudgeBudget(
472
473
  */
473
474
  export function selectUltragoalNudgeTarget(
474
475
  plan: UltragoalPlan,
475
- options: { currentGoalObjective?: string; retryFailed?: boolean } = {},
476
+ options: { currentGoalObjective?: string; retryFailed?: boolean; excludedGoalIds?: readonly string[] } = {},
476
477
  ): UltragoalNudgeTarget | null {
477
478
  const objective = options.currentGoalObjective?.trim();
479
+ const isExcluded = (goalId: string): boolean => options.excludedGoalIds?.includes(goalId) ?? false;
478
480
  if (objective) {
479
481
  const matched = plan.goals.find(
480
- goal => goal.objective.trim() === objective && SCHEDULABLE_STATUSES.has(goal.status),
482
+ goal => goal.objective.trim() === objective && SCHEDULABLE_STATUSES.has(goal.status) && !isExcluded(goal.id),
481
483
  );
482
484
  if (matched) return { goalId: matched.id, targetKind: "story" };
483
485
  }
484
- const next = chooseNextGoal(plan, options.retryFailed === true);
486
+ const schedulableGoals = options.excludedGoalIds?.length
487
+ ? plan.goals.filter(goal => !isExcluded(goal.id))
488
+ : plan.goals;
489
+ const next = selectNextSchedulableGoal(schedulableGoals, options.retryFailed === true);
485
490
  if (next) return { goalId: next.id, targetKind: "story" };
486
491
  const completion = getUltragoalRunCompletionState(plan, { retryFailed: options.retryFailed });
487
492
  if (completion.needsFinalAggregateReceipt) {
488
493
  const required = requiredUltragoalGoals(plan);
489
494
  const finalGoal = required.at(-1);
490
- if (finalGoal) return { goalId: finalGoal.id, targetKind: "final_aggregate_receipt" };
495
+ if (finalGoal && !isExcluded(finalGoal.id))
496
+ return { goalId: finalGoal.id, targetKind: "final_aggregate_receipt" };
491
497
  }
492
498
  return null;
493
499
  }
@@ -1125,7 +1131,11 @@ export async function getUltragoalStatus(
1125
1131
  else if (counts.active > 0) status = "active";
1126
1132
  else if (counts.failed > 0) status = "failed";
1127
1133
  else if (counts.blocked > 0 || counts.review_blocked > 0) status = "blocked";
1128
- const nudgeTarget = selectUltragoalNudgeTarget(plan, { currentGoalObjective: currentGoal?.objective });
1134
+ const fence = await readUltragoalSuccessionFence(cwd, resolvedSessionId);
1135
+ const nudgeTarget = selectUltragoalNudgeTarget(plan, {
1136
+ currentGoalObjective: currentGoal?.objective,
1137
+ excludedGoalIds: fence?.selectedGoalIds,
1138
+ });
1129
1139
  let nudgeFields: Partial<UltragoalStatusSummary> = {};
1130
1140
  if (nudgeTarget) {
1131
1141
  const { budget } = await resolveUltragoalNudgeBudget(cwd, agentDir);
@@ -14,6 +14,7 @@ import { randomBytes } from "node:crypto";
14
14
  import { readdirSync, readFileSync } from "node:fs";
15
15
  import { readFile } from "node:fs/promises";
16
16
  import { type LinuxProcPidProbeResult, probeLinuxProcPidSync } from "../gjc-runtime/linux-proc";
17
+ import { checkTmuxSelfInjection } from "../tools/tmux-self-injection-guard";
17
18
  import {
18
19
  buildReceipt,
19
20
  type CompletionEvidence,
@@ -558,6 +559,16 @@ export function defaultFinalizeChecks(
558
559
  return {
559
560
  async runValidation(spec, signal) {
560
561
  if (signal?.aborted) throw new ValidationObservationUncertainError(spec.command, workspace, signal.reason);
562
+ // Same preflight as BashTool: refuse before spawn so a plan command cannot type into this pane.
563
+ const injection = await checkTmuxSelfInjection(spec.command, {
564
+ env: process.env,
565
+ cwd: workspace,
566
+ executionEnv: process.env,
567
+ });
568
+ if (signal?.aborted) throw new ValidationObservationUncertainError(spec.command, workspace, signal.reason);
569
+ if (injection.block) {
570
+ return { exactCommand: spec.command, cwd: workspace, exitStatus: 1, pass: false };
571
+ }
561
572
  let proc: Bun.Subprocess<"ignore", "pipe", "pipe">;
562
573
  try {
563
574
  proc = Bun.spawn(["bash", "-lc", spec.command], {