@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
@@ -237,8 +237,8 @@ function assertManagedParentSync(localRoot: string): void {
237
237
  }
238
238
 
239
239
  /**
240
- * Rebind a pinned root only when this call created it because the path was
241
- * absent. An already-present directory must still be the pinned one.
240
+ * Keep a pinned root unless a changed inode is validated as a settled
241
+ * recreation. A root created by this call may be rebound after migration.
242
242
  */
243
243
  function acceptManagedRootForInit(
244
244
  localRoot: string,
@@ -250,6 +250,7 @@ function acceptManagedRootForInit(
250
250
  if (sameManagedRoot(rootStat, recorded)) return;
251
251
  const pending = pendingCreatedRoots.get(localRoot);
252
252
  if (pending && sameManagedRoot(rootStat, pending)) return;
253
+ if (isSettledRecreatedManagedRoot(localRoot, rootStat)) return;
253
254
  throw new Error("Unsafe local:// root");
254
255
  }
255
256
  if (recorded || adoptedExisting) {
@@ -259,6 +260,28 @@ function acceptManagedRootForInit(
259
260
  if (isUnsafeDirectory(rootStat)) throw new Error("Unsafe local:// root");
260
261
  }
261
262
 
263
+ /**
264
+ * A pinned root whose inode changed is still ours when a gjc initializer
265
+ * completed it: the temp sweeper (or `gjc gc`) removed the directory and
266
+ * another process for the same session recreated it, writing a settled
267
+ * migration marker. POSIX owner and write-bit checks on the root and parent
268
+ * protect this exception. Windows changed-inode adoption stays fail-closed
269
+ * until this path validates the replacement's identity and ACLs.
270
+ */
271
+ function isSettledRecreatedManagedRoot(localRoot: string, rootStat: DirectoryIdentityStat): boolean {
272
+ if (process.platform === "win32") return false;
273
+ try {
274
+ assertAdoptedManagedRoot(rootStat);
275
+ assertManagedParentSync(localRoot);
276
+ const marker = path.join(localRoot, LEGACY_MIGRATION_MARKER);
277
+ const markerStat = fsSync.lstatSync(marker);
278
+ if (!markerStat.isFile() || markerStat.isSymbolicLink()) return false;
279
+ return isSettledMigrationMarkerValue(fsSync.readFileSync(marker, "utf8"));
280
+ } catch {
281
+ return false;
282
+ }
283
+ }
284
+
262
285
  const LEGACY_MIGRATION_MARKER = ".gjc-local-legacy-migrated-v1";
263
286
  const MAX_LEGACY_LOCAL_BYTES = 64 * 1024 * 1024;
264
287
 
@@ -714,7 +737,9 @@ function initializeLocalRootSyncWhenLegacyAbsent(options: LocalProtocolOptions,
714
737
  return;
715
738
  }
716
739
  const pending = pendingCreatedRoots.get(localRoot);
717
- if (!(pending && sameManagedRoot(present, pending))) throw new Error("Unsafe local:// root");
740
+ if (!(pending && sameManagedRoot(present, pending)) && !isSettledRecreatedManagedRoot(localRoot, present)) {
741
+ throw new Error("Unsafe local:// root");
742
+ }
718
743
  }
719
744
  }
720
745
  const scratchParent = path.dirname(localRoot);
@@ -21,6 +21,8 @@ export interface InternalResource {
21
21
  size?: number;
22
22
  /** Underlying filesystem path (for debugging, not exposed to agent) */
23
23
  sourcePath?: string;
24
+ /** True when model-facing reads must wrap the selected content as untrusted. */
25
+ untrusted?: boolean;
24
26
  /** Additional notes about resolution */
25
27
  notes?: string[];
26
28
  /**
package/src/lsp/client.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import { isEnoent, isKnownSinkPeerClosedError, logger, untilAborted } from "@gajae-code/utils";
2
+ import { projectSafeSpawnEnv } from "@gajae-code/utils/shell-config";
2
3
  import { formatCrashDiagnosticNotice, writeCrashReport } from "../debug/crash-diagnostics";
3
4
  import { registerResourceOwner, spawnOwnedProcess } from "../runtime/process-lifecycle";
4
5
  import { ToolAbortError, throwIfAborted } from "../tools/tool-errors";
@@ -538,7 +539,7 @@ export async function getOrCreateClient(config: ServerConfig, cwd: string, initT
538
539
  const owner = spawnOwnedProcess([command, ...args], {
539
540
  cwd,
540
541
  stdin: "pipe",
541
- env: env ? { ...Bun.env, ...env } : undefined,
542
+ env: { ...projectSafeSpawnEnv(), ...env },
542
543
  name: `lsp:${config.command}`,
543
544
  });
544
545
  const proc = owner.child;
@@ -3,6 +3,7 @@
3
3
  * Uses Biome's CLI with JSON output instead of LSP (which has stale diagnostics issues).
4
4
  */
5
5
  import path from "node:path";
6
+ import { projectSafeSpawnEnv } from "@gajae-code/utils/shell-config";
6
7
  import type { Diagnostic, DiagnosticSeverity, LinterClient, ServerConfig } from "../../lsp/types";
7
8
 
8
9
  // =============================================================================
@@ -84,6 +85,7 @@ async function runBiome(
84
85
  try {
85
86
  const proc = Bun.spawn([command, ...args], {
86
87
  cwd,
88
+ env: projectSafeSpawnEnv(),
87
89
  stdout: "pipe",
88
90
  stderr: "pipe",
89
91
  windowsHide: true,
@@ -2,6 +2,7 @@
2
2
  * SwiftLint CLI-based linter client.
3
3
  * Parses SwiftLint's JSON reporter output into LSP Diagnostic format.
4
4
  */
5
+ import { projectSafeSpawnEnv } from "@gajae-code/utils/shell-config";
5
6
  import type { Diagnostic, DiagnosticSeverity, LinterClient, ServerConfig } from "../../lsp/types";
6
7
 
7
8
  /** Shape of a single violation from `swiftlint lint --reporter json`. */
@@ -36,6 +37,7 @@ async function runSwiftLint(
36
37
  try {
37
38
  const proc = Bun.spawn([command, ...args], {
38
39
  cwd,
40
+ env: projectSafeSpawnEnv(),
39
41
  stdout: "pipe",
40
42
  stderr: "pipe",
41
43
  windowsHide: true,
package/src/main.ts CHANGED
@@ -5,6 +5,7 @@
5
5
  * createAgentSession() options. The SDK does the heavy lifting.
6
6
  */
7
7
 
8
+ import { existsSync } from "node:fs";
8
9
  import * as fs from "node:fs/promises";
9
10
  import * as os from "node:os";
10
11
  import * as path from "node:path";
@@ -21,14 +22,16 @@ import {
21
22
  VERSION,
22
23
  } from "@gajae-code/utils";
23
24
  import chalk from "chalk";
24
- import type { Args } from "./cli/args";
25
+ import { selectAppendSystemPromptFile } from "./cli/append-system-prompt";
26
+ import { type Args, resolvePlanModelOverride } from "./cli/args";
25
27
  import { processFileArguments } from "./cli/file-processor";
26
28
  import { fetchGithubChannelRelease } from "./cli/github-release";
27
29
  import { buildInitialMessage } from "./cli/initial-message";
28
30
  import { resolveLaunchDisposition } from "./cli/launch-disposition";
29
31
  import { runListModelsCommand } from "./cli/list-models";
30
32
  import { selectSession } from "./cli/session-picker";
31
- import { findConfigFile } from "./config";
33
+ import { getConfigDirs } from "./config";
34
+ import { FileLockAcquireError, FileLockStagingCleanupError } from "./config/file-lock";
32
35
  import {
33
36
  activateModelProfile,
34
37
  applyModelProfileRuntimeBindings,
@@ -74,7 +77,12 @@ import {
74
77
  discoverAuthStorage,
75
78
  } from "./sdk";
76
79
  import { processIncarnation } from "./sdk/broker/process-incarnation";
77
- import { newMasterAttestationEpoch, resolveSessionLocator, SessionIndex } from "./sdk/broker/session-index";
80
+ import {
81
+ newMasterAttestationEpoch,
82
+ resolveSessionLocator,
83
+ SessionIndex,
84
+ type SessionLocatorV2,
85
+ } from "./sdk/broker/session-index";
78
86
  import type { AgentSession } from "./session/agent-session";
79
87
  import { ManagedAppendIdentityMismatchError } from "./session/internal/managed-session-storage";
80
88
  import { SessionMigrationBusyError } from "./session/internal/session-open-errors";
@@ -249,8 +257,6 @@ export function resolveAcpStartupOptions(
249
257
  | "resume"
250
258
  | "sessionDir"
251
259
  | "skills"
252
- | "slow"
253
- | "smol"
254
260
  | "plan"
255
261
  | "systemPrompt"
256
262
  | "thinking"
@@ -291,8 +297,6 @@ export function resolveAcpStartupOptions(
291
297
  ...(parsed.resume ? ["--resume"] : []),
292
298
  ...(parsed.sessionDir ? ["--session-dir"] : []),
293
299
  ...(parsed.skills !== undefined ? ["--skills"] : []),
294
- ...(parsed.slow ? ["--slow"] : []),
295
- ...(parsed.smol ? ["--smol"] : []),
296
300
  ...(parsed.plan ? ["--plan"] : []),
297
301
  ...(parsed.systemPrompt ? ["--system-prompt"] : []),
298
302
  ...(parsed.tmux ? ["--tmux"] : []),
@@ -1446,17 +1450,13 @@ async function maybeAutoChdir(parsed: Args): Promise<void> {
1446
1450
  }
1447
1451
  }
1448
1452
 
1449
- /** Discover APPEND_SYSTEM.md file if no CLI append system prompt was provided */
1453
+ /** User APPEND_SYSTEM.md only. Repository copies are not appended. */
1450
1454
  function discoverAppendSystemPromptFile(): string | undefined {
1451
- const projectPath = findConfigFile("APPEND_SYSTEM.md", { user: false });
1452
- if (projectPath) {
1453
- return projectPath;
1454
- }
1455
- const globalPath = findConfigFile("APPEND_SYSTEM.md", { user: true });
1456
- if (globalPath) {
1457
- return globalPath;
1458
- }
1459
- return undefined;
1455
+ const candidates = getConfigDirs("", { existingOnly: false }).flatMap(entry => {
1456
+ const filePath = path.join(entry.path, "APPEND_SYSTEM.md");
1457
+ return existsSync(filePath) ? [{ level: entry.level, path: filePath }] : [];
1458
+ });
1459
+ return selectAppendSystemPromptFile(candidates);
1460
1460
  }
1461
1461
 
1462
1462
  async function buildSessionOptions(
@@ -1617,6 +1617,7 @@ type RunPrintMode = (session: AgentSession, options: PrintModeOptions) => Promis
1617
1617
  export interface RunRootCommandDependencies {
1618
1618
  createAgentSession?: typeof createAgentSession;
1619
1619
  createSessionManager?: typeof createSessionManager;
1620
+ registerDirectSession?: typeof registerDirectSession;
1620
1621
  discoverAuthStorage?: typeof discoverAuthStorage;
1621
1622
  resolveStartupAuthConfig?: typeof resolveStartupAuthConfig;
1622
1623
  runAcpMode?: (options?: { agentDir?: string }) => Promise<void>;
@@ -1663,84 +1664,278 @@ interface DirectSessionRegistrationDependencies {
1663
1664
  masterRole?: { ownerSessionId: string; attestationEpoch: string };
1664
1665
  }
1665
1666
 
1667
+ /**
1668
+ * Handle for a direct CLI session's broker-index registration.
1669
+ *
1670
+ * A direct session registers once at startup, but `/new`, `/resume` and forks
1671
+ * replace the manager's session id inside the same process. The index row is
1672
+ * keyed on that id, so without a follow-up the successor is never indexed and the
1673
+ * predecessor stays live for as long as this pid is alive (the broker keeps
1674
+ * heartbeating a live pid). `syncIdentity` moves the registration to whichever
1675
+ * id the manager the session has adopted currently reports.
1676
+ */
1677
+ export interface DirectSessionRegistration {
1678
+ /**
1679
+ * Register the current manager's session id and release the one it replaced.
1680
+ *
1681
+ * `adoptedManager` rebinds the registration to the manager the session now
1682
+ * owns: a bounded cold-sidecar fork adopts its own forked manager before it
1683
+ * emits `session_switch`, so the wired handler must hand that manager over or
1684
+ * the sync keeps reading the startup manager's predecessor id.
1685
+ */
1686
+ syncIdentity(adoptedManager?: DirectSessionManager): Promise<void>;
1687
+ }
1688
+
1689
+ const NOOP_DIRECT_SESSION_REGISTRATION: DirectSessionRegistration = { syncIdentity: async () => {} };
1690
+
1691
+ /**
1692
+ * Appends allowed for one successor registration before the switch gives up and
1693
+ * keeps the predecessor registered. `SessionIndex.append` takes the shared index
1694
+ * lock independently per append, so a peer holding it is a transient failure.
1695
+ */
1696
+ const DIRECT_SESSION_REGISTRATION_ATTEMPTS = 3;
1697
+
1666
1698
  /** Register a durable direct CLI session while leaving ephemeral sessions lock-free. */
1667
1699
  export async function registerDirectSession(
1668
1700
  sessionManager: DirectSessionManager | undefined,
1669
1701
  agentDir: string,
1670
1702
  lifecycleRequestId: string | undefined,
1671
1703
  dependencies: DirectSessionRegistrationDependencies = {},
1672
- ): Promise<void> {
1673
- const directSessionId = directSessionRegistrationId(sessionManager, lifecycleRequestId);
1674
- if (!directSessionId || !sessionManager) return;
1704
+ ): Promise<DirectSessionRegistration> {
1705
+ const initialSessionId = directSessionRegistrationId(sessionManager, lifecycleRequestId);
1706
+ if (!initialSessionId || !sessionManager) return NOOP_DIRECT_SESSION_REGISTRATION;
1675
1707
  const sessionIndex = dependencies.createIndex?.(agentDir) ?? new SessionIndex(agentDir);
1676
- const locator = await resolveSessionLocator(sessionManager.getCwd(), agentDir);
1677
1708
  const directSessionIncarnation = (dependencies.processIncarnation ?? processIncarnation)(process.pid);
1678
- await sessionIndex.append({
1679
- type: "host_registered",
1680
- sessionId: directSessionId,
1681
- locator,
1682
- endpointGeneration: 0,
1683
- pid: process.pid,
1684
- ...(directSessionIncarnation ? { processIncarnation: directSessionIncarnation } : {}),
1685
- ...(dependencies.masterRole && directSessionIncarnation
1686
- ? {
1687
- masterRole: {
1688
- version: 2 as const,
1689
- ownerSessionId: dependencies.masterRole.ownerSessionId,
1690
- launchPid: process.pid,
1691
- launchProcessIncarnation: directSessionIncarnation,
1692
- role: "master" as const,
1693
- attestationEpoch: dependencies.masterRole.attestationEpoch,
1694
- },
1695
- }
1696
- : {}),
1697
- });
1698
- if (locator.worktreeRoot !== null) {
1709
+ // The session can adopt a different manager mid-process (bounded cold-sidecar
1710
+ // fork), so every read goes through the manager that is current now.
1711
+ let activeManager = sessionManager;
1712
+ let registeredLocator: SessionLocatorV2 | undefined;
1713
+ let registeredSessionId: string | undefined;
1714
+ // Predecessors whose `host_unregistered` append has not succeeded yet. Once the
1715
+ // successor is registered the old id is no longer `registeredSessionId`, so a
1716
+ // release that failed here would otherwise be lost and keep the predecessor live
1717
+ // in the index for the rest of the process. An entry is dropped as soon as its id
1718
+ // is current again: replaying it would retire a session this process reactivated.
1719
+ const pendingPredecessorReleases: Array<{ sessionId: string; locator: SessionLocatorV2 | undefined }> = [];
1720
+ const forgetPendingRelease = (sessionId: string): void => {
1721
+ for (let index = pendingPredecessorReleases.length - 1; index >= 0; index--) {
1722
+ if (pendingPredecessorReleases[index]?.sessionId === sessionId) pendingPredecessorReleases.splice(index, 1);
1723
+ }
1724
+ };
1725
+
1726
+ const register = async (sessionId: string): Promise<void> => {
1727
+ const locator = await resolveSessionLocator(activeManager.getCwd(), agentDir);
1699
1728
  try {
1700
- await releaseLaunchWorktreeReservationAfterRegistration(agentDir, locator.worktreeRoot);
1729
+ await sessionIndex.append({
1730
+ type: "host_registered",
1731
+ sessionId,
1732
+ locator,
1733
+ endpointGeneration: 0,
1734
+ pid: process.pid,
1735
+ ...(directSessionIncarnation ? { processIncarnation: directSessionIncarnation } : {}),
1736
+ ...(dependencies.masterRole && directSessionIncarnation
1737
+ ? {
1738
+ masterRole: {
1739
+ version: 2 as const,
1740
+ ownerSessionId: dependencies.masterRole.ownerSessionId,
1741
+ launchPid: process.pid,
1742
+ launchProcessIncarnation: directSessionIncarnation,
1743
+ role: "master" as const,
1744
+ attestationEpoch: dependencies.masterRole.attestationEpoch,
1745
+ },
1746
+ }
1747
+ : {}),
1748
+ });
1701
1749
  } catch (error) {
1702
- logger.warn("Failed to release launch worktree reservation after host registration", {
1750
+ // A lock-release refusal after the row is durable is a real startup failure:
1751
+ // the next process would observe a registered session whose lock generation
1752
+ // this process can no longer prove. Surface it instead of adopting the row.
1753
+ if (error instanceof Error && /^Failed to release file lock:/u.test(error.message)) throw error;
1754
+ // `SessionIndex.append` can reject after its row is already durable (awaited
1755
+ // post-append maintenance), so a rejection does not prove the registration is
1756
+ // missing. Adopt the id when this process already owns a live row for it, so a
1757
+ // later switch still releases it; otherwise the switch keeps failing and the
1758
+ // previous identity stays registered.
1759
+ if (!isRegisteredLive(sessionId)) throw error;
1760
+ logger.warn("Direct session registration append failed after its row was durable", {
1703
1761
  error: error instanceof Error ? error.message : String(error),
1704
1762
  });
1705
1763
  }
1706
- }
1707
- (dependencies.registerPostmortem ?? postmortem.register)("direct-session-index", async _reason => {
1764
+ // This id is current again: any queued release for it is stale.
1765
+ forgetPendingRelease(sessionId);
1766
+ registeredLocator = locator;
1767
+ registeredSessionId = sessionId;
1768
+ };
1769
+ const isRegisteredLive = (sessionId: string): boolean => {
1770
+ try {
1771
+ return sessionIndex
1772
+ .listSessions()
1773
+ .sessions.some(row => row.sessionId === sessionId && row.pid === process.pid && row.live === true);
1774
+ } catch {
1775
+ return false;
1776
+ }
1777
+ };
1778
+ const unregister = async (sessionId: string, locator: SessionLocatorV2 | undefined): Promise<void> => {
1779
+ if (!locator) return;
1708
1780
  await sessionIndex.append({
1709
1781
  type: "host_unregistered",
1710
- sessionId: directSessionId,
1782
+ sessionId,
1711
1783
  locator,
1712
1784
  endpointGeneration: 0,
1713
1785
  pid: process.pid,
1714
1786
  });
1787
+ };
1788
+
1789
+ /**
1790
+ * Release queued predecessors in order. A failed append keeps its entry, so the
1791
+ * next sync or the teardown handler retries it; the failure itself is a warning
1792
+ * rather than a failed identity switch, matching the best-effort release of the
1793
+ * launch worktree reservation below.
1794
+ */
1795
+ const releasePendingPredecessors = async (): Promise<void> => {
1796
+ while (pendingPredecessorReleases.length > 0) {
1797
+ const predecessor = pendingPredecessorReleases[0]!;
1798
+ // Defence in depth for the reactivation order: never retire the id this
1799
+ // process currently publishes as its own.
1800
+ if (predecessor.sessionId === registeredSessionId) {
1801
+ pendingPredecessorReleases.shift();
1802
+ continue;
1803
+ }
1804
+ try {
1805
+ await unregister(predecessor.sessionId, predecessor.locator);
1806
+ } catch (error) {
1807
+ logger.warn("Failed to release a superseded session registration", {
1808
+ error: error instanceof Error ? error.message : String(error),
1809
+ });
1810
+ return;
1811
+ }
1812
+ pendingPredecessorReleases.shift();
1813
+ }
1814
+ };
1815
+
1816
+ /**
1817
+ * Register `sessionId`, retrying a bounded number of times so a transient
1818
+ * index-lock loss self-heals instead of leaving the successor unregistered
1819
+ * until the next identity change.
1820
+ */
1821
+ const registerWithRetry = async (sessionId: string): Promise<void> => {
1822
+ for (let attempt = 1; ; attempt++) {
1823
+ try {
1824
+ await register(sessionId);
1825
+ return;
1826
+ } catch (error) {
1827
+ // Only lock contention self-heals. A refused release is an identity
1828
+ // verdict; retrying it would mask a committed registration whose lock
1829
+ // generation no longer matches.
1830
+ const code = (error as NodeJS.ErrnoException | undefined)?.code;
1831
+ const transient =
1832
+ code === "sharing_violation" ||
1833
+ code === "access_denied" ||
1834
+ code === "EBUSY" ||
1835
+ code === "EPERM" ||
1836
+ code === "EACCES" ||
1837
+ code === "ENOTEMPTY" ||
1838
+ code === "removal_record_busy";
1839
+ if (!transient || attempt >= DIRECT_SESSION_REGISTRATION_ATTEMPTS) throw error;
1840
+ await Bun.sleep(50 * attempt);
1841
+ }
1842
+ }
1843
+ };
1844
+
1845
+ /**
1846
+ * Move the registration to the id `activeManager` currently reports.
1847
+ *
1848
+ * A successor append that cannot land after its bounded retries keeps the
1849
+ * predecessor registered and the successor unregistered, so every later call
1850
+ * (any session event, or teardown) retries the successor instead of leaving it
1851
+ * unindexed for the life of the process.
1852
+ */
1853
+ const syncToActiveManager = async (): Promise<void> => {
1854
+ const currentId = activeManager.getSessionFile() === undefined ? undefined : activeManager.getSessionId();
1855
+ // A predecessor left over from a failed release is retried here even when
1856
+ // the identity itself did not change.
1857
+ if (!currentId || currentId === registeredSessionId) {
1858
+ await releasePendingPredecessors();
1859
+ return;
1860
+ }
1861
+ // The predecessor is released under the locator it was registered with, and
1862
+ // only after the successor's own append succeeded.
1863
+ const predecessorId = registeredSessionId;
1864
+ const predecessorLocator = registeredLocator;
1865
+ await registerWithRetry(currentId);
1866
+ if (predecessorId) {
1867
+ pendingPredecessorReleases.push({ sessionId: predecessorId, locator: predecessorLocator });
1868
+ await releasePendingPredecessors();
1869
+ }
1870
+ };
1871
+
1872
+ await register(initialSessionId);
1873
+ if (registeredLocator?.worktreeRoot != null) {
1874
+ try {
1875
+ await releaseLaunchWorktreeReservationAfterRegistration(agentDir, registeredLocator.worktreeRoot);
1876
+ } catch (error) {
1877
+ logger.warn("Failed to release launch worktree reservation after host registration", {
1878
+ error: error instanceof Error ? error.message : String(error),
1879
+ });
1880
+ }
1881
+ }
1882
+ // Teardown registers the id the session is on now — a post-switch append that
1883
+ // failed would otherwise leave the active transcript unindexed — then releases
1884
+ // everything this process still holds.
1885
+ (dependencies.registerPostmortem ?? postmortem.register)("direct-session-index", async _reason => {
1886
+ try {
1887
+ await syncToActiveManager();
1888
+ } catch (error) {
1889
+ logger.warn("Failed to register the active direct session during teardown", {
1890
+ error: error instanceof Error ? error.message : String(error),
1891
+ });
1892
+ }
1893
+ if (registeredSessionId) await unregister(registeredSessionId, registeredLocator);
1894
+ await releasePendingPredecessors();
1715
1895
  });
1716
- }
1717
1896
 
1718
- export interface ModelRoleOverrides {
1719
- smol?: string;
1720
- slow?: string;
1721
- plan?: string;
1897
+ let syncing: Promise<void> = Promise.resolve();
1898
+ return {
1899
+ syncIdentity: adoptedManager => {
1900
+ // Serialized: a rapid /new /new must register in order and never interleave.
1901
+ const run = syncing.then(async () => {
1902
+ // An ephemeral manager owns nothing the index could fence, so the tracked
1903
+ // manager survives an adoption that has no durable transcript.
1904
+ if (adoptedManager?.getSessionFile() !== undefined) activeManager = adoptedManager;
1905
+ await syncToActiveManager();
1906
+ });
1907
+ syncing = run.catch(() => {});
1908
+ return run;
1909
+ },
1910
+ };
1722
1911
  }
1723
1912
 
1724
- /**
1725
- * Resolve the ephemeral `smol`/`slow`/`plan` model-role overrides.
1726
- *
1727
- * Precedence per role is CLI flag > documented `GJC_*_MODEL` > legacy
1728
- * `PI_*_MODEL`, matching the repo-wide GJC-first/PI-fallback convention.
1729
- * Resolution reads the process environment via `$pickenv`, which trims values
1730
- * and treats empty/whitespace as unset; it is deliberately kept separate from
1731
- * credential env resolution (`$credentialEnv`/`$pickCredentialEnv`). The
1732
- * function is pure and stateless, so it reads fresh each call and a later
1733
- * invocation never inherits an earlier one's values.
1734
- */
1735
- export function resolveModelRoleOverrides(parsed: Pick<Args, "smol" | "slow" | "plan">): ModelRoleOverrides {
1736
- const overrides: ModelRoleOverrides = {};
1737
- const smol = parsed.smol ?? $pickenv("GJC_SMOL_MODEL", "PI_SMOL_MODEL");
1738
- const slow = parsed.slow ?? $pickenv("GJC_SLOW_MODEL", "PI_SLOW_MODEL");
1739
- const plan = parsed.plan ?? $pickenv("GJC_PLAN_MODEL", "PI_PLAN_MODEL");
1740
- if (smol) overrides.smol = smol;
1741
- if (slow) overrides.slow = slow;
1742
- if (plan) overrides.plan = plan;
1743
- return overrides;
1913
+ function directSessionIndexLockFailureMessage(error: unknown, agentDir: string): string | undefined {
1914
+ if (error instanceof FileLockStagingCleanupError) {
1915
+ return (
1916
+ "Session index lock staging cleanup failed; acquisition stopped instead of retrying with another staging directory.\n" +
1917
+ `Lock: ${error.lockPath}\nStaging: ${error.stagingPath}\nCode: ${error.code ?? "unknown"}\n${error.message}`
1918
+ );
1919
+ }
1920
+ if (error instanceof FileLockAcquireError) {
1921
+ const code = error.publicationFailureCode ?? error.code;
1922
+ if (error.publicationFailureCode === "sharing_violation" || error.holderIsLive)
1923
+ return (
1924
+ "Session index is busy (validated lock-publication contention). Retry in a few seconds.\n" +
1925
+ `Lock: ${error.lockPath}\nCode: ${code}\n${error.message}`
1926
+ );
1927
+ return (
1928
+ "Session index lock acquisition failed; no session was registered.\n" +
1929
+ `Lock: ${error.lockPath}\nCode: ${code}\nHolder: ${error.holder}`
1930
+ );
1931
+ }
1932
+ if (!(error instanceof Error) || !/^Failed to publish file lock:/u.test(error.message)) return undefined;
1933
+ const code =
1934
+ (error as NodeJS.ErrnoException).code ??
1935
+ error.message.match(/^Failed to publish file lock: ([a-z0-9_]+)/u)?.[1] ??
1936
+ "unknown";
1937
+ const lockPath = path.join(agentDir, "sdk", "sessions", "index.jsonl.lock");
1938
+ return `Session index lock publication failed; no session was registered.\nLock: ${lockPath}\nCode: ${code}\n${error.message}`;
1744
1939
  }
1745
1940
 
1746
1941
  /**
@@ -2002,19 +2197,12 @@ export async function runRootCommand(
2002
2197
  // Initialize discovery system with settings for provider persistence
2003
2198
  logger.time("initializeWithSettings", initializeWithSettings, settingsInstance);
2004
2199
 
2005
- // Apply model role overrides from CLI args or env vars (ephemeral, not persisted).
2006
- // Precedence per role: CLI flag > documented GJC_*_MODEL > legacy PI_*_MODEL.
2007
- const roleOverrides = resolveModelRoleOverrides(parsedArgs);
2008
- if (roleOverrides.smol || roleOverrides.slow || roleOverrides.plan) {
2009
- settingsInstance.overrideModelRoles({
2010
- ...(roleOverrides.smol ? { smol: roleOverrides.smol } : {}),
2011
- ...(roleOverrides.slow ? { slow: roleOverrides.slow } : {}),
2012
- ...(roleOverrides.plan ? { plan: roleOverrides.plan } : {}),
2013
- });
2014
- }
2200
+ // Plan mode switches to the `plan` model role; --plan / GJC_PLAN_MODEL override it
2201
+ // for this run only (ephemeral, not persisted).
2202
+ const planModelOverride = resolvePlanModelOverride(parsedArgs);
2203
+ if (planModelOverride) settingsInstance.overrideModelRoles({ plan: planModelOverride });
2015
2204
  // Apply --clipboard-transport / --clipboard-ssh-host as ephemeral runtime
2016
- // overrides (CLI > persisted config > "auto" default). Not persisted —
2017
- // mirrors the role-override precedence above.
2205
+ // overrides (CLI > persisted config > "auto" default). Not persisted.
2018
2206
  if (parsedArgs.clipboardTransport) {
2019
2207
  settingsInstance.override("clipboard.transport", parsedArgs.clipboardTransport);
2020
2208
  }
@@ -2302,15 +2490,64 @@ export async function runRootCommand(
2302
2490
  // Register a resumed direct session before constructing the agent: GC holds the
2303
2491
  // same index lock while deleting artifacts, so startup and deletion are fenced.
2304
2492
  // Ephemeral sessions have no durable artifacts or liveness authority to fence.
2305
- await registerDirectSession(sessionManager, settingsInstance.getAgentDir(), process.env.GJC_LIFECYCLE_REQUEST_ID, {
2306
- masterRole: masterModeContext
2307
- ? {
2308
- ownerSessionId: masterModeContext.ownerSessionId,
2309
- attestationEpoch: masterModeContext.attestationEpoch,
2493
+ const sessionAgentDir = settingsInstance.getAgentDir();
2494
+ let directSessionRegistration: DirectSessionRegistration;
2495
+ try {
2496
+ directSessionRegistration = await (deps.registerDirectSession ?? registerDirectSession)(
2497
+ sessionManager,
2498
+ sessionAgentDir,
2499
+ process.env.GJC_LIFECYCLE_REQUEST_ID,
2500
+ {
2501
+ masterRole: masterModeContext
2502
+ ? {
2503
+ ownerSessionId: masterModeContext.ownerSessionId,
2504
+ attestationEpoch: masterModeContext.attestationEpoch,
2505
+ }
2506
+ : undefined,
2507
+ },
2508
+ );
2509
+ } catch (error) {
2510
+ const message = directSessionIndexLockFailureMessage(error, sessionAgentDir);
2511
+ if (message === undefined) throw error;
2512
+ process.stderr.write(`${message}\n`);
2513
+ if (!deps.suppressProcessExit) process.exitCode = 1;
2514
+ await sessionManager?.close().catch(closeError => {
2515
+ logger.warn("Failed to close session manager after session-index registration failure", {
2516
+ error: closeError instanceof Error ? closeError.message : String(closeError),
2517
+ });
2518
+ });
2519
+ authStorage.close();
2520
+ stopThemeWatcher();
2521
+ await postmortem.cleanup();
2522
+ return;
2523
+ }
2524
+ // /new, /resume and forks replace the manager's session id in this same process.
2525
+ // Follow it, or the broker index keeps listing the predecessor as live forever.
2526
+ //
2527
+ // The extension context is not a usable source here: `ExtensionRunner` is
2528
+ // constructed with the startup manager and `createContext()` keeps handing that
2529
+ // facade back, while a bounded cold-sidecar fork replaces only the session's own
2530
+ // manager. Read the live session at dispatch time instead.
2531
+ let liveSession: AgentSession | undefined;
2532
+ sessionOptions.extensions = [
2533
+ ...(sessionOptions.extensions ?? []),
2534
+ pi => {
2535
+ const sync = async (): Promise<void> => {
2536
+ try {
2537
+ await directSessionRegistration.syncIdentity(liveSession?.sessionManager);
2538
+ } catch (error) {
2539
+ logger.warn("Failed to move direct session registration to the new session id", {
2540
+ error: error instanceof Error ? error.message : String(error),
2541
+ });
2310
2542
  }
2311
- : undefined,
2312
- });
2313
-
2543
+ };
2544
+ pi.on("session_switch", sync);
2545
+ pi.on("session_branch", sync);
2546
+ // A transition whose append failed is retried at the next turn boundary, not
2547
+ // only on another identity change.
2548
+ pi.on("agent_end", sync);
2549
+ },
2550
+ ];
2314
2551
  const createAgentSessionImpl = deps.createAgentSession ?? createAgentSession;
2315
2552
  const createSession: CreateSessionForMain = async (options, context): Promise<CreateAgentSessionResult> => {
2316
2553
  const result = await logger.time("createAgentSession", createAgentSessionImpl, options);
@@ -2346,6 +2583,7 @@ export async function runRootCommand(
2346
2583
  startDeferredMemoryBackend,
2347
2584
  eventBus,
2348
2585
  } = await createSession(sessionOptions, { skipPostCreateModelRefresh: hasRootStartupProfile });
2586
+ liveSession = session;
2349
2587
  applyCliRuntimeApiKeyOverride(authStorage, parsedArgs.apiKey, session.model);
2350
2588
  // Herdr integration: report gjc lifecycle state when running in a Herdr pane.
2351
2589
  installHerdrReporter(listener => session.subscribe(listener));