@gajae-code/coding-agent 0.18.11 → 0.19.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (462) hide show
  1. package/CHANGELOG.md +760 -0
  2. package/dist/types/async/job-manager.d.ts +7 -3
  3. package/dist/types/async/result-formatting.d.ts +2 -0
  4. package/dist/types/autoresearch/git.d.ts +1 -1
  5. package/dist/types/capability/fs.d.ts +2 -2
  6. package/dist/types/capability/rule.d.ts +7 -2
  7. package/dist/types/cli/accounts-cli.d.ts +2 -2
  8. package/dist/types/cli/append-system-prompt.d.ts +6 -0
  9. package/dist/types/cli/args.d.ts +7 -2
  10. package/dist/types/cli/customize-doctor-precedence.d.ts +16 -0
  11. package/dist/types/cli/customize-doctor.d.ts +4 -3
  12. package/dist/types/cli/doctor/install-repairs.d.ts +4 -1
  13. package/dist/types/cli/install-activation.d.ts +10 -4
  14. package/dist/types/cli/public-command-errors.d.ts +2 -0
  15. package/dist/types/cli/root-flags.d.ts +0 -12
  16. package/dist/types/cli/update-cli.d.ts +21 -3
  17. package/dist/types/cli-main.d.ts +0 -6
  18. package/dist/types/cli-ordinary.d.ts +2 -2
  19. package/dist/types/commands/accounts.d.ts +2 -1
  20. package/dist/types/commands/launch.d.ts +0 -6
  21. package/dist/types/commands/migrate.d.ts +1 -1
  22. package/dist/types/commands/sdk.d.ts +13 -0
  23. package/dist/types/commit/changelog/index.d.ts +1 -0
  24. package/dist/types/commit/map-reduce/index.d.ts +4 -4
  25. package/dist/types/config/autorouting-tier-map.d.ts +53 -1
  26. package/dist/types/config/config-hot-reload-trust.d.ts +12 -0
  27. package/dist/types/config/config-hot-reload.d.ts +44 -0
  28. package/dist/types/config/file-lock.d.ts +52 -3
  29. package/dist/types/config/model-discovery-manager.d.ts +2 -0
  30. package/dist/types/config/model-profile-activation.d.ts +79 -9
  31. package/dist/types/config/model-registry.d.ts +72 -8
  32. package/dist/types/config/model-resolver.d.ts +7 -4
  33. package/dist/types/config/models-config-schema.d.ts +10 -0
  34. package/dist/types/config/settings-schema.d.ts +57 -4
  35. package/dist/types/config/settings.d.ts +33 -1
  36. package/dist/types/coordinator-mcp/server.d.ts +4 -0
  37. package/dist/types/customization/import.d.ts +2 -0
  38. package/dist/types/customization/types.d.ts +1 -1
  39. package/dist/types/dap/config.d.ts +8 -4
  40. package/dist/types/dap/session.d.ts +11 -2
  41. package/dist/types/debug/system-info.d.ts +6 -1
  42. package/dist/types/discovery/helpers.d.ts +7 -2
  43. package/dist/types/discovery/mcp-compat.d.ts +3 -1
  44. package/dist/types/edit/index.d.ts +5 -0
  45. package/dist/types/edit/modes/patch.d.ts +14 -0
  46. package/dist/types/edit/modes/replace.d.ts +3 -0
  47. package/dist/types/eval/py/runtime.d.ts +9 -0
  48. package/dist/types/exec/bash-executor.d.ts +12 -0
  49. package/dist/types/export/ttsr.d.ts +19 -0
  50. package/dist/types/extensibility/extensions/types.d.ts +5 -1
  51. package/dist/types/extensibility/slash-commands.d.ts +1 -0
  52. package/dist/types/gjc-runtime/managed-owner-admission.d.ts +3 -0
  53. package/dist/types/gjc-runtime/ralplan-current-final.d.ts +54 -0
  54. package/dist/types/gjc-runtime/ralplan-runtime.d.ts +5 -0
  55. package/dist/types/gjc-runtime/session-state-lock.d.ts +2 -0
  56. package/dist/types/gjc-runtime/state-runtime.d.ts +0 -2
  57. package/dist/types/gjc-runtime/tmux-sessions.d.ts +24 -0
  58. package/dist/types/gjc-runtime/ultragoal-change-set.d.ts +6 -0
  59. package/dist/types/gjc-runtime/ultragoal-runtime.d.ts +1 -0
  60. package/dist/types/harness-control-plane/receipt-spool.d.ts +4 -0
  61. package/dist/types/hindsight/content.d.ts +5 -6
  62. package/dist/types/internal-urls/types.d.ts +2 -0
  63. package/dist/types/lsp/clients/swiftlint-client.d.ts +0 -4
  64. package/dist/types/main.d.ts +23 -18
  65. package/dist/types/migrate/action-planner.d.ts +10 -1
  66. package/dist/types/migrate/adapters/codex.d.ts +2 -1
  67. package/dist/types/migrate/adapters/index.d.ts +42 -1
  68. package/dist/types/migrate/executor.d.ts +3 -1
  69. package/dist/types/migrate/hook-module.d.ts +23 -0
  70. package/dist/types/migrate/report.d.ts +2 -1
  71. package/dist/types/migrate/types.d.ts +82 -3
  72. package/dist/types/modes/components/agent-dashboard.d.ts +1 -1
  73. package/dist/types/modes/components/model-selector.d.ts +43 -0
  74. package/dist/types/modes/components/settings-selector.d.ts +6 -0
  75. package/dist/types/modes/controllers/selector-controller.d.ts +2 -0
  76. package/dist/types/modes/prompt-suggestion-controller.d.ts +1 -1
  77. package/dist/types/modes/theme/theme.d.ts +1 -0
  78. package/dist/types/modes/utils/host-status.d.ts +15 -11
  79. package/dist/types/runtime-mcp/content-limits.d.ts +4 -0
  80. package/dist/types/runtime-mcp/manager.d.ts +7 -0
  81. package/dist/types/runtime-mcp/plugin-mcp-name-filter.d.ts +22 -0
  82. package/dist/types/runtime-mcp/project-host-values.d.ts +11 -5
  83. package/dist/types/runtime-mcp/tool-bridge.d.ts +4 -0
  84. package/dist/types/sdk/broker/broker-exit.d.ts +3 -3
  85. package/dist/types/sdk/broker/broker.d.ts +29 -2
  86. package/dist/types/sdk/broker/daemon-entry.d.ts +11 -1
  87. package/dist/types/sdk/broker/doctor-restart.d.ts +1 -1
  88. package/dist/types/sdk/broker/ensure.d.ts +141 -9
  89. package/dist/types/sdk/broker/hop.d.ts +45 -0
  90. package/dist/types/sdk/broker/lifecycle-ledger.d.ts +18 -10
  91. package/dist/types/sdk/broker/lifecycle.d.ts +22 -0
  92. package/dist/types/sdk/broker/lock-host-identity.d.ts +5 -0
  93. package/dist/types/sdk/broker/session-index.d.ts +6 -1
  94. package/dist/types/sdk/broker/transport.d.ts +2 -0
  95. package/dist/types/sdk/bus/chat-command-policy.d.ts +2 -1
  96. package/dist/types/sdk/bus/index.d.ts +44 -0
  97. package/dist/types/sdk/bus/reconciliation-store.d.ts +10 -0
  98. package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +9 -1
  99. package/dist/types/sdk/bus/telegram-daemon.d.ts +2 -0
  100. package/dist/types/sdk/bus/threaded-render.d.ts +2 -1
  101. package/dist/types/sdk/cli/session-cli.d.ts +0 -1
  102. package/dist/types/sdk/host/control/operations.d.ts +5 -1
  103. package/dist/types/sdk/host/events.d.ts +4 -0
  104. package/dist/types/sdk/host/host.d.ts +2 -1
  105. package/dist/types/sdk/host/prompt-image-upload.d.ts +43 -0
  106. package/dist/types/sdk/host/session-runtime.d.ts +40 -8
  107. package/dist/types/sdk/host/surface-policy.d.ts +2 -1
  108. package/dist/types/sdk/lifecycle/service.d.ts +12 -0
  109. package/dist/types/sdk/prompt-deadline-flush.d.ts +42 -4
  110. package/dist/types/sdk/prompt-deadline-manager.d.ts +2 -0
  111. package/dist/types/sdk/prompt-status.d.ts +11 -4
  112. package/dist/types/sdk/protocol/adapter-validation.d.ts +10 -4
  113. package/dist/types/sdk/reconciliation-extensions.d.ts +1 -1
  114. package/dist/types/sdk/session-list.d.ts +1 -1
  115. package/dist/types/session/agent-session.d.ts +107 -9
  116. package/dist/types/session/internal/managed-session-scope.d.ts +2 -2
  117. package/dist/types/session/internal/managed-session-storage.d.ts +1 -1
  118. package/dist/types/session/internal/native-publish-outcome.d.ts +1 -1
  119. package/dist/types/session/messages.d.ts +5 -1
  120. package/dist/types/session/session-manager.d.ts +29 -3
  121. package/dist/types/session/startup-auth-config.d.ts +24 -2
  122. package/dist/types/session/streaming-output.d.ts +11 -0
  123. package/dist/types/session/terminal-abort.d.ts +3 -1
  124. package/dist/types/session-import/service.d.ts +1 -1
  125. package/dist/types/skill-state/workflow-mutation-guard.d.ts +20 -4
  126. package/dist/types/slash-commands/helpers/mcp.d.ts +5 -0
  127. package/dist/types/slash-commands/helpers/provider-auth-status.d.ts +8 -0
  128. package/dist/types/ssh/ssh-executor.d.ts +8 -1
  129. package/dist/types/system-prompt.d.ts +3 -2
  130. package/dist/types/task/executor.d.ts +19 -2
  131. package/dist/types/task/role-body.d.ts +11 -0
  132. package/dist/types/tools/ast-edit-write-path.d.ts +10 -0
  133. package/dist/types/tools/atomic-file-write.d.ts +9 -7
  134. package/dist/types/tools/awaiting-input.d.ts +28 -0
  135. package/dist/types/tools/bash-allowed-prefixes.d.ts +17 -0
  136. package/dist/types/tools/bash-skill-urls.d.ts +4 -0
  137. package/dist/types/tools/bash.d.ts +7 -0
  138. package/dist/types/tools/descriptors.d.ts +15 -2
  139. package/dist/types/tools/fetch.d.ts +7 -1
  140. package/dist/types/tools/index.d.ts +2 -0
  141. package/dist/types/tools/irc.d.ts +5 -3
  142. package/dist/types/tools/python.d.ts +1 -1
  143. package/dist/types/tools/report-issue.d.ts +41 -0
  144. package/dist/types/tools/subagent.d.ts +23 -0
  145. package/dist/types/tools/tmux-self-injection-guard.d.ts +2 -0
  146. package/dist/types/tools/ultragoal-ask-guard.d.ts +1 -1
  147. package/dist/types/tools/write.d.ts +2 -0
  148. package/dist/types/utils/file-mentions.d.ts +5 -0
  149. package/dist/types/utils/image-limits.d.ts +7 -0
  150. package/dist/types/utils/image-loading.d.ts +0 -1
  151. package/dist/types/utils/open.d.ts +0 -1
  152. package/dist/types/utils/pasted-image-loading.d.ts +1 -5
  153. package/dist/types/utils/prompt-suggestion.d.ts +2 -2
  154. package/dist/types/utils/shell-snapshot.d.ts +2 -0
  155. package/dist/types/utils/title-generator.d.ts +13 -3
  156. package/dist/types/web/scrapers/types.d.ts +8 -1
  157. package/dist/types/web/search/providers/codex.d.ts +6 -7
  158. package/dist/types/web/search/providers/insane.d.ts +7 -1
  159. package/dist/types/web/search/providers/searxng.d.ts +5 -0
  160. package/package.json +10 -9
  161. package/scripts/acp-conformance-agent.ts +12 -2
  162. package/scripts/build-binary.ts +2 -1
  163. package/scripts/capture-subagent-compact-panel-showcase.ts +105 -0
  164. package/scripts/generate-docs-index.ts +8 -1
  165. package/scripts/generate-sdk-operation-inventory.ts +38 -4
  166. package/src/async/job-manager.ts +356 -56
  167. package/src/async/result-formatting.ts +40 -0
  168. package/src/autoresearch/git.ts +9 -2
  169. package/src/autoresearch/prompts/prompt-setup.md +1 -1
  170. package/src/capability/fs.ts +58 -22
  171. package/src/capability/index.ts +2 -2
  172. package/src/capability/rule.ts +11 -2
  173. package/src/cli/accounts-cli.ts +176 -8
  174. package/src/cli/append-system-prompt.ts +12 -0
  175. package/src/cli/args.ts +11 -7
  176. package/src/cli/auth-broker-cli.ts +21 -11
  177. package/src/cli/customize-doctor-precedence.ts +31 -0
  178. package/src/cli/customize-doctor.ts +49 -39
  179. package/src/cli/doctor/install-repairs.ts +35 -5
  180. package/src/cli/fast-help.ts +1 -3
  181. package/src/cli/install-activation.ts +47 -4
  182. package/src/cli/migrate-cli.ts +19 -6
  183. package/src/cli/public-command-errors.ts +6 -0
  184. package/src/cli/root-flags.ts +2 -4
  185. package/src/cli/stats-cli.ts +9 -5
  186. package/src/cli/update-cli.ts +471 -13
  187. package/src/cli-ordinary.ts +7 -2
  188. package/src/commands/accounts.ts +10 -5
  189. package/src/commands/harness.ts +25 -15
  190. package/src/commands/migrate.ts +3 -3
  191. package/src/commands/sdk.ts +277 -50
  192. package/src/commands/ultragoal.ts +16 -8
  193. package/src/commit/agentic/agent.ts +6 -6
  194. package/src/commit/analysis/conventional.ts +14 -5
  195. package/src/commit/changelog/index.ts +39 -1
  196. package/src/commit/map-reduce/index.ts +7 -7
  197. package/src/commit/message.ts +10 -3
  198. package/src/commit/pipeline.ts +12 -12
  199. package/src/config/autorouting-tier-map.ts +50 -1
  200. package/src/config/config-file.ts +106 -3
  201. package/src/config/config-hot-reload-trust.ts +148 -0
  202. package/src/config/config-hot-reload.ts +742 -0
  203. package/src/config/file-lock.ts +1615 -328
  204. package/src/config/keybindings.ts +154 -7
  205. package/src/config/model-bindings-applier.ts +85 -47
  206. package/src/config/model-discovery-manager.ts +10 -0
  207. package/src/config/model-profile-activation.ts +889 -148
  208. package/src/config/model-registry.ts +1706 -205
  209. package/src/config/model-resolver.ts +52 -22
  210. package/src/config/models-config-schema.ts +6 -0
  211. package/src/config/settings-schema.ts +73 -4
  212. package/src/config/settings.ts +460 -24
  213. package/src/coordinator-mcp/policy.ts +26 -1
  214. package/src/coordinator-mcp/server.ts +37 -2
  215. package/src/crash/sanitize.ts +7 -1
  216. package/src/customization/import.ts +279 -8
  217. package/src/customization/inventory.ts +24 -8
  218. package/src/customization/types.ts +1 -0
  219. package/src/dap/client.ts +3 -2
  220. package/src/dap/config.ts +31 -11
  221. package/src/dap/defaults.json +1 -1
  222. package/src/dap/session.ts +161 -36
  223. package/src/debug/crash-diagnostics.ts +89 -6
  224. package/src/debug/report-bundle.ts +90 -2
  225. package/src/debug/system-info.ts +8 -3
  226. package/src/defaults/gjc/extensions/grok-cli-vendor/src/shared/base-url.ts +21 -7
  227. package/src/discovery/agents-md.ts +1 -1
  228. package/src/discovery/builtin.ts +4 -3
  229. package/src/discovery/helpers.ts +25 -16
  230. package/src/discovery/mcp-compat.ts +120 -17
  231. package/src/edit/apply-patch/index.ts +12 -1
  232. package/src/edit/index.ts +204 -22
  233. package/src/edit/modes/patch.ts +214 -8
  234. package/src/edit/modes/replace.ts +136 -0
  235. package/src/eval/js/context-manager.ts +29 -3
  236. package/src/eval/js/worker-entry.ts +51 -18
  237. package/src/eval/py/executor.ts +14 -4
  238. package/src/eval/py/runner.py +144 -5
  239. package/src/eval/py/runtime.ts +44 -4
  240. package/src/exec/bash-executor.ts +81 -4
  241. package/src/exec/bash-shell-guardian.ts +4 -1
  242. package/src/exec/bash-shell-supervisor.ts +8 -4
  243. package/src/exec/isolated-shell.ts +5 -1
  244. package/src/exec/non-interactive-env.ts +0 -1
  245. package/src/export/html/index.ts +86 -6
  246. package/src/export/ttsr.ts +176 -1
  247. package/src/extensibility/custom-tools/wrapper.ts +3 -1
  248. package/src/extensibility/extensions/loader.ts +10 -1
  249. package/src/extensibility/extensions/ouroboros-ooo-bridge.ts +63 -1
  250. package/src/extensibility/extensions/types.ts +5 -1
  251. package/src/extensibility/hooks/loader.ts +10 -2
  252. package/src/extensibility/plugins/marketplace/fetcher.ts +2 -2
  253. package/src/extensibility/plugins/marketplace/manager.ts +36 -10
  254. package/src/extensibility/slash-commands.ts +5 -0
  255. package/src/gjc-runtime/gc-runtime.ts +37 -7
  256. package/src/gjc-runtime/launch-tmux.ts +30 -15
  257. package/src/gjc-runtime/launch-worktree.test.ts +148 -0
  258. package/src/gjc-runtime/launch-worktree.ts +101 -14
  259. package/src/gjc-runtime/managed-owner-admission.ts +46 -2
  260. package/src/gjc-runtime/ralplan-current-final.ts +294 -0
  261. package/src/gjc-runtime/ralplan-runtime.ts +95 -1
  262. package/src/gjc-runtime/session-resolution.ts +38 -6
  263. package/src/gjc-runtime/session-state-lock.ts +59 -14
  264. package/src/gjc-runtime/state-runtime.ts +23 -19
  265. package/src/gjc-runtime/state-writer.ts +109 -5
  266. package/src/gjc-runtime/tmux-sessions.ts +93 -13
  267. package/src/gjc-runtime/ultragoal-change-set.ts +90 -11
  268. package/src/gjc-runtime/ultragoal-guard.ts +9 -1
  269. package/src/gjc-runtime/ultragoal-runtime.ts +15 -5
  270. package/src/harness-control-plane/finalize.ts +11 -0
  271. package/src/harness-control-plane/receipt-spool.ts +105 -28
  272. package/src/harness-control-plane/storage.ts +124 -6
  273. package/src/hindsight/content.ts +116 -16
  274. package/src/hindsight/mental-models.ts +22 -10
  275. package/src/hindsight/state.ts +7 -4
  276. package/src/hooks/native-skill-hook.ts +17 -12
  277. package/src/internal-urls/artifact-protocol.ts +3 -1
  278. package/src/internal-urls/docs-index.generated.ts +23 -22
  279. package/src/internal-urls/local-protocol.ts +167 -8
  280. package/src/internal-urls/types.ts +2 -0
  281. package/src/lsp/client.ts +2 -1
  282. package/src/lsp/clients/biome-client.ts +2 -0
  283. package/src/lsp/clients/swiftlint-client.ts +2 -0
  284. package/src/lsp/index.ts +29 -0
  285. package/src/lsp/workspace-path.ts +23 -0
  286. package/src/main.ts +330 -92
  287. package/src/memories/index.ts +25 -2
  288. package/src/migrate/action-planner.ts +574 -2
  289. package/src/migrate/adapters/claude-code.ts +162 -5
  290. package/src/migrate/adapters/codex.ts +77 -7
  291. package/src/migrate/adapters/index.ts +258 -5
  292. package/src/migrate/adapters/opencode.ts +10 -1
  293. package/src/migrate/executor.ts +222 -7
  294. package/src/migrate/hook-module.ts +193 -0
  295. package/src/migrate/report.ts +63 -2
  296. package/src/migrate/types.ts +84 -4
  297. package/src/modes/DESIGN.md +51 -0
  298. package/src/modes/acp/acp-agent.ts +10 -1
  299. package/src/modes/acp/permission-mode.ts +18 -1
  300. package/src/modes/components/agent-dashboard.ts +20 -4
  301. package/src/modes/components/assistant-message.ts +55 -1
  302. package/src/modes/components/customization/customization-dashboard.ts +7 -0
  303. package/src/modes/components/customization/import-wizard.ts +29 -3
  304. package/src/modes/components/model-selector.ts +157 -103
  305. package/src/modes/components/settings-selector.ts +103 -37
  306. package/src/modes/components/welcome.ts +3 -1
  307. package/src/modes/controllers/command-controller.ts +300 -9
  308. package/src/modes/controllers/event-controller.ts +1 -1
  309. package/src/modes/controllers/extension-ui-controller.ts +31 -1
  310. package/src/modes/controllers/goal-mode-controller.ts +79 -10
  311. package/src/modes/controllers/input-controller.ts +2 -0
  312. package/src/modes/controllers/plan-mode-controller.ts +66 -11
  313. package/src/modes/controllers/runtime-mcp-command-controller.ts +7 -6
  314. package/src/modes/controllers/selector-controller.ts +76 -7
  315. package/src/modes/interactive-mode.ts +123 -4
  316. package/src/modes/prompt-suggestion-controller.ts +1 -1
  317. package/src/modes/runtime-init.ts +5 -1
  318. package/src/modes/theme/theme.ts +15 -0
  319. package/src/modes/utils/host-status.ts +21 -10
  320. package/src/prompts/agent-fragments/browser-aside.md +2 -2
  321. package/src/prompts/system/irc-incoming.md +3 -1
  322. package/src/prompts/tools/debug.md +1 -1
  323. package/src/prompts/tools/irc.md +1 -1
  324. package/src/prompts/tools/read.md +1 -1
  325. package/src/prompts/tools/report-issue.md +13 -0
  326. package/src/prompts/tools/task.md +1 -1
  327. package/src/runtime-credential-selector.ts +2 -2
  328. package/src/runtime-mcp/content-limits.ts +7 -0
  329. package/src/runtime-mcp/manager-pool.test.ts +5 -4
  330. package/src/runtime-mcp/manager.ts +40 -3
  331. package/src/runtime-mcp/oauth-flow.ts +32 -6
  332. package/src/runtime-mcp/plugin-mcp-name-filter.ts +62 -0
  333. package/src/runtime-mcp/project-host-values.ts +12 -8
  334. package/src/runtime-mcp/render.ts +16 -1
  335. package/src/runtime-mcp/tool-bridge.ts +194 -21
  336. package/src/runtime-mcp/transports/stdio.ts +2 -1
  337. package/src/sdk/broker/broker-exit.ts +10 -5
  338. package/src/sdk/broker/broker.ts +484 -85
  339. package/src/sdk/broker/daemon-entry.ts +338 -116
  340. package/src/sdk/broker/doctor-restart.ts +2 -1
  341. package/src/sdk/broker/ensure.ts +1214 -268
  342. package/src/sdk/broker/hop.ts +286 -0
  343. package/src/sdk/broker/lifecycle-ledger.ts +38 -11
  344. package/src/sdk/broker/lifecycle.ts +102 -6
  345. package/src/sdk/broker/lock-host-identity.ts +29 -0
  346. package/src/sdk/broker/session-index.ts +91 -16
  347. package/src/sdk/broker/transport.ts +17 -1
  348. package/src/sdk/bus/chat-command-policy.ts +16 -3
  349. package/src/sdk/bus/chat-daemon-control.ts +2 -2
  350. package/src/sdk/bus/index.ts +460 -35
  351. package/src/sdk/bus/reconciliation-store.ts +201 -8
  352. package/src/sdk/bus/session-control.ts +2 -0
  353. package/src/sdk/bus/slack-daemon.ts +105 -9
  354. package/src/sdk/bus/telegram-daemon-contract.ts +9 -1
  355. package/src/sdk/bus/telegram-daemon.ts +10 -2
  356. package/src/sdk/bus/threaded-render.ts +20 -1
  357. package/src/sdk/cli/session-cli.ts +23 -6
  358. package/src/sdk/host/control/dispatch.ts +13 -1
  359. package/src/sdk/host/control/operations.ts +10 -1
  360. package/src/sdk/host/events.ts +67 -4
  361. package/src/sdk/host/host.ts +8 -2
  362. package/src/sdk/host/prompt-image-upload.ts +438 -0
  363. package/src/sdk/host/sdk-surface-parity.test.ts +15 -0
  364. package/src/sdk/host/session-runtime.test.ts +1737 -129
  365. package/src/sdk/host/session-runtime.ts +684 -83
  366. package/src/sdk/host/surface-policy.ts +4 -0
  367. package/src/sdk/host/websocket-transport.lifecycle.test.ts +90 -2
  368. package/src/sdk/host/websocket-transport.ts +19 -7
  369. package/src/sdk/lifecycle/service.ts +15 -1
  370. package/src/sdk/prompt-deadline-flush.ts +78 -5
  371. package/src/sdk/prompt-deadline-manager.ts +68 -14
  372. package/src/sdk/prompt-status.ts +6 -4
  373. package/src/sdk/protocol/adapter-validation.ts +81 -12
  374. package/src/sdk/protocol/operation-inventory.generated.json +261 -2
  375. package/src/sdk/protocol/operation-registry.ts +33 -3
  376. package/src/sdk/reconciliation-extensions.ts +1 -0
  377. package/src/sdk/session-list.ts +1 -0
  378. package/src/sdk/session.ts +237 -97
  379. package/src/session/agent-session.ts +2726 -479
  380. package/src/session/history-storage.ts +182 -12
  381. package/src/session/internal/managed-session-scope.ts +173 -57
  382. package/src/session/internal/managed-session-storage.ts +85 -12
  383. package/src/session/internal/native-publish-outcome.ts +5 -1
  384. package/src/session/messages.ts +47 -6
  385. package/src/session/session-manager.ts +326 -19
  386. package/src/session/startup-auth-config.ts +116 -8
  387. package/src/session/streaming-output.ts +262 -4
  388. package/src/session/terminal-abort.ts +14 -2
  389. package/src/session-import/redact.ts +53 -2
  390. package/src/session-import/service.ts +1 -0
  391. package/src/skill-state/workflow-mutation-guard.ts +2145 -206
  392. package/src/slash-commands/builtin-registry.ts +21 -6
  393. package/src/slash-commands/helpers/credential-switch.ts +10 -5
  394. package/src/slash-commands/helpers/mcp.ts +11 -3
  395. package/src/slash-commands/helpers/provider-auth-status.ts +456 -0
  396. package/src/ssh/ssh-executor.ts +9 -2
  397. package/src/stt/recorder.ts +26 -9
  398. package/src/system-prompt.ts +61 -16
  399. package/src/task/executor.ts +115 -20
  400. package/src/task/index.ts +389 -175
  401. package/src/task/role-body.ts +84 -0
  402. package/src/task/token-log-mode.test.ts +162 -0
  403. package/src/task/token-log.ts +91 -2
  404. package/src/tools/archive-reader.ts +650 -37
  405. package/src/tools/ask.ts +1 -0
  406. package/src/tools/ast-edit-write-path.ts +64 -0
  407. package/src/tools/ast-edit.ts +18 -1
  408. package/src/tools/ast-grep.ts +7 -2
  409. package/src/tools/atomic-file-write.ts +46 -15
  410. package/src/tools/auto-generated-guard.ts +15 -3
  411. package/src/tools/awaiting-input.ts +114 -0
  412. package/src/tools/bash-allowed-prefixes.ts +214 -6
  413. package/src/tools/bash-interactive.ts +14 -2
  414. package/src/tools/bash-skill-urls.ts +58 -5
  415. package/src/tools/bash.ts +113 -26
  416. package/src/tools/browser/tab-worker.ts +39 -13
  417. package/src/tools/browser.ts +17 -7
  418. package/src/tools/cron.ts +10 -1
  419. package/src/tools/debug.ts +16 -8
  420. package/src/tools/descriptors.test.ts +44 -0
  421. package/src/tools/descriptors.ts +50 -6
  422. package/src/tools/eval.ts +15 -0
  423. package/src/tools/fetch.ts +93 -4
  424. package/src/tools/gh.ts +27 -12
  425. package/src/tools/image-gen.ts +20 -6
  426. package/src/tools/index.ts +2 -0
  427. package/src/tools/irc.ts +5 -3
  428. package/src/tools/monitor.ts +9 -1
  429. package/src/tools/output-meta.ts +74 -5
  430. package/src/tools/python.ts +1 -1
  431. package/src/tools/read.ts +26 -37
  432. package/src/tools/recipe/runner.ts +23 -12
  433. package/src/tools/render-utils.ts +2 -2
  434. package/src/tools/report-issue.ts +184 -0
  435. package/src/tools/search.ts +3 -2
  436. package/src/tools/ssh.ts +274 -8
  437. package/src/tools/subagent-render.ts +114 -12
  438. package/src/tools/subagent.ts +118 -5
  439. package/src/tools/tmux-self-injection-guard.ts +3301 -122
  440. package/src/tools/todo-write.ts +41 -3
  441. package/src/tools/tool-catalog.generated.ts +32 -4
  442. package/src/tools/ultragoal-ask-guard.ts +13 -1
  443. package/src/tools/write.ts +42 -3
  444. package/src/tui/tree-list.ts +15 -1
  445. package/src/utils/commit-message-generator.ts +12 -4
  446. package/src/utils/external-editor.ts +21 -7
  447. package/src/utils/file-mentions.ts +75 -7
  448. package/src/utils/git.ts +38 -7
  449. package/src/utils/image-limits.ts +8 -0
  450. package/src/utils/image-loading.ts +1 -1
  451. package/src/utils/open.ts +73 -4
  452. package/src/utils/pasted-image-loading.ts +9 -8
  453. package/src/utils/prompt-suggestion.ts +5 -4
  454. package/src/utils/shell-snapshot.ts +9 -1
  455. package/src/utils/title-generator.ts +72 -22
  456. package/src/web/scrapers/index.ts +3 -1
  457. package/src/web/scrapers/types.ts +8 -0
  458. package/src/web/scrapers/youtube.ts +3 -2
  459. package/src/web/search/index.ts +2 -1
  460. package/src/web/search/providers/codex.ts +29 -32
  461. package/src/web/search/providers/insane.ts +91 -28
  462. package/src/web/search/providers/searxng.ts +62 -10
@@ -1,6 +1,8 @@
1
1
  import { AsyncLocalStorage } from "node:async_hooks";
2
+ import { constants as fsConstants } from "node:fs";
2
3
  import * as fs from "node:fs/promises";
3
4
  import * as path from "node:path";
5
+ import { canonicalEnvKey, loadedProjectEnvSnapshot } from "@gajae-code/utils/dirs";
4
6
  import { withFileLock } from "../config/file-lock";
5
7
  import type { ReceiptEnvelope } from "./receipts";
6
8
 
@@ -21,6 +23,42 @@ export interface ReceiptSpoolAppendResult {
21
23
  const receiptSpoolDirStorage = new AsyncLocalStorage<string | undefined>();
22
24
  const spoolQueues = new Map<string, Promise<void>>();
23
25
  const noop = (): void => undefined;
26
+ const NOFOLLOW = fsConstants.O_NOFOLLOW ?? 0;
27
+
28
+ /** True only when an exception's `code` is `ENOENT`. Other codes propagate. */
29
+ export function isEnoent(error: unknown): boolean {
30
+ return (error as NodeJS.ErrnoException).code === "ENOENT";
31
+ }
32
+
33
+ /**
34
+ * Bun copies the cwd dotenv into `process.env` before module init. The startup
35
+ * project snapshot is the declaration set trusted directory overrides use: a
36
+ * path that resolves like that declaration, or a dynamic declaration, is not a
37
+ * spool directory. A shell or `--receipt-spool-dir` value that resolves
38
+ * somewhere else is kept.
39
+ */
40
+ function projectDeclaredReceiptSpoolDir(raw: string): boolean {
41
+ const snapshot = loadedProjectEnvSnapshot();
42
+ const key = canonicalEnvKey(RECEIPT_SPOOL_DIR_ENV);
43
+ const projectValue = snapshot.values[key];
44
+ if (projectValue === undefined) return false;
45
+ if (snapshot.dynamic.has(key)) return true;
46
+ const declared = projectValue.trim();
47
+ const candidate = raw.trim();
48
+ if (!declared || !candidate) return declared === candidate;
49
+ return path.resolve(declared) === path.resolve(candidate);
50
+ }
51
+
52
+ function adoptReceiptSpoolDir(raw: string | undefined): string | undefined {
53
+ if (raw === undefined || projectDeclaredReceiptSpoolDir(raw)) return undefined;
54
+ const trimmed = raw.trim();
55
+ return trimmed ? path.resolve(trimmed) : undefined;
56
+ }
57
+
58
+ /** A spool directory the startup project snapshot does not declare. */
59
+ export function trustedReceiptSpoolDir(raw: string | undefined): string | undefined {
60
+ return adoptReceiptSpoolDir(raw);
61
+ }
24
62
 
25
63
  interface ReceiptSpoolCursorCache {
26
64
  mtimeMs: number;
@@ -44,10 +82,15 @@ export function resetReceiptSpoolCursorCacheForTesting(): void {
44
82
 
45
83
  async function spoolStat(spoolFile: string): Promise<{ mtimeMs: number; size: number } | undefined> {
46
84
  try {
47
- const stats = await fs.stat(spoolFile);
48
- return { mtimeMs: stats.mtimeMs, size: stats.size };
85
+ const handle = await fs.open(spoolFile, fsConstants.O_RDONLY | NOFOLLOW);
86
+ try {
87
+ const stats = await handle.stat();
88
+ return { mtimeMs: stats.mtimeMs, size: stats.size };
89
+ } finally {
90
+ await handle.close();
91
+ }
49
92
  } catch (error) {
50
- if ((error as NodeJS.ErrnoException).code === "ENOENT") return undefined;
93
+ if (isEnoent(error)) return undefined;
51
94
  throw error;
52
95
  }
53
96
  }
@@ -64,15 +107,17 @@ async function cacheHighestCursor(spoolFile: string, highest: bigint): Promise<v
64
107
  export async function withReceiptSpoolDir<T>(spoolDir: string, fn: () => Promise<T>): Promise<T> {
65
108
  const trimmed = spoolDir.trim();
66
109
  if (!trimmed) throw new Error("receipt_spool_dir_empty");
67
- const resolved = path.resolve(trimmed);
110
+ const resolved = adoptReceiptSpoolDir(trimmed);
111
+ // Owner input repeats process.env. A project-declared directory must not
112
+ // become the active spool via that copy.
113
+ if (!resolved) return fn();
68
114
  return receiptSpoolDirStorage.run(resolved, fn);
69
115
  }
70
116
 
71
117
  export function resolveReceiptSpoolDir(env: NodeJS.ProcessEnv = process.env): string | undefined {
72
118
  const active = receiptSpoolDirStorage.getStore();
73
119
  if (active !== undefined) return active;
74
- const raw = env[RECEIPT_SPOOL_DIR_ENV]?.trim();
75
- return raw ? path.resolve(raw) : undefined;
120
+ return adoptReceiptSpoolDir(env[RECEIPT_SPOOL_DIR_ENV]);
76
121
  }
77
122
 
78
123
  export function receiptSpoolPath(spoolDir: string): string {
@@ -95,30 +140,56 @@ export function formatReceiptSpoolCursor(cursor: bigint): string {
95
140
 
96
141
  export async function readHighestReceiptSpoolCursor(spoolDir: string): Promise<bigint> {
97
142
  const spoolFile = receiptSpoolPath(spoolDir);
98
- const revision = await spoolStat(spoolFile);
99
- if (!revision) {
100
- cursorCache.delete(spoolFile);
101
- return 0n;
143
+ let handle: fs.FileHandle;
144
+ try {
145
+ handle = await fs.open(spoolFile, fsConstants.O_RDONLY | NOFOLLOW);
146
+ } catch (error) {
147
+ if (isEnoent(error)) {
148
+ cursorCache.delete(spoolFile);
149
+ return 0n;
150
+ }
151
+ throw error;
102
152
  }
103
- const cached = cursorCache.get(spoolFile);
104
- if (cached && sameSpoolRevision(cached, revision)) return cached.highest;
105
-
106
- fullScanCount++;
107
- const raw = await fs.readFile(spoolFile, "utf8");
108
- let highest = 0n;
109
- for (const line of raw.split("\n")) {
110
- const trimmed = line.trim();
111
- if (!trimmed) continue;
112
- try {
113
- const parsed = JSON.parse(trimmed) as { cursor?: unknown };
114
- const cursor = parseCursor(parsed.cursor);
115
- if (cursor !== undefined && cursor > highest) highest = cursor;
116
- } catch {
117
- // A crash may leave a torn tail; a full scan ignores it and re-establishes the cache.
153
+ try {
154
+ const stats = await handle.stat();
155
+ const revision = { mtimeMs: stats.mtimeMs, size: stats.size };
156
+ const cached = cursorCache.get(spoolFile);
157
+ if (cached && sameSpoolRevision(cached, revision)) return cached.highest;
158
+
159
+ fullScanCount++;
160
+ const raw = await handle.readFile({ encoding: "utf8" });
161
+ let highest = 0n;
162
+ for (const line of raw.split("\n")) {
163
+ const trimmed = line.trim();
164
+ if (!trimmed) continue;
165
+ try {
166
+ const parsed = JSON.parse(trimmed) as { cursor?: unknown };
167
+ const cursor = parseCursor(parsed.cursor);
168
+ if (cursor !== undefined && cursor > highest) highest = cursor;
169
+ } catch {
170
+ // A crash may leave a torn tail; a full scan ignores it and re-establishes the cache.
171
+ }
118
172
  }
173
+ cursorCache.set(spoolFile, { ...revision, highest });
174
+ return highest;
175
+ } finally {
176
+ await handle.close();
177
+ }
178
+ }
179
+
180
+ async function tightenSpoolDir(dir: string): Promise<void> {
181
+ if (process.platform === "win32") return;
182
+ let handle: fs.FileHandle;
183
+ try {
184
+ handle = await fs.open(dir, fsConstants.O_RDONLY | (fsConstants.O_DIRECTORY ?? 0) | NOFOLLOW);
185
+ } catch {
186
+ return;
187
+ }
188
+ try {
189
+ await handle.chmod(0o700);
190
+ } finally {
191
+ await handle.close();
119
192
  }
120
- cursorCache.set(spoolFile, { ...revision, highest });
121
- return highest;
122
193
  }
123
194
 
124
195
  async function enqueueSpoolAppend<T>(spoolFile: string, task: () => Promise<T>): Promise<T> {
@@ -142,13 +213,19 @@ export async function appendReceiptToSpool(
142
213
  const spoolFile = receiptSpoolPath(resolvedDir);
143
214
  return enqueueSpoolAppend(spoolFile, async () => {
144
215
  await fs.mkdir(resolvedDir, { recursive: true, mode: 0o700 });
216
+ await tightenSpoolDir(resolvedDir);
145
217
  return withFileLock(
146
218
  spoolFile,
147
219
  async () => {
148
220
  const cursor = formatReceiptSpoolCursor((await readHighestReceiptSpoolCursor(resolvedDir)) + 1n);
149
221
  const record: ReceiptSpoolRecord = { cursor, envelope };
150
- const handle = await fs.open(spoolFile, "a", 0o600);
222
+ const handle = await fs.open(
223
+ spoolFile,
224
+ fsConstants.O_WRONLY | fsConstants.O_CREAT | fsConstants.O_APPEND | NOFOLLOW,
225
+ 0o600,
226
+ );
151
227
  try {
228
+ if (process.platform !== "win32") await handle.chmod(0o600);
152
229
  await handle.writeFile(`${JSON.stringify(record)}\n`, "utf8");
153
230
  await handle.sync();
154
231
  } finally {
@@ -293,11 +293,131 @@ export function sessionPaths(root: string, sessionId: string): SessionPaths {
293
293
  };
294
294
  }
295
295
 
296
+ const NO_FOLLOW = process.platform === "win32" ? 0 : (fsSync.constants.O_NOFOLLOW ?? 0);
297
+
298
+ function errnoCode(error: unknown): string | undefined {
299
+ return error instanceof Error && "code" in error ? (error as NodeJS.ErrnoException).code : undefined;
300
+ }
301
+
302
+ function sameInode(left: fsSync.BigIntStats, right: fsSync.BigIntStats): boolean {
303
+ return left.dev === right.dev && left.ino === right.ino && left.nlink === right.nlink;
304
+ }
305
+
306
+ /**
307
+ * Leaf directory mode 0700. `lstat` plus `chmod` does not need read permission, so a
308
+ * mode 0300 directory stays writable. A symlink leaf is refused before chmod.
309
+ * This does not pin the directory against a later rename in a writable parent.
310
+ */
311
+ async function ensureOwnerDirectory(dir: string): Promise<void> {
312
+ await fs.mkdir(dir, { recursive: true, mode: 0o700 });
313
+ let current: fsSync.Stats;
314
+ try {
315
+ current = await fs.lstat(dir);
316
+ } catch (error) {
317
+ const code = errnoCode(error);
318
+ if (code === "ELOOP" || code === "ENOTDIR") throw new Error("harness state directory is not a directory");
319
+ throw error;
320
+ }
321
+ if (current.isSymbolicLink() || !current.isDirectory())
322
+ throw new Error("harness state directory is not a directory");
323
+ if (process.platform === "win32" || (current.mode & 0o077) === 0) return;
324
+ await fs.chmod(dir, 0o700);
325
+ const tightened = await fs.lstat(dir);
326
+ if (tightened.isSymbolicLink() || !tightened.isDirectory() || (tightened.mode & 0o777) !== 0o700) {
327
+ throw new Error("harness state directory could not be tightened to mode 0700");
328
+ }
329
+ }
330
+
331
+ async function assertRegularOrMissing(file: string): Promise<void> {
332
+ try {
333
+ const existing = await fs.lstat(file);
334
+ if (existing.isSymbolicLink() || !existing.isFile()) throw new Error("harness state path is not a regular file");
335
+ } catch (error) {
336
+ if (errnoCode(error) !== "ENOENT") throw error;
337
+ }
338
+ }
339
+
340
+ /**
341
+ * Append like the workflow owner-only writer: mode 0600, `O_NOFOLLOW` when the file
342
+ * exists, `O_EXCL` when it does not, and chmod to 0600 when group or other bits are set.
343
+ */
344
+ async function appendOwnerOnly(filePath: string, text: string): Promise<void> {
345
+ await ensureOwnerDirectory(path.dirname(filePath));
346
+ let handle: fs.FileHandle | undefined;
347
+ let initial: fsSync.BigIntStats | undefined;
348
+ for (let attempt = 0; attempt < 2; attempt++) {
349
+ initial = undefined;
350
+ try {
351
+ initial = await fs.lstat(filePath, { bigint: true });
352
+ if (initial.isSymbolicLink() || !initial.isFile()) throw new Error("harness state path is not a regular file");
353
+ } catch (error) {
354
+ if (errnoCode(error) !== "ENOENT") throw error;
355
+ }
356
+ const flags = initial
357
+ ? fsSync.constants.O_WRONLY | fsSync.constants.O_APPEND | NO_FOLLOW
358
+ : fsSync.constants.O_WRONLY | fsSync.constants.O_APPEND | fsSync.constants.O_CREAT | fsSync.constants.O_EXCL;
359
+ try {
360
+ handle = await fs.open(filePath, flags, 0o600);
361
+ break;
362
+ } catch (error) {
363
+ const code = errnoCode(error);
364
+ if (code === "ELOOP") throw new Error("harness state path is not a regular file");
365
+ if (initial === undefined && code === "EEXIST" && attempt === 0) continue;
366
+ throw error;
367
+ }
368
+ }
369
+ if (!handle) throw new Error("harness state path identity changed before append");
370
+ try {
371
+ const opened = await handle.stat({ bigint: true });
372
+ const named = await fs.lstat(filePath, { bigint: true });
373
+ if (
374
+ opened.isSymbolicLink() ||
375
+ !opened.isFile() ||
376
+ named.isSymbolicLink() ||
377
+ !named.isFile() ||
378
+ (initial !== undefined && !sameInode(initial, opened)) ||
379
+ !sameInode(opened, named)
380
+ ) {
381
+ throw new Error("harness state path identity changed before append");
382
+ }
383
+ if (process.platform !== "win32" && (Number(opened.mode) & 0o077) !== 0) {
384
+ await handle.chmod(0o600);
385
+ const tightened = await handle.stat({ bigint: true });
386
+ if ((Number(tightened.mode) & 0o777) !== 0o600) {
387
+ throw new Error("harness state path could not be tightened to mode 0600");
388
+ }
389
+ }
390
+ await handle.writeFile(text, "utf8");
391
+ } finally {
392
+ await handle.close();
393
+ }
394
+ }
395
+
296
396
  async function writeJsonAtomic(file: string, value: unknown): Promise<void> {
297
- await fs.mkdir(path.dirname(file), { recursive: true });
397
+ await ensureOwnerDirectory(path.dirname(file));
398
+ await assertRegularOrMissing(file);
298
399
  const tmp = `${file}.tmp-${randomBytes(4).toString("hex")}`;
299
- await fs.writeFile(tmp, `${JSON.stringify(value, null, 2)}\n`, "utf8");
300
- await fs.rename(tmp, file);
400
+ const flags = fsSync.constants.O_WRONLY | fsSync.constants.O_CREAT | fsSync.constants.O_EXCL | NO_FOLLOW;
401
+ const handle = await fs.open(tmp, flags, 0o600);
402
+ try {
403
+ if (process.platform !== "win32") {
404
+ await handle.chmod(0o600);
405
+ const created = await handle.stat();
406
+ if (!created.isFile() || (created.mode & 0o777) !== 0o600) {
407
+ throw new Error("harness state path could not be tightened to mode 0600");
408
+ }
409
+ }
410
+ await handle.writeFile(`${JSON.stringify(value, null, 2)}\n`, "utf8");
411
+ } finally {
412
+ await handle.close();
413
+ }
414
+ try {
415
+ await assertRegularOrMissing(file);
416
+ await fs.rename(tmp, file);
417
+ } catch (error) {
418
+ await fs.rm(tmp, { force: true }).catch(() => undefined);
419
+ throw error;
420
+ }
301
421
  }
302
422
 
303
423
  async function readJson<T>(file: string): Promise<T | null> {
@@ -396,7 +516,6 @@ export async function resolveHarnessSessionRoot(
396
516
 
397
517
  export async function writeSessionState(root: string, state: SessionState): Promise<void> {
398
518
  const paths = sessionPaths(root, state.sessionId);
399
- await fs.mkdir(paths.dir, { recursive: true });
400
519
  await writeJsonAtomic(paths.state, state);
401
520
  }
402
521
 
@@ -407,8 +526,7 @@ export async function sessionExists(root: string, sessionId: string): Promise<bo
407
526
  /** Append a single severity envelope to events.jsonl. Single-writer discipline is the owner's job (M3). */
408
527
  export async function appendEvent(root: string, sessionId: string, envelope: EventEnvelope): Promise<void> {
409
528
  const paths = sessionPaths(root, sessionId);
410
- await fs.mkdir(paths.dir, { recursive: true });
411
- await fs.appendFile(paths.events, `${JSON.stringify(envelope)}\n`, "utf8");
529
+ await appendOwnerOnly(paths.events, `${JSON.stringify(envelope)}\n`);
412
530
  }
413
531
 
414
532
  /** Read events from cursor (exclusive). Tail-only: never mutates the log. */
@@ -20,27 +20,126 @@ export interface RecallResultLike {
20
20
  mentioned_at?: string | null;
21
21
  }
22
22
 
23
- const MEMORIES_REGEX = /<memories>[\s\S]*?<\/memories>/g;
24
- const LEGACY_HINDSIGHT_MEMORIES_REGEX = /<hindsight_memories>[\s\S]*?<\/hindsight_memories>/g;
25
- const LEGACY_RELEVANT_MEMORIES_REGEX = /<relevant_memories>[\s\S]*?<\/relevant_memories>/g;
26
- const MENTAL_MODELS_REGEX = /<mental_models>[\s\S]*?<\/mental_models>/g;
23
+ const MEMORY_TAG_NAMES = ["memories", "hindsight_memories", "relevant_memories", "mental_models"] as const;
24
+
25
+ function memoryTagExpression(name: string): RegExp {
26
+ return new RegExp(`<\\s*\\/?\\s*${name}\\s*>`, "gi");
27
+ }
28
+
29
+ interface MemoryTagSpan {
30
+ start: number;
31
+ end: number;
32
+ close: boolean;
33
+ paired: boolean;
34
+ absorbed: boolean;
35
+ }
36
+
37
+ function findMemoryTagSpans(content: string, name: string): MemoryTagSpan[] {
38
+ const spans: MemoryTagSpan[] = [];
39
+ for (const match of content.matchAll(memoryTagExpression(name))) {
40
+ const raw = match[0];
41
+ const start = match.index ?? 0;
42
+ spans.push({
43
+ start,
44
+ end: start + raw.length,
45
+ close: raw.includes("/"),
46
+ paired: false,
47
+ absorbed: false,
48
+ });
49
+ }
50
+ return spans;
51
+ }
52
+
53
+ function mergeSpans(ranges: Array<[number, number]>): Array<[number, number]> {
54
+ const sorted = ranges.filter(([start, end]) => end > start).sort((a, b) => a[0] - b[0] || b[1] - a[1]);
55
+ const merged: Array<[number, number]> = [];
56
+ for (const [start, end] of sorted) {
57
+ const last = merged[merged.length - 1];
58
+ if (!last || start > last[1]) merged.push([start, end]);
59
+ else last[1] = Math.max(last[1], end);
60
+ }
61
+ return merged;
62
+ }
63
+
64
+ /**
65
+ * Remove one memory-tag family.
66
+ *
67
+ * A matched block is removed whole. An extra closer after that block is the
68
+ * recall frame ending early: the escaped text through that closer is removed
69
+ * so it is not retained as instructions. A closer or opener with no block
70
+ * before it is removed as a token only, and the surrounding prose stays.
71
+ */
72
+ function stripMemoryTagFamily(content: string, name: string): string {
73
+ const tags = findMemoryTagSpans(content, name);
74
+ if (tags.length === 0) return content;
75
+
76
+ const ranges: Array<[number, number]> = [];
77
+ const stack: MemoryTagSpan[] = [];
78
+ for (const tag of tags) {
79
+ if (!tag.close) {
80
+ stack.push(tag);
81
+ continue;
82
+ }
83
+ const open = stack.pop();
84
+ if (!open) continue;
85
+ open.paired = true;
86
+ tag.paired = true;
87
+ ranges.push([open.start, tag.end]);
88
+ }
89
+
90
+ for (const tag of tags) {
91
+ if (!tag.close || tag.paired) continue;
92
+ let previous: MemoryTagSpan | undefined;
93
+ for (const candidate of tags) {
94
+ if (candidate.end <= tag.start) previous = candidate;
95
+ else break;
96
+ }
97
+ const followsClosedBlock = Boolean(previous?.close && (previous.paired || previous.absorbed));
98
+ if (followsClosedBlock && previous) {
99
+ ranges.push([previous.end, tag.end]);
100
+ tag.absorbed = true;
101
+ } else {
102
+ ranges.push([tag.start, tag.end]);
103
+ }
104
+ }
105
+
106
+ for (const tag of tags) {
107
+ if (!tag.close && !tag.paired) ranges.push([tag.start, tag.end]);
108
+ }
109
+
110
+ let out = content;
111
+ for (const [start, end] of mergeSpans(ranges).reverse()) {
112
+ out = out.slice(0, start) + out.slice(end);
113
+ }
114
+ return out;
115
+ }
116
+
117
+ /** Escape recognized tag delimiters so recalled text cannot close the outer frame. */
118
+ function stripMemoryTagTokens(text: string): string {
119
+ let out = text;
120
+ for (const name of MEMORY_TAG_NAMES) {
121
+ out = out.replace(memoryTagExpression(name), tag => tag.replace("<", "&lt;"));
122
+ }
123
+ return out;
124
+ }
27
125
 
28
126
  /**
29
127
  * Strip `<memories>`, `<mental_models>`, and legacy memory blocks.
30
128
  *
31
- * Both `<memories>` (per-turn recall) and `<mental_models>` (curated semantic
32
- * memory) are injected into the system prompt. If either leaks into the
33
- * retention transcript, every retain becomes a tighter feedback loop —
34
- * paraphrased memories feed the next consolidation, which feeds the next
35
- * mental-model refresh, which feeds the next retain. Always strip before
36
- * retaining.
129
+ * Matching is case-insensitive and allows whitespace inside the tag.
130
+ * Both per-turn recall and curated mental models are injected around the
131
+ * prompt. If a closer, a case variant, or an early-ended frame leaks into
132
+ * the retention transcript, the next retain stores the breakout as a new
133
+ * memory. Always strip before retaining.
37
134
  */
38
135
  export function stripMemoryTags(content: string): string {
39
- return content
40
- .replace(MEMORIES_REGEX, "")
41
- .replace(MENTAL_MODELS_REGEX, "")
42
- .replace(LEGACY_HINDSIGHT_MEMORIES_REGEX, "")
43
- .replace(LEGACY_RELEVANT_MEMORIES_REGEX, "");
136
+ let out = content;
137
+ while (true) {
138
+ let stripped = out;
139
+ for (const name of MEMORY_TAG_NAMES) stripped = stripMemoryTagFamily(stripped, name);
140
+ if (stripped === out) return out;
141
+ out = stripped;
142
+ }
44
143
  }
45
144
 
46
145
  /** Format recall results into a bullet list for context injection. */
@@ -50,7 +149,8 @@ export function formatMemories(results: RecallResultLike[]): string {
50
149
  .map(r => {
51
150
  const typeStr = r.type ? ` [${r.type}]` : "";
52
151
  const dateStr = r.mentioned_at ? ` (${r.mentioned_at})` : "";
53
- return `- ${r.text}${typeStr}${dateStr}`;
152
+ const text = stripMemoryTagTokens(r.text);
153
+ return `- ${text}${typeStr}${dateStr}`;
54
154
  })
55
155
  .join("\n\n");
56
156
  }
@@ -219,6 +219,17 @@ const TRUNCATION_MARKER = "\n\n…[mental-model snapshot truncated at render bud
219
219
  */
220
220
  const MIN_CONTENT_ROOM_CHARS = 64;
221
221
 
222
+ const MEMORY_FRAME_NAMES = ["memories", "hindsight_memories", "relevant_memories", "mental_models"] as const;
223
+
224
+ /** Escape the same frame tokens as recalled memory text so a stored closer cannot end this block early. */
225
+ function escapeMemoryFrameTokens(text: string): string {
226
+ let out = text;
227
+ for (const name of MEMORY_FRAME_NAMES) {
228
+ out = out.replace(new RegExp(`<\\s*\\/?\\s*${name}\\s*>`, "gi"), tag => tag.replace("<", "&lt;"));
229
+ }
230
+ return out;
231
+ }
232
+
222
233
  /** Smallest budget that can yield a usable block (wrapper + preamble + marker + a few chars of content). */
223
234
  function minRenderBudgetChars(): number {
224
235
  const cleanOverhead = `<mental_models>\n${PREAMBLE}\n\n\n</mental_models>`.length;
@@ -235,7 +246,6 @@ export function renderMentalModelsBlock(models: MentalModelSummary[], budgetChar
235
246
  if (budgetChars < minRenderBudgetChars()) return "";
236
247
 
237
248
  const truncatedOverhead = `<mental_models>\n${PREAMBLE}\n\n${TRUNCATION_MARKER}\n</mental_models>`.length;
238
- const cleanOverhead = `<mental_models>\n${PREAMBLE}\n\n\n</mental_models>`.length;
239
249
  const innerBudget = Math.max(0, budgetChars - truncatedOverhead);
240
250
  const perModelBudget = Math.max(120, Math.floor(innerBudget / Math.max(1, models.length)));
241
251
 
@@ -260,18 +270,20 @@ export function renderMentalModelsBlock(models: MentalModelSummary[], budgetChar
260
270
  consumed += sectionCost;
261
271
  }
262
272
 
273
+ // Escape after the heading/body join. A closer split across that newline
274
+ // (`</mental_models` + `>`) is not visible in either field alone.
275
+ // The wrapper is added after this step and is not escaped.
276
+ let inner = escapeMemoryFrameTokens(sections.join("\n\n"));
263
277
  const tail = truncated ? TRUNCATION_MARKER : "";
264
- let assembled = `<mental_models>\n${PREAMBLE}\n\n${sections.join("\n\n")}${tail}\n</mental_models>`;
278
+ let assembled = `<mental_models>\n${PREAMBLE}\n\n${inner}${tail}\n</mental_models>`;
265
279
 
266
- // Final hard-cap: if the careful per-model budgeting still slips past the
267
- // requested ceiling (small budgets, fat preambles, etc.), brutally truncate
268
- // the body region while keeping the wrapper intact so `stripMemoryTags` can
269
- // still find the closing tag.
280
+ // Final hard-cap: slice the already escaped body so a cut cannot restore a raw closer.
281
+ // The marker is always appended on this path, so reserve its overhead.
282
+ // Escaped tags are longer than the raw text the per-model budget measured.
270
283
  if (assembled.length > budgetChars) {
271
- const overhead = truncated ? truncatedOverhead : cleanOverhead;
272
- const room = Math.max(0, budgetChars - overhead);
273
- const body = sections.join("\n\n").slice(0, room).trimEnd();
274
- assembled = `<mental_models>\n${PREAMBLE}\n\n${body}${TRUNCATION_MARKER}\n</mental_models>`;
284
+ const room = Math.max(0, budgetChars - truncatedOverhead);
285
+ inner = inner.slice(0, room).trimEnd();
286
+ assembled = `<mental_models>\n${PREAMBLE}\n\n${inner}${TRUNCATION_MARKER}\n</mental_models>`;
275
287
  }
276
288
  return assembled;
277
289
  }
@@ -1,6 +1,7 @@
1
1
  import { createHash } from "node:crypto";
2
2
 
3
3
  import { logger } from "@gajae-code/utils";
4
+ import { redactMemorySecretsForTesting as redactMemorySecrets } from "../memories";
4
5
  import type { AgentSession } from "../session/agent-session";
5
6
  import { type BankScope, ensureBankMission } from "./bank";
6
7
  import type { HindsightApi, MemoryItemInput } from "./client";
@@ -149,8 +150,8 @@ export class HindsightRetainQueue {
149
150
  try {
150
151
  await ensureBankMission(state.client, state.bankId, state.config, state.missionsSet);
151
152
  const batch: MemoryItemInput[] = items.map(item => ({
152
- content: item.content,
153
- context: item.context ?? state.config.retainContext,
153
+ content: redactMemorySecrets(item.content),
154
+ context: item.context === undefined ? state.config.retainContext : redactMemorySecrets(item.context),
154
155
  metadata: { session_id: sessionId },
155
156
  tags: state.retainTags,
156
157
  }));
@@ -316,8 +317,10 @@ export class HindsightSessionState {
316
317
  documentId = `${this.sessionId}-${Date.now()}`;
317
318
  }
318
319
 
319
- const { transcript } = prepareRetentionTranscript(target, true);
320
- if (!transcript) return false;
320
+ const prepared = prepareRetentionTranscript(target, true);
321
+ if (!prepared.transcript) return false;
322
+ // Same scrub the local memory backend applies before it stores consolidation text.
323
+ const transcript = redactMemorySecrets(prepared.transcript);
321
324
 
322
325
  await ensureBankMission(this.client, this.bankId, this.config, this.missionsSet);
323
326
  await this.client.retain(this.bankId, transcript, {
@@ -1,10 +1,12 @@
1
1
  import { appendFile, mkdir, stat } from "node:fs/promises";
2
2
  import * as path from "node:path";
3
3
  import { getAgentDir, getConfigDirName } from "@gajae-code/utils";
4
+ import { projectEnvSnapshot } from "@gajae-code/utils/env-file";
4
5
  import { YAML } from "bun";
5
6
  import type { SkillDiscoverySettings } from "../config/skill-settings-defaults";
6
7
  import { DEFAULT_DISABLED_EXTENSIONS, DEFAULT_SKILL_DISCOVERY_SETTINGS } from "../config/skill-settings-defaults";
7
8
  import { sessionLogsDir } from "../gjc-runtime/session-layout";
9
+ import { trustedCoordinatorEnv } from "../gjc-runtime/sidecar-bootstrap-env";
8
10
  import {
9
11
  detectMcpDelegateFlowActivation,
10
12
  type McpDelegateHostContextV1,
@@ -281,14 +283,14 @@ function readTurnId(payload: HookPayload): string | undefined {
281
283
  return safeString(payload.turn_id ?? payload.turnId).trim() || undefined;
282
284
  }
283
285
 
284
- function readSessionFile(payload: HookPayload): string | undefined {
285
- return (
286
- safeString(
287
- payload.session_file ?? payload.sessionFile ?? payload.transcript_path ?? payload.transcriptPath,
288
- ).trim() ||
289
- process.env.GJC_SESSION_FILE?.trim() ||
290
- undefined
291
- );
286
+ function readSessionFile(payload: HookPayload, cwd: string): string | undefined {
287
+ const explicit = safeString(
288
+ payload.session_file ?? payload.sessionFile ?? payload.transcript_path ?? payload.transcriptPath,
289
+ ).trim();
290
+ if (explicit) return explicit;
291
+ // Bun has already copied the project dotenv into the environment. A value
292
+ // that matches that snapshot must not select another transcript.
293
+ return trustedCoordinatorEnv("GJC_SESSION_FILE", process.env, projectEnvSnapshot(cwd));
292
294
  }
293
295
 
294
296
  export async function dispatchGjcNativeSkillHook(
@@ -297,6 +299,7 @@ export async function dispatchGjcNativeSkillHook(
297
299
  ): Promise<GjcNativeHookDispatchResult> {
298
300
  const hookEventName = readHookEventName(payload);
299
301
  const cwd = (options.cwd ?? safeString(payload.cwd).trim()) || process.cwd();
302
+ const sessionFile = readSessionFile(payload, cwd);
300
303
  if (hookEventName === "UserPromptSubmit") {
301
304
  const recoveryDiagnostics = await collectUserPromptStateRecoveryDiagnostics({
302
305
  cwd,
@@ -304,7 +307,7 @@ export async function dispatchGjcNativeSkillHook(
304
307
  threadId: readThreadId(payload),
305
308
  stateDir: options.stateDir,
306
309
  prompt: readPromptText(payload),
307
- sessionFile: readSessionFile(payload),
310
+ sessionFile,
308
311
  });
309
312
  const recoveryContext = buildStateRecoveryDiagnosticsContext(recoveryDiagnostics);
310
313
  const prompt = readPromptText(payload);
@@ -346,7 +349,7 @@ export async function dispatchGjcNativeSkillHook(
346
349
  threadId: readThreadId(payload),
347
350
  stateDir: options.stateDir,
348
351
  prompt,
349
- sessionFile: readSessionFile(payload),
352
+ sessionFile,
350
353
  });
351
354
  if (activeUltragoalContext?.startsWith("BLOCK_ULTRAGOAL_COMPLETION:")) {
352
355
  return {
@@ -390,7 +393,7 @@ export async function dispatchGjcNativeSkillHook(
390
393
  sessionId: readSessionId(payload),
391
394
  threadId: readThreadId(payload),
392
395
  stateDir: options.stateDir,
393
- sessionFile: readSessionFile(payload),
396
+ sessionFile,
394
397
  }),
395
398
  };
396
399
  }
@@ -424,7 +427,9 @@ async function readStdinJson(): Promise<{ payload: HookPayload; parseError: Erro
424
427
  }
425
428
 
426
429
  async function logHookError(cwd: string, type: string, error: unknown): Promise<void> {
427
- const gjcSessionId = process.env.GJC_SESSION_ID?.trim();
430
+ // Bun has already copied the project dotenv into the environment. A value
431
+ // that matches that snapshot must not select another session's log directory.
432
+ const gjcSessionId = trustedCoordinatorEnv("GJC_SESSION_ID", process.env, projectEnvSnapshot(cwd));
428
433
  if (!gjcSessionId) {
429
434
  console.error(
430
435
  JSON.stringify({