@gajae-code/coding-agent 0.18.12 → 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 (305) hide show
  1. package/CHANGELOG.md +467 -0
  2. package/dist/types/capability/rule.d.ts +7 -2
  3. package/dist/types/cli/append-system-prompt.d.ts +6 -0
  4. package/dist/types/cli/args.d.ts +7 -2
  5. package/dist/types/cli/customize-doctor-precedence.d.ts +16 -0
  6. package/dist/types/cli/customize-doctor.d.ts +4 -3
  7. package/dist/types/cli/public-command-errors.d.ts +1 -0
  8. package/dist/types/cli/root-flags.d.ts +0 -12
  9. package/dist/types/cli/update-cli.d.ts +7 -2
  10. package/dist/types/cli-main.d.ts +0 -6
  11. package/dist/types/commands/launch.d.ts +0 -6
  12. package/dist/types/commands/migrate.d.ts +1 -1
  13. package/dist/types/commands/sdk.d.ts +8 -0
  14. package/dist/types/commit/map-reduce/index.d.ts +4 -4
  15. package/dist/types/config/autorouting-tier-map.d.ts +23 -1
  16. package/dist/types/config/config-hot-reload.d.ts +5 -0
  17. package/dist/types/config/file-lock.d.ts +44 -2
  18. package/dist/types/config/model-profile-activation.d.ts +19 -5
  19. package/dist/types/config/model-registry.d.ts +16 -3
  20. package/dist/types/config/model-resolver.d.ts +5 -4
  21. package/dist/types/config/settings-schema.d.ts +11 -1
  22. package/dist/types/coordinator-mcp/server.d.ts +4 -0
  23. package/dist/types/customization/import.d.ts +2 -0
  24. package/dist/types/customization/types.d.ts +1 -1
  25. package/dist/types/dap/session.d.ts +11 -2
  26. package/dist/types/discovery/helpers.d.ts +7 -2
  27. package/dist/types/edit/index.d.ts +5 -0
  28. package/dist/types/edit/modes/patch.d.ts +9 -0
  29. package/dist/types/eval/py/runtime.d.ts +9 -0
  30. package/dist/types/export/ttsr.d.ts +19 -0
  31. package/dist/types/extensibility/extensions/types.d.ts +5 -1
  32. package/dist/types/extensibility/slash-commands.d.ts +1 -0
  33. package/dist/types/gjc-runtime/ralplan-current-final.d.ts +54 -0
  34. package/dist/types/gjc-runtime/ralplan-runtime.d.ts +5 -0
  35. package/dist/types/gjc-runtime/tmux-sessions.d.ts +24 -0
  36. package/dist/types/gjc-runtime/ultragoal-change-set.d.ts +6 -0
  37. package/dist/types/gjc-runtime/ultragoal-runtime.d.ts +1 -0
  38. package/dist/types/harness-control-plane/receipt-spool.d.ts +4 -0
  39. package/dist/types/hindsight/content.d.ts +5 -6
  40. package/dist/types/internal-urls/types.d.ts +2 -0
  41. package/dist/types/lsp/clients/swiftlint-client.d.ts +0 -4
  42. package/dist/types/main.d.ts +23 -18
  43. package/dist/types/migrate/action-planner.d.ts +10 -1
  44. package/dist/types/migrate/adapters/codex.d.ts +1 -1
  45. package/dist/types/migrate/adapters/index.d.ts +35 -3
  46. package/dist/types/migrate/hook-module.d.ts +23 -0
  47. package/dist/types/migrate/report.d.ts +2 -1
  48. package/dist/types/migrate/types.d.ts +69 -2
  49. package/dist/types/modes/components/agent-dashboard.d.ts +1 -1
  50. package/dist/types/modes/components/model-selector.d.ts +43 -0
  51. package/dist/types/modes/prompt-suggestion-controller.d.ts +1 -1
  52. package/dist/types/modes/utils/host-status.d.ts +15 -11
  53. package/dist/types/runtime-mcp/project-host-values.d.ts +11 -5
  54. package/dist/types/sdk/broker/broker-exit.d.ts +1 -1
  55. package/dist/types/sdk/broker/broker.d.ts +22 -1
  56. package/dist/types/sdk/broker/ensure.d.ts +5 -2
  57. package/dist/types/sdk/broker/hop.d.ts +9 -0
  58. package/dist/types/sdk/broker/lifecycle.d.ts +22 -0
  59. package/dist/types/sdk/broker/session-index.d.ts +2 -0
  60. package/dist/types/sdk/broker/transport.d.ts +2 -0
  61. package/dist/types/sdk/bus/index.d.ts +21 -0
  62. package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +7 -1
  63. package/dist/types/sdk/bus/telegram-daemon.d.ts +2 -0
  64. package/dist/types/sdk/bus/threaded-render.d.ts +2 -1
  65. package/dist/types/sdk/host/control/operations.d.ts +5 -1
  66. package/dist/types/sdk/host/events.d.ts +4 -0
  67. package/dist/types/sdk/host/host.d.ts +2 -1
  68. package/dist/types/sdk/host/prompt-image-upload.d.ts +43 -0
  69. package/dist/types/sdk/host/session-runtime.d.ts +8 -8
  70. package/dist/types/sdk/host/surface-policy.d.ts +2 -1
  71. package/dist/types/sdk/lifecycle/service.d.ts +12 -0
  72. package/dist/types/sdk/prompt-status.d.ts +11 -4
  73. package/dist/types/session/agent-session.d.ts +50 -8
  74. package/dist/types/session/session-manager.d.ts +16 -0
  75. package/dist/types/skill-state/workflow-mutation-guard.d.ts +13 -1
  76. package/dist/types/slash-commands/helpers/mcp.d.ts +5 -0
  77. package/dist/types/ssh/ssh-executor.d.ts +8 -1
  78. package/dist/types/system-prompt.d.ts +3 -2
  79. package/dist/types/task/role-body.d.ts +11 -0
  80. package/dist/types/tools/awaiting-input.d.ts +28 -0
  81. package/dist/types/tools/bash-allowed-prefixes.d.ts +6 -0
  82. package/dist/types/tools/bash-skill-urls.d.ts +4 -0
  83. package/dist/types/tools/bash.d.ts +7 -0
  84. package/dist/types/tools/descriptors.d.ts +13 -2
  85. package/dist/types/tools/fetch.d.ts +6 -0
  86. package/dist/types/tools/irc.d.ts +5 -3
  87. package/dist/types/tools/tmux-self-injection-guard.d.ts +2 -0
  88. package/dist/types/tools/ultragoal-ask-guard.d.ts +1 -1
  89. package/dist/types/tools/write.d.ts +2 -0
  90. package/dist/types/utils/file-mentions.d.ts +5 -0
  91. package/dist/types/utils/image-limits.d.ts +7 -0
  92. package/dist/types/utils/image-loading.d.ts +0 -1
  93. package/dist/types/utils/open.d.ts +0 -1
  94. package/dist/types/utils/pasted-image-loading.d.ts +1 -5
  95. package/dist/types/utils/prompt-suggestion.d.ts +2 -2
  96. package/dist/types/utils/shell-snapshot.d.ts +2 -0
  97. package/dist/types/utils/title-generator.d.ts +13 -3
  98. package/dist/types/web/search/providers/codex.d.ts +6 -7
  99. package/package.json +9 -8
  100. package/scripts/acp-conformance-agent.ts +12 -2
  101. package/scripts/generate-sdk-operation-inventory.ts +16 -0
  102. package/src/capability/fs.ts +31 -3
  103. package/src/capability/rule.ts +11 -2
  104. package/src/cli/append-system-prompt.ts +12 -0
  105. package/src/cli/args.ts +11 -7
  106. package/src/cli/customize-doctor-precedence.ts +31 -0
  107. package/src/cli/customize-doctor.ts +49 -39
  108. package/src/cli/fast-help.ts +0 -2
  109. package/src/cli/migrate-cli.ts +14 -3
  110. package/src/cli/public-command-errors.ts +2 -0
  111. package/src/cli/root-flags.ts +0 -2
  112. package/src/cli/update-cli.ts +247 -6
  113. package/src/commands/harness.ts +25 -15
  114. package/src/commands/migrate.ts +3 -3
  115. package/src/commands/sdk.ts +81 -11
  116. package/src/commit/agentic/agent.ts +6 -6
  117. package/src/commit/analysis/conventional.ts +14 -5
  118. package/src/commit/map-reduce/index.ts +7 -7
  119. package/src/commit/pipeline.ts +12 -12
  120. package/src/config/autorouting-tier-map.ts +20 -1
  121. package/src/config/config-file.ts +106 -3
  122. package/src/config/config-hot-reload.ts +8 -0
  123. package/src/config/file-lock.ts +1431 -304
  124. package/src/config/keybindings.ts +154 -7
  125. package/src/config/model-profile-activation.ts +177 -39
  126. package/src/config/model-registry.ts +380 -55
  127. package/src/config/model-resolver.ts +49 -21
  128. package/src/config/settings-schema.ts +13 -1
  129. package/src/config/settings.ts +2 -0
  130. package/src/coordinator-mcp/server.ts +37 -2
  131. package/src/crash/sanitize.ts +3 -1
  132. package/src/customization/import.ts +276 -7
  133. package/src/customization/inventory.ts +24 -8
  134. package/src/customization/types.ts +1 -0
  135. package/src/dap/client.ts +3 -2
  136. package/src/dap/session.ts +161 -36
  137. package/src/debug/crash-diagnostics.ts +87 -5
  138. package/src/debug/report-bundle.ts +71 -2
  139. package/src/discovery/builtin.ts +4 -3
  140. package/src/discovery/helpers.ts +24 -14
  141. package/src/edit/index.ts +191 -5
  142. package/src/edit/modes/patch.ts +27 -0
  143. package/src/eval/py/runtime.ts +44 -4
  144. package/src/exec/non-interactive-env.ts +0 -1
  145. package/src/export/html/index.ts +60 -20
  146. package/src/export/ttsr.ts +176 -1
  147. package/src/extensibility/extensions/loader.ts +2 -0
  148. package/src/extensibility/extensions/ouroboros-ooo-bridge.ts +63 -1
  149. package/src/extensibility/extensions/types.ts +5 -1
  150. package/src/extensibility/hooks/loader.ts +10 -2
  151. package/src/extensibility/plugins/marketplace/manager.ts +36 -10
  152. package/src/extensibility/slash-commands.ts +5 -0
  153. package/src/gjc-runtime/gc-runtime.ts +4 -1
  154. package/src/gjc-runtime/launch-tmux.ts +23 -13
  155. package/src/gjc-runtime/ralplan-current-final.ts +294 -0
  156. package/src/gjc-runtime/ralplan-runtime.ts +95 -1
  157. package/src/gjc-runtime/state-writer.ts +109 -5
  158. package/src/gjc-runtime/tmux-sessions.ts +93 -13
  159. package/src/gjc-runtime/ultragoal-change-set.ts +90 -11
  160. package/src/gjc-runtime/ultragoal-guard.ts +9 -1
  161. package/src/gjc-runtime/ultragoal-runtime.ts +15 -5
  162. package/src/harness-control-plane/finalize.ts +11 -0
  163. package/src/harness-control-plane/receipt-spool.ts +105 -28
  164. package/src/harness-control-plane/storage.ts +124 -6
  165. package/src/hindsight/content.ts +116 -16
  166. package/src/hindsight/mental-models.ts +22 -10
  167. package/src/hindsight/state.ts +7 -4
  168. package/src/hooks/native-skill-hook.ts +3 -1
  169. package/src/internal-urls/artifact-protocol.ts +3 -1
  170. package/src/internal-urls/docs-index.generated.ts +14 -14
  171. package/src/internal-urls/local-protocol.ts +28 -3
  172. package/src/internal-urls/types.ts +2 -0
  173. package/src/lsp/client.ts +2 -1
  174. package/src/lsp/clients/biome-client.ts +2 -0
  175. package/src/lsp/clients/swiftlint-client.ts +2 -0
  176. package/src/main.ts +330 -92
  177. package/src/memories/index.ts +25 -2
  178. package/src/migrate/action-planner.ts +517 -2
  179. package/src/migrate/adapters/claude-code.ts +141 -3
  180. package/src/migrate/adapters/codex.ts +57 -5
  181. package/src/migrate/adapters/index.ts +240 -5
  182. package/src/migrate/adapters/opencode.ts +4 -0
  183. package/src/migrate/executor.ts +141 -3
  184. package/src/migrate/hook-module.ts +193 -0
  185. package/src/migrate/report.ts +45 -1
  186. package/src/migrate/types.ts +73 -3
  187. package/src/modes/acp/acp-agent.ts +10 -1
  188. package/src/modes/components/agent-dashboard.ts +20 -4
  189. package/src/modes/components/customization/customization-dashboard.ts +7 -0
  190. package/src/modes/components/customization/import-wizard.ts +29 -3
  191. package/src/modes/components/model-selector.ts +157 -103
  192. package/src/modes/components/welcome.ts +3 -1
  193. package/src/modes/controllers/command-controller.ts +271 -16
  194. package/src/modes/controllers/event-controller.ts +1 -1
  195. package/src/modes/controllers/extension-ui-controller.ts +31 -1
  196. package/src/modes/controllers/goal-mode-controller.ts +79 -10
  197. package/src/modes/controllers/input-controller.ts +2 -0
  198. package/src/modes/controllers/plan-mode-controller.ts +49 -4
  199. package/src/modes/controllers/runtime-mcp-command-controller.ts +7 -6
  200. package/src/modes/controllers/selector-controller.ts +14 -1
  201. package/src/modes/interactive-mode.ts +55 -4
  202. package/src/modes/prompt-suggestion-controller.ts +1 -1
  203. package/src/modes/runtime-init.ts +5 -1
  204. package/src/modes/utils/host-status.ts +21 -10
  205. package/src/prompts/agent-fragments/browser-aside.md +2 -2
  206. package/src/prompts/system/irc-incoming.md +3 -1
  207. package/src/prompts/tools/irc.md +1 -1
  208. package/src/runtime-mcp/manager.ts +8 -3
  209. package/src/runtime-mcp/project-host-values.ts +12 -8
  210. package/src/runtime-mcp/tool-bridge.ts +68 -4
  211. package/src/runtime-mcp/transports/stdio.ts +2 -1
  212. package/src/sdk/broker/broker-exit.ts +2 -0
  213. package/src/sdk/broker/broker.ts +143 -4
  214. package/src/sdk/broker/ensure.ts +74 -6
  215. package/src/sdk/broker/hop.ts +13 -1
  216. package/src/sdk/broker/lifecycle-ledger.ts +3 -0
  217. package/src/sdk/broker/lifecycle.ts +102 -6
  218. package/src/sdk/broker/session-index.ts +29 -4
  219. package/src/sdk/broker/transport.ts +17 -1
  220. package/src/sdk/bus/chat-daemon-control.ts +2 -2
  221. package/src/sdk/bus/index.ts +408 -14
  222. package/src/sdk/bus/session-control.ts +2 -0
  223. package/src/sdk/bus/slack-daemon.ts +105 -9
  224. package/src/sdk/bus/telegram-daemon-contract.ts +7 -1
  225. package/src/sdk/bus/telegram-daemon.ts +10 -2
  226. package/src/sdk/bus/threaded-render.ts +20 -1
  227. package/src/sdk/cli/session-cli.ts +2 -0
  228. package/src/sdk/host/control/dispatch.ts +13 -1
  229. package/src/sdk/host/control/operations.ts +10 -1
  230. package/src/sdk/host/events.ts +67 -4
  231. package/src/sdk/host/host.ts +8 -2
  232. package/src/sdk/host/prompt-image-upload.ts +438 -0
  233. package/src/sdk/host/sdk-surface-parity.test.ts +15 -0
  234. package/src/sdk/host/session-runtime.test.ts +1537 -24
  235. package/src/sdk/host/session-runtime.ts +332 -36
  236. package/src/sdk/host/surface-policy.ts +4 -0
  237. package/src/sdk/host/websocket-transport.lifecycle.test.ts +90 -2
  238. package/src/sdk/host/websocket-transport.ts +19 -7
  239. package/src/sdk/lifecycle/service.ts +14 -1
  240. package/src/sdk/prompt-status.ts +6 -4
  241. package/src/sdk/protocol/adapter-validation.ts +28 -1
  242. package/src/sdk/protocol/operation-inventory.generated.json +160 -0
  243. package/src/sdk/protocol/operation-registry.ts +33 -3
  244. package/src/sdk/session.ts +123 -36
  245. package/src/session/agent-session.ts +1614 -329
  246. package/src/session/history-storage.ts +182 -12
  247. package/src/session/internal/managed-session-scope.ts +47 -18
  248. package/src/session/internal/managed-session-storage.ts +71 -1
  249. package/src/session/session-manager.ts +90 -5
  250. package/src/session-import/redact.ts +53 -2
  251. package/src/skill-state/workflow-mutation-guard.ts +1498 -18
  252. package/src/slash-commands/builtin-registry.ts +6 -3
  253. package/src/slash-commands/helpers/mcp.ts +11 -3
  254. package/src/ssh/ssh-executor.ts +9 -2
  255. package/src/stt/recorder.ts +26 -9
  256. package/src/system-prompt.ts +61 -16
  257. package/src/task/executor.ts +12 -8
  258. package/src/task/role-body.ts +84 -0
  259. package/src/task/token-log-mode.test.ts +162 -0
  260. package/src/task/token-log.ts +91 -2
  261. package/src/tools/archive-reader.ts +650 -37
  262. package/src/tools/ask.ts +1 -0
  263. package/src/tools/ast-grep.ts +7 -2
  264. package/src/tools/auto-generated-guard.ts +12 -3
  265. package/src/tools/awaiting-input.ts +114 -0
  266. package/src/tools/bash-allowed-prefixes.ts +61 -1
  267. package/src/tools/bash-skill-urls.ts +58 -5
  268. package/src/tools/bash.ts +99 -26
  269. package/src/tools/browser/tab-worker.ts +39 -13
  270. package/src/tools/browser.ts +17 -7
  271. package/src/tools/cron.ts +10 -1
  272. package/src/tools/debug.ts +9 -2
  273. package/src/tools/descriptors.test.ts +4 -4
  274. package/src/tools/descriptors.ts +16 -4
  275. package/src/tools/eval.ts +15 -0
  276. package/src/tools/fetch.ts +90 -2
  277. package/src/tools/gh.ts +27 -12
  278. package/src/tools/image-gen.ts +20 -6
  279. package/src/tools/irc.ts +5 -3
  280. package/src/tools/monitor.ts +9 -1
  281. package/src/tools/output-meta.ts +51 -3
  282. package/src/tools/read.ts +9 -2
  283. package/src/tools/recipe/runner.ts +23 -12
  284. package/src/tools/render-utils.ts +2 -2
  285. package/src/tools/search.ts +3 -2
  286. package/src/tools/ssh.ts +274 -8
  287. package/src/tools/subagent.ts +10 -4
  288. package/src/tools/tmux-self-injection-guard.ts +3301 -122
  289. package/src/tools/tool-catalog.generated.ts +1 -1
  290. package/src/tools/ultragoal-ask-guard.ts +13 -1
  291. package/src/tools/write.ts +7 -0
  292. package/src/tui/tree-list.ts +15 -1
  293. package/src/utils/commit-message-generator.ts +3 -2
  294. package/src/utils/external-editor.ts +21 -7
  295. package/src/utils/file-mentions.ts +46 -6
  296. package/src/utils/git.ts +38 -7
  297. package/src/utils/image-limits.ts +8 -0
  298. package/src/utils/image-loading.ts +1 -1
  299. package/src/utils/open.ts +73 -4
  300. package/src/utils/pasted-image-loading.ts +9 -8
  301. package/src/utils/prompt-suggestion.ts +5 -4
  302. package/src/utils/shell-snapshot.ts +9 -1
  303. package/src/utils/title-generator.ts +65 -20
  304. package/src/web/search/index.ts +2 -1
  305. package/src/web/search/providers/codex.ts +29 -32
@@ -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, {
@@ -427,7 +427,9 @@ async function readStdinJson(): Promise<{ payload: HookPayload; parseError: Erro
427
427
  }
428
428
 
429
429
  async function logHookError(cwd: string, type: string, error: unknown): Promise<void> {
430
- 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));
431
433
  if (!gjcSessionId) {
432
434
  console.error(
433
435
  JSON.stringify({
@@ -2,7 +2,8 @@
2
2
  * Protocol handler for artifact:// URLs.
3
3
  *
4
4
  * Resolves artifact IDs only against artifacts directories explicitly authorized
5
- * by the caller's ResolveContext. Unlike agent://, artifacts are raw text.
5
+ * by the caller's ResolveContext. SSH output artifacts are marked untrusted so
6
+ * ReadTool wraps the selected text after applying selectors and truncation.
6
7
  *
7
8
  * URL form:
8
9
  * - artifact://<id> - Full artifact content
@@ -97,6 +98,7 @@ export class ArtifactProtocolHandler implements ProtocolHandler {
97
98
  contentType: "text/plain",
98
99
  size: Buffer.byteLength(content, "utf-8"),
99
100
  sourcePath: foundPath,
101
+ untrusted: path.basename(foundPath).endsWith(".ssh.log"),
100
102
  };
101
103
  }
102
104
  }