@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
@@ -10,7 +10,7 @@ import { randomUUID } from "node:crypto";
10
10
  import * as fs from "node:fs";
11
11
  import * as os from "node:os";
12
12
  import * as path from "node:path";
13
- import { exactReplaceRetained, exactUnlink } from "@gajae-code/natives";
13
+ import { exactReplaceRetained, exactUnlink, exactUnlinkDirect } from "@gajae-code/natives";
14
14
  import { $which, APP_NAME, isCompiledBinary, isEnoent, logger, redactCrashSecrets, VERSION } from "@gajae-code/utils";
15
15
  import { $ } from "bun";
16
16
  import chalk from "chalk";
@@ -287,6 +287,40 @@ function resolveNpmManagedTarget(
287
287
  return undefined;
288
288
  }
289
289
 
290
+ /**
291
+ * Detect a project-local package install: `<...>/node_modules/.bin/<name>` with
292
+ * or without an executable extension.
293
+ *
294
+ * On Windows a dependency-installed CLI is a real `.exe` copied into
295
+ * `node_modules/.bin`, so the `.cmd`/`.ps1` shim rules never match it and
296
+ * `gjc update` used to treat it as a standalone binary and try to replace it
297
+ * (#6842). That file belongs to the package manager and to the project's
298
+ * dependency tree: replacing it breaks the lockfile contract and, on Windows,
299
+ * fails on the file's ACL. The caller must redirect to the package manager
300
+ * instead of downloading a replacement.
301
+ */
302
+ function resolveProjectLocalPackageInstall(
303
+ ompPath: string,
304
+ platform: PathPlatform = process.platform,
305
+ ): { packageRoot: string } | undefined {
306
+ const pathApi = pathApiForPlatform(platform);
307
+ const resolved = pathApi.normalize(ompPath);
308
+ const binDir = pathApi.dirname(resolved);
309
+ if (pathApi.basename(binDir).toLowerCase() !== ".bin") return undefined;
310
+ if (pathApi.basename(pathApi.dirname(binDir)).toLowerCase() !== "node_modules") return undefined;
311
+ const extension = pathApi.extname(resolved).toLowerCase();
312
+ const basename = extension ? pathApi.basename(resolved, extension) : pathApi.basename(resolved);
313
+ if (basename.toLowerCase() !== APP_NAME.toLowerCase()) return undefined;
314
+ return { packageRoot: pathApi.dirname(binDir) };
315
+ }
316
+
317
+ export function resolveProjectLocalPackageInstallForTest(
318
+ ompPath: string,
319
+ platform: PathPlatform,
320
+ ): { packageRoot: string } | undefined {
321
+ return resolveProjectLocalPackageInstall(ompPath, platform);
322
+ }
323
+
290
324
  function resolveUpdateMethod(ompPath: string, bunBinDir: string | undefined): "bun" | "npm" | "binary" {
291
325
  if (resolveNpmManagedTarget(ompPath)) return "npm";
292
326
  if (!bunBinDir) return "binary";
@@ -401,6 +435,15 @@ async function resolveUpdateTarget(): Promise<UpdateTarget> {
401
435
  );
402
436
  }
403
437
 
438
+ const projectLocalInstall = ompPath ? resolveProjectLocalPackageInstall(ompPath) : undefined;
439
+ if (ompPath && projectLocalInstall) {
440
+ throw new Error(
441
+ formatUnsupportedTargetMessage(
442
+ `${APP_NAME} at ${ompPath} is installed by a package manager in this project's node_modules; update the dependency instead of replacing that file`,
443
+ ),
444
+ );
445
+ }
446
+
404
447
  if (ompPath && isShimPath(ompPath, bunBinDir)) {
405
448
  if (isProtectedSourcePath(userPath)) {
406
449
  throw new Error(formatUnsupportedTargetMessage(`Refusing to install over a source checkout at ${userPath}`));
@@ -1351,14 +1394,179 @@ function binaryStagingPath(targetPath: string, stamp: string): string {
1351
1394
  return `${targetPath.slice(0, -extension.length)}.new.${stamp}${extension}`;
1352
1395
  }
1353
1396
 
1397
+ const UPDATE_FILE_ID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/u;
1398
+ const EXACT_REPLACE_DESTINATION_NAME = /^\.gjc-exact-replace-destination-([0-9a-f]+)-([0-9a-f]+)$/u;
1399
+ // Do not race a separate process that is still completing a native replacement in this directory.
1400
+ const EXACT_REPLACE_CLEANUP_GRACE_MS = 10 * 60 * 1000;
1401
+
1402
+ function isUpdateStagingName(targetPath: string, name: string): boolean {
1403
+ const extension = path.extname(targetPath);
1404
+ const prefix = `${path.basename(targetPath, extension)}.new.`;
1405
+ if (!name.startsWith(prefix)) return false;
1406
+ const suffix = name.slice(prefix.length);
1407
+ if (extension && !suffix.endsWith(extension)) return false;
1408
+ const id = extension ? suffix.slice(0, -extension.length) : suffix;
1409
+ return UPDATE_FILE_ID.test(id);
1410
+ }
1411
+
1412
+ function isUpdateBackupName(targetPath: string, name: string): boolean {
1413
+ const prefix = `${path.basename(targetPath)}.bak.`;
1414
+ if (name.startsWith(prefix) && UPDATE_FILE_ID.test(name.slice(prefix.length))) return true;
1415
+ const legacyPrefix = `${path.basename(targetPath)}-`;
1416
+ if (!name.startsWith(legacyPrefix)) return false;
1417
+ const legacyName = name.slice(legacyPrefix.length);
1418
+ const marker = legacyName.indexOf(".bak.");
1419
+ if (marker < 0) return false;
1420
+ const version = legacyName.slice(0, marker);
1421
+ const timestamp = legacyName.slice(marker + ".bak.".length);
1422
+ return (
1423
+ /^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?$/u.test(version) && /^[0-9A-Za-z._-]+$/u.test(timestamp)
1424
+ );
1425
+ }
1426
+
1427
+ export function isUpdateStagingNameForTest(targetPath: string, name: string): boolean {
1428
+ return isUpdateStagingName(targetPath, name);
1429
+ }
1430
+
1431
+ export function isUpdateBackupNameForTest(targetPath: string, name: string): boolean {
1432
+ return isUpdateBackupName(targetPath, name);
1433
+ }
1434
+
1435
+ async function removeExactUpdateArtifact(filePath: string, parent: DirectoryIdentity): Promise<string | undefined> {
1436
+ try {
1437
+ const snapshot = await snapshotRegularFile(filePath);
1438
+ if (!snapshot) return undefined;
1439
+ if (snapshot.identity.parentDev !== parent.dev || snapshot.identity.parentIno !== parent.ino)
1440
+ return `parent_identity_changed:${filePath}`;
1441
+ const removed = exactUnlinkDirect(
1442
+ filePath,
1443
+ toNativeIdentity(snapshot.identity, `.gjc-update-cleanup-${randomUUID()}`),
1444
+ );
1445
+ if (removed.ok || removed.code === "not_found") return undefined;
1446
+ const retained = [
1447
+ removed.detachedPath ? `detached=${removed.detachedPath}` : undefined,
1448
+ removed.retainedPlaceholderPath ? `placeholder=${removed.retainedPlaceholderPath}` : undefined,
1449
+ removed.retainedUnknownPath ? `unknown=${removed.retainedUnknownPath}` : undefined,
1450
+ removed.retainedSuccessorPath ? `successor=${removed.retainedSuccessorPath}` : undefined,
1451
+ ].filter((entry): entry is string => entry !== undefined);
1452
+ return `exact_unlink_${removed.code ?? "failed"}:${filePath}${retained.length ? `|${retained.join("|")}` : ""}`;
1453
+ } catch (error) {
1454
+ return `snapshot_failed:${filePath}:${error instanceof Error ? error.message : String(error)}`;
1455
+ }
1456
+ }
1457
+
1458
+ /**
1459
+ * Reclaim only generated update artifacts while holding the install lock.
1460
+ * Keep a stage only while an active activation record owns it, and retain at
1461
+ * most the rollback backup the record can still need.
1462
+ */
1463
+ async function cleanupBinaryUpdateArtifacts(
1464
+ targetPath: string,
1465
+ originalParent: DirectoryIdentity,
1466
+ retainedRetryCandidatePath?: string,
1467
+ ): Promise<string | undefined> {
1468
+ const parentPath = path.dirname(path.resolve(targetPath));
1469
+ const warnings: string[] = [];
1470
+ try {
1471
+ const currentParent = await snapshotDirectory(parentPath);
1472
+ if (!sameDirectoryIdentity(originalParent, currentParent)) return "installation_parent_changed";
1473
+ const entries = await fs.promises.readdir(parentPath, { withFileTypes: true });
1474
+ const activation = await readActivationRecord(targetPath);
1475
+ if (activation.status === "malformed" || activation.status === "foreign") {
1476
+ warnings.push(`activation_record_untrusted:${activation.reason}`);
1477
+ } else {
1478
+ const record = activation.status === "valid" ? activation.record : undefined;
1479
+ const terminal = record?.phase === "verified" || record?.phase === "rolled_back";
1480
+ const pinnedStages = new Set<string>();
1481
+ if (record && !terminal) pinnedStages.add(path.resolve(record.stagingPath));
1482
+ if (retainedRetryCandidatePath) pinnedStages.add(path.resolve(retainedRetryCandidatePath));
1483
+ for (const entry of entries) {
1484
+ if (!isUpdateStagingName(targetPath, entry.name)) continue;
1485
+ const stagingPath = path.join(parentPath, entry.name);
1486
+ if (pinnedStages.has(stagingPath)) continue;
1487
+ const warning = await removeExactUpdateArtifact(stagingPath, originalParent);
1488
+ if (warning) warnings.push(warning);
1489
+ }
1490
+
1491
+ const recognizedBackup = record && isUpdateBackupName(targetPath, record.backupName);
1492
+ let preserveBackup: string | undefined;
1493
+ let mayPruneBackups = activation.status === "missing";
1494
+ if (record && recognizedBackup) {
1495
+ if (record.phase === "rolled_back" || !record.baseline.exists) {
1496
+ mayPruneBackups = true;
1497
+ } else if (record.phase === "verified" || record.phase === "published" || record.phase === "uncertain") {
1498
+ if (record.backupIdentity) {
1499
+ const baseline = await snapshotRegularFile(path.join(parentPath, record.backupName));
1500
+ if (baseline && sameFileIdentity(record.backupIdentity, baseline.identity)) {
1501
+ preserveBackup = record.backupName;
1502
+ mayPruneBackups = true;
1503
+ }
1504
+ }
1505
+ } else if (record.phase === "staged" || record.phase === "pending_activation") {
1506
+ const current = await snapshotRegularFile(targetPath);
1507
+ const baselineUnchanged = record.baseline.exists
1508
+ ? !!current && !!record.targetIdentity && sameFileIdentity(record.targetIdentity, current.identity)
1509
+ : current === undefined;
1510
+ if (baselineUnchanged) {
1511
+ const transactionBackup = await snapshotRegularFile(path.join(parentPath, record.backupName));
1512
+ if (!transactionBackup) mayPruneBackups = true;
1513
+ else if (
1514
+ record.backupIdentity &&
1515
+ sameFileIdentity(record.backupIdentity, transactionBackup.identity)
1516
+ ) {
1517
+ preserveBackup = record.backupName;
1518
+ mayPruneBackups = true;
1519
+ }
1520
+ }
1521
+ }
1522
+ }
1523
+ if (mayPruneBackups) {
1524
+ for (const entry of entries) {
1525
+ if (!isUpdateBackupName(targetPath, entry.name) || entry.name === preserveBackup) continue;
1526
+ const warning = await removeExactUpdateArtifact(path.join(parentPath, entry.name), originalParent);
1527
+ if (warning) warnings.push(warning);
1528
+ }
1529
+ }
1530
+ }
1531
+
1532
+ if (process.platform !== "win32") {
1533
+ const staleBefore = BigInt(Date.now() - EXACT_REPLACE_CLEANUP_GRACE_MS) * 1_000_000n;
1534
+ for (const entry of entries) {
1535
+ const match = EXACT_REPLACE_DESTINATION_NAME.exec(entry.name);
1536
+ if (!match) continue;
1537
+ const placeholderPath = path.join(parentPath, entry.name);
1538
+ const placeholder = await snapshotRegularFile(placeholderPath);
1539
+ if (!placeholder) continue;
1540
+ const { identity } = placeholder;
1541
+ if (
1542
+ identity.parentDev !== originalParent.dev ||
1543
+ identity.parentIno !== originalParent.ino ||
1544
+ identity.size !== "0" ||
1545
+ identity.nlink !== "1" ||
1546
+ BigInt(identity.ino).toString(16) !== match[1]
1547
+ )
1548
+ continue;
1549
+ // The current flow has returned from its synchronous native calls; older
1550
+ // process IDs get the grace window above before their residue is reaped.
1551
+ if (match[2] !== process.pid.toString(16) && BigInt(identity.mtimeNs) >= staleBefore) continue;
1552
+ const warning = await removeExactUpdateArtifact(placeholderPath, originalParent);
1553
+ if (warning) warnings.push(warning);
1554
+ }
1555
+ }
1556
+ } catch (error) {
1557
+ warnings.push(error instanceof Error ? error.message : String(error));
1558
+ }
1559
+ return warnings.length > 0 ? warnings.join("\n") : undefined;
1560
+ }
1561
+
1354
1562
  /**
1355
1563
  * Orchestrate download → fsync → replace → verify with a strict ordering
1356
1564
  * contract: the downloaded temp binary MUST be flushed to stable storage
1357
1565
  * before it is published (renamed into place) or exec'd for verification.
1358
1566
  *
1359
1567
  * If fsync fails the temp bytes are not durable, so we abort before
1360
- * replacement/verification and retain the staging artifact rather than risk
1361
- * deleting a substituted pathname or installing a possibly-truncated binary.
1568
+ * replacement/verification. Final cleanup removes the exact staging identity
1569
+ * only when no activation record still owns it.
1362
1570
  */
1363
1571
  export async function runBinaryUpdateFlow(
1364
1572
  targetPath: string,
@@ -1370,15 +1578,19 @@ export async function runBinaryUpdateFlow(
1370
1578
  const tempPath = binaryStagingPath(targetPath, stamp);
1371
1579
  const backupPath = `${targetPath}.bak.${stamp}`;
1372
1580
  const releaseLock = await acquireBinaryUpdateLock(targetPath);
1581
+ let originalParent: DirectoryIdentity | undefined;
1582
+ let verification: InstalledVersionVerification | undefined;
1583
+ let failed = false;
1584
+ let failure: unknown;
1373
1585
  try {
1374
1586
  const originalTarget = await snapshotRegularFile(targetPath);
1375
- const originalParent = await snapshotDirectory(path.dirname(targetPath));
1587
+ originalParent = await snapshotDirectory(path.dirname(targetPath));
1376
1588
  if (!originalParent) throw new Error("installation_parent_unavailable");
1377
1589
  await flow.download(url, tempPath);
1378
1590
  await flow.fsync(tempPath);
1379
1591
 
1380
1592
  flow.beforeReplace?.();
1381
- return await flow.replace({
1593
+ verification = await flow.replace({
1382
1594
  targetPath,
1383
1595
  tempPath,
1384
1596
  backupPath,
@@ -1387,9 +1599,38 @@ export async function runBinaryUpdateFlow(
1387
1599
  originalParent,
1388
1600
  verifyInstalledVersion: flow.verifyInstalledVersion,
1389
1601
  });
1390
- } finally {
1602
+ } catch (error) {
1603
+ failed = true;
1604
+ failure = error;
1605
+ }
1606
+ const retainedRetryCandidatePath =
1607
+ failure instanceof ActivationRetryCandidateRepointError ? failure.candidatePath : undefined;
1608
+ const cleanupWarning = originalParent
1609
+ ? await cleanupBinaryUpdateArtifacts(targetPath, originalParent, retainedRetryCandidatePath)
1610
+ : undefined;
1611
+ try {
1391
1612
  await releaseLock();
1613
+ } catch (error) {
1614
+ if (!failed) {
1615
+ failed = true;
1616
+ failure = error;
1617
+ } else {
1618
+ logger.warn("Update install lock cleanup failed", {
1619
+ warning: error instanceof Error ? error.message : String(error),
1620
+ });
1621
+ }
1392
1622
  }
1623
+ if (failed) {
1624
+ if (cleanupWarning)
1625
+ logger.warn("Update artifact cleanup failed after unsuccessful update", { warning: cleanupWarning });
1626
+ throw failure;
1627
+ }
1628
+ if (!verification) throw new Error("binary_update_verification_missing");
1629
+ if (!cleanupWarning) return verification;
1630
+ return {
1631
+ ...verification,
1632
+ cleanupWarning: [verification.cleanupWarning, cleanupWarning].filter(Boolean).join("\n"),
1633
+ };
1393
1634
  }
1394
1635
 
1395
1636
  export async function acquireBinaryUpdateLock(targetPath: string): Promise<() => Promise<void>> {
@@ -47,7 +47,11 @@ import {
47
47
  } from "../harness-control-plane/lifecycle-reconciliation";
48
48
  import { type ResolvedOwner, RuntimeOwner, resolveOwner, resolveOwnerLive } from "../harness-control-plane/owner";
49
49
  import { preserveDirtyWorktree } from "../harness-control-plane/preserve";
50
- import { RECEIPT_SPOOL_DIR_ENV } from "../harness-control-plane/receipt-spool";
50
+ import {
51
+ RECEIPT_SPOOL_DIR_ENV,
52
+ resolveReceiptSpoolDir,
53
+ trustedReceiptSpoolDir,
54
+ } from "../harness-control-plane/receipt-spool";
51
55
  import { buildReceipt, requiresVanishBeforeAction, type VanishEvidence } from "../harness-control-plane/receipts";
52
56
  import { createSdkSessionTransport, spawnNormalHarnessSession } from "../harness-control-plane/sdk-transport";
53
57
  import { classifyLeaseStatus, readLease } from "../harness-control-plane/session-lease";
@@ -1004,8 +1008,9 @@ export default class Harness extends Command {
1004
1008
  `${GJC_TMUX_OWNER_STATE_DIR_ENV}=${shellQuote(ownerStateDir)}`,
1005
1009
  `${GJC_TMUX_OWNER_SERVER_KEY_ENV}=${shellQuote(socketKey)}`,
1006
1010
  ];
1007
- if (process.env[RECEIPT_SPOOL_DIR_ENV])
1008
- envAssignments.push(`${RECEIPT_SPOOL_DIR_ENV}=${shellQuote(process.env[RECEIPT_SPOOL_DIR_ENV])}`);
1011
+ const trustedSpoolDir = resolveReceiptSpoolDir();
1012
+ if (trustedSpoolDir) envAssignments.push(`${RECEIPT_SPOOL_DIR_ENV}=${shellQuote(trustedSpoolDir)}`);
1013
+ else envAssignments.unshift(`-u ${RECEIPT_SPOOL_DIR_ENV}`);
1009
1014
  if (process.env.GJC_HARNESS_TEST_NODE_MODULES)
1010
1015
  envAssignments.push(`GJC_HARNESS_TEST_NODE_MODULES=${shellQuote(process.env.GJC_HARNESS_TEST_NODE_MODULES)}`);
1011
1016
  if (process.env.GJC_SDK_DISABLE)
@@ -1203,19 +1208,20 @@ export default class Harness extends Command {
1203
1208
  }
1204
1209
  const fallbackReason = tmux.reason;
1205
1210
  const cmd = this.#buildOwnerCommand(sessionId);
1211
+ const trustedSpoolDir = resolveReceiptSpoolDir();
1212
+ const childEnv: NodeJS.ProcessEnv = {
1213
+ ...process.env,
1214
+ GJC_HARNESS_STATE_ROOT: root,
1215
+ ...(process.env.GJC_HARNESS_TEST_NODE_MODULES
1216
+ ? { GJC_HARNESS_TEST_NODE_MODULES: process.env.GJC_HARNESS_TEST_NODE_MODULES }
1217
+ : {}),
1218
+ ...(process.env.GJC_SDK_DISABLE ? { GJC_SDK_DISABLE: process.env.GJC_SDK_DISABLE } : {}),
1219
+ };
1220
+ if (trustedSpoolDir) childEnv[RECEIPT_SPOOL_DIR_ENV] = trustedSpoolDir;
1221
+ else delete childEnv[RECEIPT_SPOOL_DIR_ENV];
1206
1222
  const child = Bun.spawn(cmd, {
1207
1223
  cwd,
1208
- env: {
1209
- ...process.env,
1210
- GJC_HARNESS_STATE_ROOT: root,
1211
- ...(process.env[RECEIPT_SPOOL_DIR_ENV]
1212
- ? { [RECEIPT_SPOOL_DIR_ENV]: process.env[RECEIPT_SPOOL_DIR_ENV] }
1213
- : {}),
1214
- ...(process.env.GJC_HARNESS_TEST_NODE_MODULES
1215
- ? { GJC_HARNESS_TEST_NODE_MODULES: process.env.GJC_HARNESS_TEST_NODE_MODULES }
1216
- : {}),
1217
- ...(process.env.GJC_SDK_DISABLE ? { GJC_SDK_DISABLE: process.env.GJC_SDK_DISABLE } : {}),
1218
- },
1224
+ env: childEnv,
1219
1225
  stdout: "ignore",
1220
1226
  stderr: "ignore",
1221
1227
  stdin: "ignore",
@@ -1374,7 +1380,11 @@ export default class Harness extends Command {
1374
1380
  if (!owner.live || !owner.socketPath) return false;
1375
1381
  const priorSpoolDir = input[RECEIPT_SPOOL_DIR_ENV];
1376
1382
  try {
1377
- if (process.env[RECEIPT_SPOOL_DIR_ENV]) input[RECEIPT_SPOOL_DIR_ENV] = process.env[RECEIPT_SPOOL_DIR_ENV];
1383
+ const fromEnv = resolveReceiptSpoolDir();
1384
+ const fromInput = trustedReceiptSpoolDir(typeof priorSpoolDir === "string" ? priorSpoolDir : undefined);
1385
+ const trustedSpoolDir = fromEnv ?? fromInput;
1386
+ if (trustedSpoolDir) input[RECEIPT_SPOOL_DIR_ENV] = trustedSpoolDir;
1387
+ else delete input[RECEIPT_SPOOL_DIR_ENV];
1378
1388
  const res = (await callEndpoint(owner.socketPath, { verb, input })) as { ok?: boolean };
1379
1389
  writeJson(res);
1380
1390
  if (res?.ok === false) process.exitCode = 1;
@@ -1,11 +1,11 @@
1
1
  /**
2
- * Import MCP servers, skills, and instructions from other coding agents into GJC.
2
+ * Import MCP servers, skills, slash commands, and instructions from other coding agents into GJC.
3
3
  */
4
4
  import { Command, Flags } from "@gajae-code/utils/cli";
5
5
  import { type MigrateCommandArgs, runMigrateCommand } from "../cli/migrate-cli";
6
6
 
7
7
  export default class Migrate extends Command {
8
- static description = "Import MCP servers and skills, plus Claude Code/Codex instructions";
8
+ static description = "Import MCP servers, skills, slash commands, and instructions from other coding agents";
9
9
 
10
10
  static examples = [
11
11
  "gjc migrate --from claude-code",
@@ -25,7 +25,7 @@ export default class Migrate extends Command {
25
25
  default: false,
26
26
  }),
27
27
  force: Flags.boolean({
28
- description: "Overwrite existing skills, MCP servers, or instruction files instead of skipping them",
28
+ description: "Overwrite existing skills, commands, MCP servers, or instruction files instead of skipping them",
29
29
  default: false,
30
30
  }),
31
31
  "dry-run": Flags.boolean({ description: "Preview the migration without writing anything", default: false }),
@@ -12,11 +12,12 @@ import {
12
12
  } from "@gajae-code/natives";
13
13
  import { getAgentDir, logger, postmortem } from "@gajae-code/utils";
14
14
  import { CliParseError, Command } from "@gajae-code/utils/cli";
15
- import { projectEnvSnapshot } from "@gajae-code/utils/env-file";
15
+ import { startupProjectEnvSnapshot } from "@gajae-code/utils/env";
16
+ import { type ProjectEnvSnapshot, projectEnvSnapshot } from "@gajae-code/utils/env-file";
16
17
  import type { Args as ParsedArgs } from "../cli/args";
17
18
  import { isSafeSdkInternalAgentDir, scanPublicCommand } from "../cli/public-command-entry";
18
19
  import { PublicCommandFailure } from "../cli/public-command-errors";
19
- import { FileLockAcquireError } from "../config/file-lock";
20
+ import { FileLockAcquireError, isFileLockAcquireTimeout } from "../config/file-lock";
20
21
  import { parseModelString } from "../config/model-resolver";
21
22
  import { Settings } from "../config/settings";
22
23
  import { trustedCoordinatorEnv } from "../gjc-runtime/sidecar-bootstrap-env";
@@ -561,6 +562,65 @@ export function startMemoryBackendAfterReadiness(start: () => Promise<void>): vo
561
562
  });
562
563
  }
563
564
 
565
+ const SDK_CWD_EQUALITY_TEST_SEAMS = [
566
+ "GJC_SDK_TEST_HANG_MODEL_PROFILE",
567
+ "GJC_SDK_TEST_FAIL_AFTER_REGISTRATION",
568
+ "GJC_SDK_TEST_EXIT_AFTER_READY",
569
+ "GJC_SDK_TEST_REJECT_AFTER_READY",
570
+ ] as const;
571
+
572
+ type SdkCwdEqualityTestSeam = (typeof SDK_CWD_EQUALITY_TEST_SEAMS)[number];
573
+
574
+ function isSdkCwdEqualityTestSeam(name: string): name is SdkCwdEqualityTestSeam {
575
+ return (SDK_CWD_EQUALITY_TEST_SEAMS as readonly string[]).includes(name);
576
+ }
577
+
578
+ function sdkCwdEqualitySnapshots(cwd: string): ProjectEnvSnapshot[] {
579
+ // The startup snapshot keeps a declaration Bun loaded before a later chdir.
580
+ return [projectEnvSnapshot(cwd), startupProjectEnvSnapshot()];
581
+ }
582
+
583
+ /**
584
+ * Cwd-equality test seams accept only an operator value. A project dotenv line,
585
+ * including one Bun expands from `$PWD`, never arms the seam.
586
+ */
587
+ export function sdkCwdEqualityTestSeamActive(
588
+ name: string,
589
+ cwd: string,
590
+ env: NodeJS.ProcessEnv = process.env,
591
+ snapshots: readonly ProjectEnvSnapshot[] = sdkCwdEqualitySnapshots(cwd),
592
+ ): boolean {
593
+ if (!isSdkCwdEqualityTestSeam(name) || env[name] !== cwd) return false;
594
+ // Provenance only. trustedCoordinatorEnv trims, so a second compare to cwd
595
+ // would drop a trusted path that still matches exactly, including trailing space.
596
+ return snapshots.every(snapshot => trustedCoordinatorEnv(name, env, snapshot) !== undefined);
597
+ }
598
+
599
+ /** Copy `env` without project-dotenv values for the four cwd-equality seams. */
600
+ export function omitUntrustedSdkCwdEqualitySeams(
601
+ env: NodeJS.ProcessEnv,
602
+ snapshots: readonly ProjectEnvSnapshot[],
603
+ ): NodeJS.ProcessEnv {
604
+ const denied: string[] = [];
605
+ for (const name of SDK_CWD_EQUALITY_TEST_SEAMS) {
606
+ const raw = env[name];
607
+ if (typeof raw !== "string" || raw.length === 0) continue;
608
+ if (snapshots.some(snapshot => trustedCoordinatorEnv(name, env, snapshot) === undefined)) denied.push(name);
609
+ }
610
+ if (denied.length === 0) return env;
611
+ const next: NodeJS.ProcessEnv = { ...env };
612
+ for (const name of denied) delete next[name];
613
+ return next;
614
+ }
615
+
616
+ function dropUntrustedSdkCwdEqualitySeams(env: NodeJS.ProcessEnv, snapshots: readonly ProjectEnvSnapshot[]): void {
617
+ const next = omitUntrustedSdkCwdEqualitySeams(env, snapshots);
618
+ if (next === env) return;
619
+ for (const name of SDK_CWD_EQUALITY_TEST_SEAMS) {
620
+ if (next[name] === undefined) delete env[name];
621
+ }
622
+ }
623
+
564
624
  /** Runs the same persisted AgentSession bootstrap used by the production CLI. */
565
625
  export async function runSessionHost(
566
626
  timing: {
@@ -592,6 +652,11 @@ export async function runSessionHost(
592
652
  const agentDir = process.env.GJC_AGENT_DIR;
593
653
  if (!agentDir) throw new Error("GJC_AGENT_DIR is required for sdk session-host-internal.");
594
654
  const cwd = timing.cwd ?? process.cwd();
655
+ const cwdSeamSnapshots = sdkCwdEqualitySnapshots(cwd);
656
+ // Drop rejected copies before startup work can spawn a child that inherits them.
657
+ dropUntrustedSdkCwdEqualitySeams(process.env, cwdSeamSnapshots);
658
+ const cwdEqualitySeam = (name: SdkCwdEqualityTestSeam): boolean =>
659
+ sdkCwdEqualityTestSeamActive(name, cwd, process.env, cwdSeamSnapshots);
595
660
  if ((await fs.realpath(request.cwd)) !== (await fs.realpath(cwd)))
596
661
  throw new Error(`Lifecycle worktree mismatch: expected ${request.cwd}, got ${cwd}.`);
597
662
  if (
@@ -1423,7 +1488,7 @@ export async function runSessionHost(
1423
1488
  try {
1424
1489
  const startupThinkingLevel = request.modelId ? parseModelString(request.modelId)?.thinkingLevel : undefined;
1425
1490
  await beforeCutoff(() =>
1426
- process.env.GJC_SDK_TEST_HANG_MODEL_PROFILE === cwd
1491
+ cwdEqualitySeam("GJC_SDK_TEST_HANG_MODEL_PROFILE")
1427
1492
  ? new Promise<void>(() => {})
1428
1493
  : applyModelProfiles({
1429
1494
  session,
@@ -1450,7 +1515,7 @@ export async function runSessionHost(
1450
1515
  const startup = await beforeCutoff<SdkStartupResult>(() => capability.promise);
1451
1516
  if (startup.status !== "started") throw startup.failure;
1452
1517
  throwIfStartupInterrupted();
1453
- if (process.env.GJC_SDK_TEST_FAIL_AFTER_REGISTRATION === cwd)
1518
+ if (cwdEqualitySeam("GJC_SDK_TEST_FAIL_AFTER_REGISTRATION"))
1454
1519
  throw new Error("Lifecycle test failure after SDK host registration.");
1455
1520
 
1456
1521
  await beforeCutoff(() => session.sessionManager.ensureOnDisk());
@@ -1519,11 +1584,11 @@ export async function runSessionHost(
1519
1584
  // Readiness is published; the session is live. Memory startup issues one LLM
1520
1585
  // request per queued rollout, so it must run outside the readiness window and
1521
1586
  // its failure must never take the session down.
1522
- if (process.env.GJC_SDK_TEST_EXIT_AFTER_READY === cwd) {
1587
+ if (cwdEqualitySeam("GJC_SDK_TEST_EXIT_AFTER_READY")) {
1523
1588
  process.stderr.write("GJC_SDK_TEST_EXIT_AFTER_READY\n");
1524
1589
  process.exit(7);
1525
1590
  }
1526
- if (process.env.GJC_SDK_TEST_REJECT_AFTER_READY === cwd) {
1591
+ if (cwdEqualitySeam("GJC_SDK_TEST_REJECT_AFTER_READY")) {
1527
1592
  // Simulates the post-readiness liveness watcher rejecting. With the
1528
1593
  // post-ready startup receipt removed, this unhandled rejection kills the
1529
1594
  // host without writing any startup receipt; the broker must classify the
@@ -1768,6 +1833,7 @@ async function runSdkInternal(internal: SdkInternalArgv, publicBrokerRun = false
1768
1833
  const command = resolveSdkInternalSpawnCommand("broker-internal");
1769
1834
  const brokerEnvironment = { ...command.env };
1770
1835
  delete brokerEnvironment.GJC_BROKER_TRAMPOLINE_RETAIN_UNTIL_PUBLICATION;
1836
+ dropUntrustedSdkCwdEqualitySeams(brokerEnvironment, sdkCwdEqualitySnapshots(process.cwd()));
1771
1837
  let child: ChildProcess;
1772
1838
  try {
1773
1839
  child = spawn(command.file, [...command.args, "--agent-dir", internal.agentDir], {
@@ -1990,9 +2056,8 @@ async function runSdkInternal(internal: SdkInternalArgv, publicBrokerRun = false
1990
2056
  return await singleStartupAttempt(deadline);
1991
2057
  } catch (error) {
1992
2058
  if (
1993
- !(error instanceof FileLockAcquireError) ||
2059
+ !isFileLockAcquireTimeout(error) ||
1994
2060
  !startupFenceAcquired ||
1995
- error.code !== "acquire_timeout" ||
1996
2061
  path.resolve(error.filePath) !==
1997
2062
  path.resolve(path.join(agentDir, "sdk", "sessions", "index.jsonl")) ||
1998
2063
  sessionIndexTimeoutRetries >= MAX_SESSION_INDEX_TIMEOUT_RETRIES
@@ -2172,14 +2237,19 @@ async function runSdkInternal(internal: SdkInternalArgv, publicBrokerRun = false
2172
2237
  }
2173
2238
  }
2174
2239
  : undefined;
2240
+ const brokerSettings = await Settings.loadForScope({ cwd: process.cwd(), agentDir });
2175
2241
  const candidate = new Broker({
2176
2242
  agentDir,
2177
2243
  startupAbortSignal: startupAbortController.signal,
2178
2244
  startupCheckpointDeadline: effectiveCheckpointDeadline,
2179
2245
  onStartupReady: () => clearTimeout(startupWatchdog),
2180
- masterOrphanGraceMs: (await Settings.loadForScope({ cwd: process.cwd(), agentDir })).get(
2181
- "sdk.masterOrphanGraceMs",
2182
- ),
2246
+ masterOrphanGraceMs: brokerSettings.get("sdk.masterOrphanGraceMs"),
2247
+ abandonedGraceMs: brokerSettings.get("sdk.brokerAbandonedGraceMs"),
2248
+ // Only the detached autostart entry retires itself when nobody is left to
2249
+ // use it. A supervised `gjc sdk broker run` service is owned by its
2250
+ // operator's process manager, which reads an owned-root completion as a
2251
+ // clean exit and would not restart it.
2252
+ reapWhenAbandoned: !publicBrokerRun,
2183
2253
  resolveDirectoryMigration: async cwd => {
2184
2254
  const settings = await Settings.loadForScope({ cwd, agentDir });
2185
2255
  try {
@@ -1,7 +1,7 @@
1
1
  import { INTENT_FIELD, type ThinkingLevel } from "@gajae-code/agent-core";
2
2
  import type { Api, Model } from "@gajae-code/ai/core";
3
3
  import { Markdown } from "@gajae-code/tui";
4
- import { prompt } from "@gajae-code/utils";
4
+ import { prompt, sanitizeText } from "@gajae-code/utils";
5
5
  import chalk from "chalk";
6
6
  import typesDescriptionPrompt from "../../commit/prompts/types-description.md" with { type: "text" };
7
7
  import type { ModelRegistry } from "../../config/model-registry";
@@ -81,7 +81,7 @@ export async function runCommitAgentSession(input: CommitAgentInput): Promise<Co
81
81
  const toolArgsById = new Map<string, { name: string; args?: Record<string, unknown> }>();
82
82
  const writeThinkingLine = (text: string) => {
83
83
  if (!process.stdout.isTTY) return;
84
- const line = chalk.dim(`… ${text}`);
84
+ const line = chalk.dim(`… ${sanitizeText(text)}`);
85
85
  process.stdout.write(`\r\x1b[2K${line}`);
86
86
  thinkingLineActive = true;
87
87
  };
@@ -118,7 +118,7 @@ export async function runCommitAgentSession(input: CommitAgentInput): Promise<Co
118
118
  clearThinkingLine();
119
119
  const assistantMessage = event.message as { stopReason?: string; errorMessage?: string };
120
120
  if (assistantMessage.stopReason === "error" && assistantMessage.errorMessage) {
121
- process.stdout.write(`● Error: ${assistantMessage.errorMessage}\n`);
121
+ process.stdout.write(`● Error: ${sanitizeText(assistantMessage.errorMessage)}\n`);
122
122
  }
123
123
  const messageText = extractMessageText(event.message?.content ?? []);
124
124
  if (messageText) {
@@ -131,10 +131,10 @@ export async function runCommitAgentSession(input: CommitAgentInput): Promise<Co
131
131
  const stored = toolArgsById.get(event.toolCallId) ?? { name: event.toolName };
132
132
  toolArgsById.delete(event.toolCallId);
133
133
  clearThinkingLine();
134
- const toolLabel = formatToolLabel(stored.name);
134
+ const toolLabel = sanitizeText(formatToolLabel(stored.name));
135
135
  const symbol = event.isError ? "" : "";
136
136
  process.stdout.write(`${symbol} ${toolLabel}\n`);
137
- const argsLines = formatToolArgs(stored.args);
137
+ const argsLines = formatToolArgs(stored.args).map(line => sanitizeText(line));
138
138
  if (argsLines.length > 0) {
139
139
  process.stdout.write(`${formatToolArgsBlock(argsLines)}\n`);
140
140
  }
@@ -201,7 +201,7 @@ function extractMessageText(content: Array<{ type: string; text?: string }>): st
201
201
  }
202
202
 
203
203
  function writeAssistantMessage(message: string): void {
204
- const lines = renderMarkdownLines(message);
204
+ const lines = renderMarkdownLines(sanitizeText(message));
205
205
  if (lines.length === 0) return;
206
206
  let firstContentIndex = lines.findIndex(line => line.trim().length > 0);
207
207
  if (firstContentIndex === -1) {
@@ -5,6 +5,7 @@ import { prompt } from "@gajae-code/utils";
5
5
  import analysisSystemPrompt from "../../commit/prompts/analysis-system.md" with { type: "text" };
6
6
  import analysisUserPrompt from "../../commit/prompts/analysis-user.md" with { type: "text" };
7
7
  import type { ConventionalAnalysis } from "../../commit/types";
8
+ import { escapePromptMetadata } from "../../session/messages";
8
9
  import { toReasoningEffort } from "../../thinking";
9
10
  import { createConventionalAnalysisTool, parseConventionalAnalysisResponse } from "../shared-llm";
10
11
 
@@ -41,13 +42,21 @@ export async function generateConventionalAnalysis({
41
42
  diff,
42
43
  }: ConventionalAnalysisInput): Promise<ConventionalAnalysis> {
43
44
  const userContent = prompt.render(analysisUserPrompt, {
44
- context_files: contextFiles && contextFiles.length > 0 ? contextFiles : undefined,
45
+ context_files:
46
+ contextFiles && contextFiles.length > 0
47
+ ? contextFiles.map(file => ({
48
+ path: escapePromptMetadata(file.path),
49
+ content: escapePromptMetadata(file.content, { preserveNewlines: true }),
50
+ }))
51
+ : undefined,
45
52
  user_context: userContext,
46
53
  types_description: typesDescription,
47
- recent_commits: recentCommits?.join("\n"),
48
- scope_candidates: scopeCandidates,
49
- stat,
50
- diff,
54
+ recent_commits: recentCommits
55
+ ?.map(subject => escapePromptMetadata(subject, { preserveNewlines: true }))
56
+ .join("\n"),
57
+ scope_candidates: escapePromptMetadata(scopeCandidates, { preserveNewlines: true }),
58
+ stat: escapePromptMetadata(stat, { preserveNewlines: true }),
59
+ diff: escapePromptMetadata(diff, { preserveNewlines: true }),
51
60
  });
52
61
 
53
62
  const response = await completeSimple(