@gajae-code/coding-agent 0.18.11 → 0.18.12

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 (270) hide show
  1. package/CHANGELOG.md +293 -0
  2. package/dist/types/async/job-manager.d.ts +7 -3
  3. package/dist/types/async/result-formatting.d.ts +2 -0
  4. package/dist/types/autoresearch/git.d.ts +1 -1
  5. package/dist/types/capability/fs.d.ts +2 -2
  6. package/dist/types/cli/accounts-cli.d.ts +2 -2
  7. package/dist/types/cli/doctor/install-repairs.d.ts +4 -1
  8. package/dist/types/cli/install-activation.d.ts +10 -4
  9. package/dist/types/cli/public-command-errors.d.ts +1 -0
  10. package/dist/types/cli/update-cli.d.ts +14 -1
  11. package/dist/types/cli-ordinary.d.ts +2 -2
  12. package/dist/types/commands/accounts.d.ts +2 -1
  13. package/dist/types/commands/migrate.d.ts +1 -1
  14. package/dist/types/commands/sdk.d.ts +5 -0
  15. package/dist/types/commit/changelog/index.d.ts +1 -0
  16. package/dist/types/config/autorouting-tier-map.d.ts +30 -0
  17. package/dist/types/config/config-hot-reload-trust.d.ts +12 -0
  18. package/dist/types/config/config-hot-reload.d.ts +39 -0
  19. package/dist/types/config/file-lock.d.ts +8 -1
  20. package/dist/types/config/model-discovery-manager.d.ts +2 -0
  21. package/dist/types/config/model-profile-activation.d.ts +62 -6
  22. package/dist/types/config/model-registry.d.ts +56 -5
  23. package/dist/types/config/model-resolver.d.ts +2 -0
  24. package/dist/types/config/models-config-schema.d.ts +10 -0
  25. package/dist/types/config/settings-schema.d.ts +46 -3
  26. package/dist/types/config/settings.d.ts +33 -1
  27. package/dist/types/dap/config.d.ts +8 -4
  28. package/dist/types/debug/system-info.d.ts +6 -1
  29. package/dist/types/discovery/mcp-compat.d.ts +3 -1
  30. package/dist/types/edit/modes/patch.d.ts +5 -0
  31. package/dist/types/edit/modes/replace.d.ts +3 -0
  32. package/dist/types/exec/bash-executor.d.ts +12 -0
  33. package/dist/types/gjc-runtime/managed-owner-admission.d.ts +3 -0
  34. package/dist/types/gjc-runtime/session-state-lock.d.ts +2 -0
  35. package/dist/types/gjc-runtime/state-runtime.d.ts +0 -2
  36. package/dist/types/migrate/adapters/codex.d.ts +2 -1
  37. package/dist/types/migrate/adapters/index.d.ts +10 -1
  38. package/dist/types/migrate/executor.d.ts +3 -1
  39. package/dist/types/migrate/types.d.ts +15 -3
  40. package/dist/types/modes/components/settings-selector.d.ts +6 -0
  41. package/dist/types/modes/controllers/selector-controller.d.ts +2 -0
  42. package/dist/types/modes/theme/theme.d.ts +1 -0
  43. package/dist/types/runtime-mcp/content-limits.d.ts +4 -0
  44. package/dist/types/runtime-mcp/manager.d.ts +7 -0
  45. package/dist/types/runtime-mcp/plugin-mcp-name-filter.d.ts +22 -0
  46. package/dist/types/runtime-mcp/tool-bridge.d.ts +4 -0
  47. package/dist/types/sdk/broker/broker-exit.d.ts +2 -2
  48. package/dist/types/sdk/broker/broker.d.ts +7 -1
  49. package/dist/types/sdk/broker/daemon-entry.d.ts +11 -1
  50. package/dist/types/sdk/broker/doctor-restart.d.ts +1 -1
  51. package/dist/types/sdk/broker/ensure.d.ts +136 -7
  52. package/dist/types/sdk/broker/hop.d.ts +36 -0
  53. package/dist/types/sdk/broker/lifecycle-ledger.d.ts +18 -10
  54. package/dist/types/sdk/broker/lock-host-identity.d.ts +5 -0
  55. package/dist/types/sdk/broker/session-index.d.ts +4 -1
  56. package/dist/types/sdk/bus/chat-command-policy.d.ts +2 -1
  57. package/dist/types/sdk/bus/index.d.ts +23 -0
  58. package/dist/types/sdk/bus/reconciliation-store.d.ts +10 -0
  59. package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +3 -1
  60. package/dist/types/sdk/cli/session-cli.d.ts +0 -1
  61. package/dist/types/sdk/host/session-runtime.d.ts +32 -0
  62. package/dist/types/sdk/prompt-deadline-flush.d.ts +42 -4
  63. package/dist/types/sdk/prompt-deadline-manager.d.ts +2 -0
  64. package/dist/types/sdk/protocol/adapter-validation.d.ts +10 -4
  65. package/dist/types/sdk/reconciliation-extensions.d.ts +1 -1
  66. package/dist/types/sdk/session-list.d.ts +1 -1
  67. package/dist/types/session/agent-session.d.ts +58 -2
  68. package/dist/types/session/internal/managed-session-scope.d.ts +2 -2
  69. package/dist/types/session/internal/managed-session-storage.d.ts +1 -1
  70. package/dist/types/session/internal/native-publish-outcome.d.ts +1 -1
  71. package/dist/types/session/messages.d.ts +5 -1
  72. package/dist/types/session/session-manager.d.ts +13 -3
  73. package/dist/types/session/startup-auth-config.d.ts +24 -2
  74. package/dist/types/session/streaming-output.d.ts +11 -0
  75. package/dist/types/session/terminal-abort.d.ts +3 -1
  76. package/dist/types/session-import/service.d.ts +1 -1
  77. package/dist/types/skill-state/workflow-mutation-guard.d.ts +7 -3
  78. package/dist/types/slash-commands/helpers/provider-auth-status.d.ts +8 -0
  79. package/dist/types/task/executor.d.ts +19 -2
  80. package/dist/types/tools/ast-edit-write-path.d.ts +10 -0
  81. package/dist/types/tools/atomic-file-write.d.ts +9 -7
  82. package/dist/types/tools/bash-allowed-prefixes.d.ts +11 -0
  83. package/dist/types/tools/descriptors.d.ts +2 -0
  84. package/dist/types/tools/fetch.d.ts +1 -1
  85. package/dist/types/tools/index.d.ts +2 -0
  86. package/dist/types/tools/python.d.ts +1 -1
  87. package/dist/types/tools/report-issue.d.ts +41 -0
  88. package/dist/types/tools/subagent.d.ts +23 -0
  89. package/dist/types/web/scrapers/types.d.ts +8 -1
  90. package/dist/types/web/search/providers/insane.d.ts +7 -1
  91. package/dist/types/web/search/providers/searxng.d.ts +5 -0
  92. package/package.json +8 -8
  93. package/scripts/build-binary.ts +2 -1
  94. package/scripts/capture-subagent-compact-panel-showcase.ts +105 -0
  95. package/scripts/generate-docs-index.ts +8 -1
  96. package/scripts/generate-sdk-operation-inventory.ts +22 -4
  97. package/src/async/job-manager.ts +356 -56
  98. package/src/async/result-formatting.ts +40 -0
  99. package/src/autoresearch/git.ts +9 -2
  100. package/src/autoresearch/prompts/prompt-setup.md +1 -1
  101. package/src/capability/fs.ts +27 -19
  102. package/src/capability/index.ts +2 -2
  103. package/src/cli/accounts-cli.ts +176 -8
  104. package/src/cli/auth-broker-cli.ts +21 -11
  105. package/src/cli/doctor/install-repairs.ts +35 -5
  106. package/src/cli/fast-help.ts +1 -1
  107. package/src/cli/install-activation.ts +47 -4
  108. package/src/cli/migrate-cli.ts +7 -5
  109. package/src/cli/public-command-errors.ts +4 -0
  110. package/src/cli/root-flags.ts +2 -2
  111. package/src/cli/stats-cli.ts +9 -5
  112. package/src/cli/update-cli.ts +224 -7
  113. package/src/cli-ordinary.ts +7 -2
  114. package/src/commands/accounts.ts +10 -5
  115. package/src/commands/migrate.ts +3 -3
  116. package/src/commands/sdk.ts +199 -42
  117. package/src/commands/ultragoal.ts +16 -8
  118. package/src/commit/changelog/index.ts +39 -1
  119. package/src/commit/message.ts +10 -3
  120. package/src/config/autorouting-tier-map.ts +30 -0
  121. package/src/config/config-hot-reload-trust.ts +148 -0
  122. package/src/config/config-hot-reload.ts +734 -0
  123. package/src/config/file-lock.ts +216 -56
  124. package/src/config/model-bindings-applier.ts +85 -47
  125. package/src/config/model-discovery-manager.ts +10 -0
  126. package/src/config/model-profile-activation.ts +716 -113
  127. package/src/config/model-registry.ts +1334 -158
  128. package/src/config/model-resolver.ts +3 -1
  129. package/src/config/models-config-schema.ts +6 -0
  130. package/src/config/settings-schema.ts +60 -3
  131. package/src/config/settings.ts +458 -24
  132. package/src/coordinator-mcp/policy.ts +26 -1
  133. package/src/crash/sanitize.ts +4 -0
  134. package/src/customization/import.ts +3 -1
  135. package/src/dap/config.ts +31 -11
  136. package/src/dap/defaults.json +1 -1
  137. package/src/debug/crash-diagnostics.ts +2 -1
  138. package/src/debug/report-bundle.ts +19 -0
  139. package/src/debug/system-info.ts +8 -3
  140. package/src/defaults/gjc/extensions/grok-cli-vendor/src/shared/base-url.ts +21 -7
  141. package/src/discovery/agents-md.ts +1 -1
  142. package/src/discovery/helpers.ts +1 -2
  143. package/src/discovery/mcp-compat.ts +120 -17
  144. package/src/edit/apply-patch/index.ts +12 -1
  145. package/src/edit/index.ts +13 -17
  146. package/src/edit/modes/patch.ts +187 -8
  147. package/src/edit/modes/replace.ts +136 -0
  148. package/src/eval/js/context-manager.ts +29 -3
  149. package/src/eval/js/worker-entry.ts +51 -18
  150. package/src/eval/py/executor.ts +14 -4
  151. package/src/eval/py/runner.py +144 -5
  152. package/src/exec/bash-executor.ts +81 -4
  153. package/src/exec/bash-shell-guardian.ts +4 -1
  154. package/src/exec/bash-shell-supervisor.ts +8 -4
  155. package/src/exec/isolated-shell.ts +5 -1
  156. package/src/export/html/index.ts +46 -6
  157. package/src/extensibility/custom-tools/wrapper.ts +3 -1
  158. package/src/extensibility/extensions/loader.ts +8 -1
  159. package/src/extensibility/plugins/marketplace/fetcher.ts +2 -2
  160. package/src/gjc-runtime/gc-runtime.ts +33 -6
  161. package/src/gjc-runtime/launch-tmux.ts +7 -2
  162. package/src/gjc-runtime/launch-worktree.test.ts +148 -0
  163. package/src/gjc-runtime/launch-worktree.ts +101 -14
  164. package/src/gjc-runtime/managed-owner-admission.ts +46 -2
  165. package/src/gjc-runtime/session-resolution.ts +38 -6
  166. package/src/gjc-runtime/session-state-lock.ts +59 -14
  167. package/src/gjc-runtime/state-runtime.ts +23 -19
  168. package/src/hooks/native-skill-hook.ts +14 -11
  169. package/src/internal-urls/docs-index.generated.ts +14 -13
  170. package/src/internal-urls/local-protocol.ts +142 -8
  171. package/src/lsp/index.ts +29 -0
  172. package/src/lsp/workspace-path.ts +23 -0
  173. package/src/migrate/action-planner.ts +57 -0
  174. package/src/migrate/adapters/claude-code.ts +23 -4
  175. package/src/migrate/adapters/codex.ts +22 -4
  176. package/src/migrate/adapters/index.ts +22 -4
  177. package/src/migrate/adapters/opencode.ts +6 -1
  178. package/src/migrate/executor.ts +84 -7
  179. package/src/migrate/report.ts +19 -2
  180. package/src/migrate/types.ts +14 -4
  181. package/src/modes/DESIGN.md +51 -0
  182. package/src/modes/acp/permission-mode.ts +18 -1
  183. package/src/modes/components/assistant-message.ts +55 -1
  184. package/src/modes/components/settings-selector.ts +103 -37
  185. package/src/modes/controllers/command-controller.ts +38 -2
  186. package/src/modes/controllers/plan-mode-controller.ts +17 -7
  187. package/src/modes/controllers/selector-controller.ts +62 -6
  188. package/src/modes/interactive-mode.ts +68 -0
  189. package/src/modes/theme/theme.ts +15 -0
  190. package/src/prompts/tools/debug.md +1 -1
  191. package/src/prompts/tools/read.md +1 -1
  192. package/src/prompts/tools/report-issue.md +13 -0
  193. package/src/prompts/tools/task.md +1 -1
  194. package/src/runtime-credential-selector.ts +2 -2
  195. package/src/runtime-mcp/content-limits.ts +7 -0
  196. package/src/runtime-mcp/manager-pool.test.ts +5 -4
  197. package/src/runtime-mcp/manager.ts +32 -0
  198. package/src/runtime-mcp/oauth-flow.ts +32 -6
  199. package/src/runtime-mcp/plugin-mcp-name-filter.ts +62 -0
  200. package/src/runtime-mcp/render.ts +16 -1
  201. package/src/runtime-mcp/tool-bridge.ts +126 -17
  202. package/src/sdk/broker/broker-exit.ts +8 -5
  203. package/src/sdk/broker/broker.ts +342 -82
  204. package/src/sdk/broker/daemon-entry.ts +338 -116
  205. package/src/sdk/broker/doctor-restart.ts +2 -1
  206. package/src/sdk/broker/ensure.ts +1143 -265
  207. package/src/sdk/broker/hop.ts +274 -0
  208. package/src/sdk/broker/lifecycle-ledger.ts +35 -11
  209. package/src/sdk/broker/lock-host-identity.ts +29 -0
  210. package/src/sdk/broker/session-index.ts +62 -12
  211. package/src/sdk/bus/chat-command-policy.ts +16 -3
  212. package/src/sdk/bus/index.ts +52 -21
  213. package/src/sdk/bus/reconciliation-store.ts +201 -8
  214. package/src/sdk/bus/telegram-daemon-contract.ts +3 -1
  215. package/src/sdk/cli/session-cli.ts +21 -6
  216. package/src/sdk/host/session-runtime.test.ts +118 -23
  217. package/src/sdk/host/session-runtime.ts +352 -47
  218. package/src/sdk/lifecycle/service.ts +1 -0
  219. package/src/sdk/prompt-deadline-flush.ts +78 -5
  220. package/src/sdk/prompt-deadline-manager.ts +68 -14
  221. package/src/sdk/protocol/adapter-validation.ts +53 -11
  222. package/src/sdk/protocol/operation-inventory.generated.json +101 -2
  223. package/src/sdk/reconciliation-extensions.ts +1 -0
  224. package/src/sdk/session-list.ts +1 -0
  225. package/src/sdk/session.ts +114 -61
  226. package/src/session/agent-session.ts +1043 -81
  227. package/src/session/internal/managed-session-scope.ts +126 -39
  228. package/src/session/internal/managed-session-storage.ts +14 -11
  229. package/src/session/internal/native-publish-outcome.ts +5 -1
  230. package/src/session/messages.ts +47 -6
  231. package/src/session/session-manager.ts +236 -14
  232. package/src/session/startup-auth-config.ts +116 -8
  233. package/src/session/streaming-output.ts +262 -4
  234. package/src/session/terminal-abort.ts +14 -2
  235. package/src/session-import/service.ts +1 -0
  236. package/src/skill-state/workflow-mutation-guard.ts +491 -32
  237. package/src/slash-commands/builtin-registry.ts +15 -3
  238. package/src/slash-commands/helpers/credential-switch.ts +10 -5
  239. package/src/slash-commands/helpers/provider-auth-status.ts +456 -0
  240. package/src/task/executor.ts +103 -12
  241. package/src/task/index.ts +389 -175
  242. package/src/tools/ast-edit-write-path.ts +64 -0
  243. package/src/tools/ast-edit.ts +18 -1
  244. package/src/tools/atomic-file-write.ts +46 -15
  245. package/src/tools/auto-generated-guard.ts +3 -0
  246. package/src/tools/bash-allowed-prefixes.ts +153 -5
  247. package/src/tools/bash-interactive.ts +14 -2
  248. package/src/tools/bash.ts +14 -0
  249. package/src/tools/debug.ts +7 -6
  250. package/src/tools/descriptors.test.ts +44 -0
  251. package/src/tools/descriptors.ts +34 -2
  252. package/src/tools/fetch.ts +3 -2
  253. package/src/tools/index.ts +2 -0
  254. package/src/tools/output-meta.ts +23 -2
  255. package/src/tools/python.ts +1 -1
  256. package/src/tools/read.ts +17 -35
  257. package/src/tools/report-issue.ts +184 -0
  258. package/src/tools/subagent-render.ts +114 -12
  259. package/src/tools/subagent.ts +108 -1
  260. package/src/tools/todo-write.ts +41 -3
  261. package/src/tools/tool-catalog.generated.ts +31 -3
  262. package/src/tools/write.ts +35 -3
  263. package/src/utils/commit-message-generator.ts +9 -2
  264. package/src/utils/file-mentions.ts +29 -1
  265. package/src/utils/title-generator.ts +7 -2
  266. package/src/web/scrapers/index.ts +3 -1
  267. package/src/web/scrapers/types.ts +8 -0
  268. package/src/web/scrapers/youtube.ts +3 -2
  269. package/src/web/search/providers/insane.ts +91 -28
  270. package/src/web/search/providers/searxng.ts +62 -10
@@ -6,6 +6,7 @@
6
6
  */
7
7
 
8
8
  import * as fs from "node:fs";
9
+ import * as os from "node:os";
9
10
  import * as path from "node:path";
10
11
  import type { AgentToolResult } from "@gajae-code/agent-core";
11
12
  import { isEnoent } from "@gajae-code/utils";
@@ -80,6 +81,9 @@ export interface ApplyPatchOptions {
80
81
  * object identity is not a durable-lock capability probe.
81
82
  */
82
83
  crossProcessLock?: boolean;
84
+ /** Explicit artifacts `local` root for this call. The caller session owns it. */
85
+ localRoot?: string;
86
+ signal?: AbortSignal;
83
87
  }
84
88
 
85
89
  // ═══════════════════════════════════════════════════════════════════════════
@@ -117,6 +121,117 @@ function loadNativePatchBindings(): Promise<NativePatchBindings> {
117
121
  return nativePatchBindingsPromise;
118
122
  }
119
123
 
124
+ function explicitLocalRootForSession(session: ToolSession): string | undefined {
125
+ const artifactsDir = session.getArtifactsDir?.();
126
+ if (!artifactsDir || session.isManagedSessionDestination?.()) return undefined;
127
+ return path.resolve(artifactsDir, "local");
128
+ }
129
+
130
+ function pathIsWithin(target: string, root: string): boolean {
131
+ const relative = path.relative(root, target);
132
+ return relative === "" || (relative !== ".." && !relative.startsWith(`..${path.sep}`) && !path.isAbsolute(relative));
133
+ }
134
+
135
+ /**
136
+ * Trust root for a delete. `localRoot` is the caller session's explicit
137
+ * artifacts root. `gjc-local/<id>` is recognized from the path. Else `cwd`.
138
+ */
139
+ function boundaryForPatchDelete(cwd: string, absolutePath: string, localRoot?: string): string {
140
+ const resolved = path.resolve(absolutePath);
141
+ if (localRoot) {
142
+ const explicit = path.resolve(localRoot);
143
+ if (pathIsWithin(resolved, explicit)) return explicit;
144
+ }
145
+ const managedParent = path.join(os.tmpdir(), "gjc-local");
146
+ if (pathIsWithin(resolved, managedParent)) {
147
+ const rest = resolved.slice(managedParent.length + path.sep.length);
148
+ const sessionSegment = rest.split(path.sep, 1)[0] ?? "";
149
+ if (sessionSegment.length > 0) return path.join(managedParent, sessionSegment);
150
+ }
151
+ return path.resolve(cwd);
152
+ }
153
+
154
+ /**
155
+ * `unlink` follows a directory symlink. Resolve the parent only, then delete
156
+ * that real parent plus the final name so a file symlink is removed as a link.
157
+ */
158
+ async function resolvePatchDeletePath(
159
+ root: string,
160
+ absolutePath: string,
161
+ displayPath: string,
162
+ signal?: AbortSignal,
163
+ ): Promise<string> {
164
+ signal?.throwIfAborted();
165
+ let realRoot: string;
166
+ try {
167
+ realRoot = await fs.promises.realpath(root);
168
+ } catch (error) {
169
+ if (isEnoent(error)) throw new ApplyPatchError(`File not found: ${displayPath}`);
170
+ throw error;
171
+ }
172
+ signal?.throwIfAborted();
173
+ let realParent: string;
174
+ try {
175
+ realParent = await fs.promises.realpath(path.dirname(absolutePath));
176
+ } catch (error) {
177
+ if (isEnoent(error)) throw new ApplyPatchError(`File not found: ${displayPath}`);
178
+ throw error;
179
+ }
180
+ signal?.throwIfAborted();
181
+ if (!pathIsWithin(realParent, realRoot)) {
182
+ throw new ApplyPatchError(`Delete target resolves outside its root: ${displayPath}`);
183
+ }
184
+ return path.join(realParent, path.basename(absolutePath));
185
+ }
186
+
187
+ /**
188
+ * `mkdir` follows a directory symlink. Walk to the nearest existing ancestor
189
+ * and return its real path plus the missing suffix. A parent that is already
190
+ * lexically outside the workspace or session local root is unchanged.
191
+ */
192
+ async function resolvePatchMkdirPath(
193
+ cwd: string,
194
+ absolutePath: string,
195
+ localRoot: string | undefined,
196
+ parentDir: string,
197
+ displayPath: string,
198
+ signal?: AbortSignal,
199
+ ): Promise<string> {
200
+ signal?.throwIfAborted();
201
+ const boundary = path.resolve(boundaryForPatchDelete(cwd, absolutePath, localRoot));
202
+ const lexicalParent = path.resolve(parentDir);
203
+ if (!pathIsWithin(lexicalParent, boundary)) return parentDir;
204
+ // A root that does not exist yet is not an escape. Keep the missing suffix
205
+ // under its real ancestor, which is what mkdir did before this check.
206
+ const realRoot = await realpathThroughExistingAncestor(boundary, signal);
207
+ signal?.throwIfAborted();
208
+ const realParent = await realpathThroughExistingAncestor(lexicalParent, signal);
209
+ signal?.throwIfAborted();
210
+ if (!pathIsWithin(realParent, realRoot)) {
211
+ throw new ApplyPatchError(`Parent directory resolves outside its root: ${displayPath}`);
212
+ }
213
+ return realParent;
214
+ }
215
+
216
+ async function realpathThroughExistingAncestor(target: string, signal?: AbortSignal): Promise<string> {
217
+ const resolved = path.resolve(target);
218
+ const missing: string[] = [];
219
+ let cursor = resolved;
220
+ for (;;) {
221
+ signal?.throwIfAborted();
222
+ try {
223
+ const real = await fs.promises.realpath(cursor);
224
+ return missing.length === 0 ? real : path.join(real, ...missing.reverse());
225
+ } catch (error) {
226
+ if (!isEnoent(error)) throw error;
227
+ const parent = path.dirname(cursor);
228
+ if (parent === cursor) return resolved;
229
+ missing.push(path.basename(cursor));
230
+ cursor = parent;
231
+ }
232
+ }
233
+ }
234
+
120
235
  async function readExistingPatchFile(fileSystem: FileSystem, absolutePath: string, path: string): Promise<string> {
121
236
  try {
122
237
  return await fileSystem.read(absolutePath);
@@ -160,6 +275,9 @@ export async function applyPatch(input: PatchInput, options: ApplyPatchOptions):
160
275
  const destPath = resolvePath(input.rename);
161
276
  if (destPath !== absolutePath) mutationPaths.push(destPath);
162
277
  }
278
+ // The cross-process lock creates missing parent directories for `<path>.lock`.
279
+ // Check every locked create/move path, including a rename on create, first.
280
+ await preflightPatchMkdir(input, options);
163
281
  const usingDefaultFs = options.fs === undefined || options.fs === defaultFileSystem;
164
282
  // dryRun/preview must stay read-only: never create durable `<path>.lock` dirs
165
283
  // (would fail on read-only parents and mutate the FS during preview) (#2900 review).
@@ -167,6 +285,36 @@ export async function applyPatch(input: PatchInput, options: ApplyPatchOptions):
167
285
  return withEditPathMutation(mutationPaths, () => applyNormalizedPatch(input, options), { crossProcess });
168
286
  }
169
287
 
288
+ /** Read-only mkdir check for one patch hunk. Does not create directories. */
289
+ export async function preflightPatchMkdir(input: PatchInput, options: ApplyPatchOptions): Promise<void> {
290
+ const resolvePath = (p: string): string => resolveToCwd(p, options.cwd);
291
+ const absolutePath = resolvePath(input.path);
292
+ const op = input.op ?? "update";
293
+ if (op === "create") {
294
+ await rejectPatchMkdirEscape(options.cwd, absolutePath, options.localRoot, input.path, options.signal);
295
+ }
296
+ if (input.rename) {
297
+ const destPath = resolvePath(input.rename);
298
+ if (destPath !== absolutePath) {
299
+ // The source is locked too. Check it before that lock can mkdir through a symlink.
300
+ await rejectPatchMkdirEscape(options.cwd, absolutePath, options.localRoot, input.path, options.signal);
301
+ await rejectPatchMkdirEscape(options.cwd, destPath, options.localRoot, input.rename, options.signal);
302
+ }
303
+ }
304
+ }
305
+
306
+ async function rejectPatchMkdirEscape(
307
+ cwd: string,
308
+ absolutePath: string,
309
+ localRoot: string | undefined,
310
+ displayPath: string,
311
+ signal?: AbortSignal,
312
+ ): Promise<void> {
313
+ const parentDir = path.dirname(absolutePath);
314
+ if (!parentDir || parentDir === ".") return;
315
+ await resolvePatchMkdirPath(cwd, absolutePath, localRoot, parentDir, displayPath, signal);
316
+ }
317
+
170
318
  /**
171
319
  * Apply a normalized patch operation to the filesystem.
172
320
  * Caller must already hold path mutation rights when concurrent writers exist.
@@ -179,6 +327,8 @@ async function applyNormalizedPatch(input: PatchInput, options: ApplyPatchOption
179
327
  fs = defaultFileSystem,
180
328
  fuzzyThreshold = DEFAULT_FUZZY_THRESHOLD,
181
329
  allowFuzzy = true,
330
+ localRoot,
331
+ signal,
182
332
  } = options;
183
333
 
184
334
  const resolvePath = (p: string): string => resolveToCwd(p, cwd);
@@ -201,11 +351,17 @@ async function applyNormalizedPatch(input: PatchInput, options: ApplyPatchOption
201
351
  const normalizedContent = normalizeCreateContent(input.diff);
202
352
  const content = normalizedContent.endsWith("\n") ? normalizedContent : `${normalizedContent}\n`;
203
353
 
354
+ const parentDir = path.dirname(absolutePath);
355
+ const mkdirPath =
356
+ parentDir && parentDir !== "."
357
+ ? await resolvePatchMkdirPath(cwd, absolutePath, localRoot, parentDir, input.path, signal)
358
+ : undefined;
204
359
  if (!dryRun) {
205
- const parentDir = path.dirname(absolutePath);
206
- if (parentDir && parentDir !== ".") {
207
- await fs.mkdir(parentDir);
360
+ if (mkdirPath) {
361
+ signal?.throwIfAborted();
362
+ await fs.mkdir(mkdirPath);
208
363
  }
364
+ signal?.throwIfAborted();
209
365
  await fs.write(absolutePath, content);
210
366
  }
211
367
 
@@ -220,9 +376,14 @@ async function applyNormalizedPatch(input: PatchInput, options: ApplyPatchOption
220
376
 
221
377
  // Handle DELETE operation
222
378
  if (op === "delete") {
223
- const oldContent = await readExistingPatchFile(fs, absolutePath, input.path);
379
+ signal?.throwIfAborted();
380
+ const boundary = boundaryForPatchDelete(cwd, absolutePath, localRoot);
381
+ const deletePath = await resolvePatchDeletePath(boundary, absolutePath, input.path, signal);
382
+ signal?.throwIfAborted();
383
+ const oldContent = await readExistingPatchFile(fs, deletePath, input.path);
384
+ signal?.throwIfAborted();
224
385
  if (!dryRun) {
225
- await fs.delete(absolutePath);
386
+ await fs.delete(deletePath);
226
387
  }
227
388
 
228
389
  return {
@@ -260,6 +421,20 @@ async function applyNormalizedPatch(input: PatchInput, options: ApplyPatchOption
260
421
  const finalContent = bom + restoreLineEndings(newContent, lineEnding);
261
422
  const destPath = input.rename ? resolvePath(input.rename) : absolutePath;
262
423
  const isMove = Boolean(input.rename) && destPath !== absolutePath;
424
+ let moveParent: string | undefined;
425
+ if (isMove) {
426
+ const parentDir = path.dirname(destPath);
427
+ if (parentDir && parentDir !== ".") {
428
+ moveParent = await resolvePatchMkdirPath(
429
+ cwd,
430
+ destPath,
431
+ localRoot,
432
+ parentDir,
433
+ input.rename ?? input.path,
434
+ signal,
435
+ );
436
+ }
437
+ }
263
438
 
264
439
  if (!dryRun) {
265
440
  // Commit-time CAS: reject if another writer mutated the file after our
@@ -269,10 +444,11 @@ async function applyNormalizedPatch(input: PatchInput, options: ApplyPatchOption
269
444
  throw new ApplyPatchError(`concurrent edit conflict: file changed since read: ${input.path}`);
270
445
  }
271
446
  if (isMove) {
272
- const parentDir = path.dirname(destPath);
273
- if (parentDir && parentDir !== ".") {
274
- await fs.mkdir(parentDir);
447
+ if (moveParent) {
448
+ signal?.throwIfAborted();
449
+ await fs.mkdir(moveParent);
275
450
  }
451
+ signal?.throwIfAborted();
276
452
  await fs.write(destPath, finalContent);
277
453
  await fs.delete(absolutePath);
278
454
  } else {
@@ -493,12 +669,15 @@ export async function executePatchSingle(
493
669
  // not `defaultFileSystem` by object identity. Force durable cross-process
494
670
  // locking so multi-process agents cannot silently race past the path mutex
495
671
  // (#2900 review: do not infer lock capability from FileSystem identity).
672
+ const localRoot = explicitLocalRootForSession(session);
496
673
  const result = await applyPatch(input, {
497
674
  cwd: session.cwd,
498
675
  fs: patchFileSystem,
499
676
  fuzzyThreshold,
500
677
  allowFuzzy,
501
678
  crossProcessLock: true,
679
+ localRoot,
680
+ signal,
502
681
  });
503
682
 
504
683
  // Post-write verification: only meaningful for in-place updates where the
@@ -9,6 +9,7 @@ import { markDesignedError } from "@gajae-code/utils/error-classification";
9
9
  import * as z from "zod/v4";
10
10
  import type { WritethroughCallback, WritethroughDeferredHandle } from "../../lsp";
11
11
  import type { ToolSession } from "../../tools";
12
+ import { assertEditableFile, assertEditableFileContent } from "../../tools/auto-generated-guard";
12
13
  import { invalidateFsScanAfterWrite } from "../../tools/fs-cache-invalidation";
13
14
  import { outputMeta } from "../../tools/output-meta";
14
15
  import { enforcePlanModeWrite, resolvePlanPath } from "../../tools/plan-mode-guard";
@@ -636,6 +637,7 @@ export async function executeReplaceSingle(
636
637
  }
637
638
 
638
639
  const absolutePath = resolvePlanPath(session, path);
640
+ await assertEditableFile(absolutePath, path, session.settings);
639
641
  return withEditPathMutation([absolutePath], () =>
640
642
  executeReplaceSingleUnderLock({
641
643
  session,
@@ -664,6 +666,7 @@ async function executeReplaceSingleUnderLock(
664
666
  },
665
667
  ): Promise<AgentToolResult<EditToolDetails, typeof replaceEditEntrySchema>> {
666
668
  const {
669
+ session,
667
670
  path,
668
671
  signal,
669
672
  batchRequest,
@@ -678,6 +681,8 @@ async function executeReplaceSingleUnderLock(
678
681
  } = options;
679
682
 
680
683
  const rawContent = await readEditFileText(absolutePath, path);
684
+ assertEditableFileContent(rawContent, path, session.settings);
685
+ await assertEditableFile(absolutePath, path, session.settings);
681
686
  const { bom, text: content } = stripBom(rawContent);
682
687
  const originalEnding = detectLineEnding(content);
683
688
  const normalizedContent = normalizeToLF(content);
@@ -749,3 +754,134 @@ async function executeReplaceSingleUnderLock(
749
754
  },
750
755
  };
751
756
  }
757
+
758
+ export async function executeReplaceEntries(
759
+ options: Omit<ExecuteReplaceSingleOptions, "params"> & { edits: ReplaceEditEntry[] },
760
+ ): Promise<AgentToolResult<EditToolDetails, typeof replaceEditSchema>> {
761
+ if (options.edits.length <= 1) {
762
+ const only = options.edits[0];
763
+ if (!only) {
764
+ throw new ToolError("edits must not be empty.");
765
+ }
766
+ return executeReplaceSingle({ ...options, params: only });
767
+ }
768
+
769
+ const { session, path, edits } = options;
770
+ try {
771
+ enforcePlanModeWrite(session, path);
772
+ for (const entry of edits) {
773
+ if (entry.old_text.length === 0) {
774
+ throw new ToolError("old_text must not be empty.");
775
+ }
776
+ }
777
+ const absolutePath = resolvePlanPath(session, path);
778
+ return await withEditPathMutation([absolutePath], () =>
779
+ executeReplaceEntriesUnderLock({ ...options, absolutePath }),
780
+ );
781
+ } catch (err) {
782
+ const errorText = err instanceof Error ? err.message : String(err);
783
+ return {
784
+ content: [{ type: "text", text: `Error editing ${path}: ${errorText}` }],
785
+ details: { diff: "", path },
786
+ isError: true,
787
+ };
788
+ }
789
+ }
790
+
791
+ async function executeReplaceEntriesUnderLock(
792
+ options: Omit<ExecuteReplaceSingleOptions, "params"> & {
793
+ edits: ReplaceEditEntry[];
794
+ absolutePath: string;
795
+ },
796
+ ): Promise<AgentToolResult<EditToolDetails, typeof replaceEditSchema>> {
797
+ const {
798
+ session,
799
+ path,
800
+ edits,
801
+ signal,
802
+ batchRequest,
803
+ allowFuzzy,
804
+ fuzzyThreshold,
805
+ writethrough,
806
+ beginDeferredDiagnosticsForPath,
807
+ absolutePath,
808
+ } = options;
809
+
810
+ const rawContent = await readEditFileText(absolutePath, path);
811
+ assertEditableFileContent(rawContent, path, session.settings);
812
+ await assertEditableFile(absolutePath, path, session.settings);
813
+
814
+ const { bom, text: content } = stripBom(rawContent);
815
+ const originalEnding = detectLineEnding(content);
816
+ let normalizedContent = normalizeToLF(content);
817
+ const originalNormalized = normalizedContent;
818
+ const texts: string[] = [];
819
+
820
+ for (const [index, entry] of edits.entries()) {
821
+ const normalizedOldText = normalizeToLF(entry.old_text);
822
+ const normalizedNewText = normalizeToLF(entry.new_text);
823
+ const replaced = await replaceText(normalizedContent, normalizedOldText, normalizedNewText, {
824
+ fuzzy: allowFuzzy,
825
+ all: entry.all ?? false,
826
+ threshold: fuzzyThreshold,
827
+ });
828
+ if (replaced.count === 0) {
829
+ const matchOutcome = await findMatch(normalizedContent, normalizedOldText, {
830
+ allowFuzzy,
831
+ threshold: fuzzyThreshold,
832
+ });
833
+ if (matchOutcome.occurrences && matchOutcome.occurrences > 1) {
834
+ throw new ToolError(formatOccurrenceError(path, matchOutcome));
835
+ }
836
+ throw new EditMatchError(path, normalizedOldText, matchOutcome.closest, {
837
+ allowFuzzy,
838
+ threshold: fuzzyThreshold,
839
+ fuzzyMatches: matchOutcome.fuzzyMatches,
840
+ });
841
+ }
842
+ if (normalizedContent === replaced.content) {
843
+ throw new ToolError(`Edits to ${path} resulted in no changes being made.`);
844
+ }
845
+ normalizedContent = replaced.content;
846
+ if (index < edits.length - 1) {
847
+ assertEditableFileContent(normalizedContent, path, session.settings);
848
+ }
849
+ texts.push(
850
+ replaced.count > 1
851
+ ? `Successfully replaced ${replaced.count} occurrences in ${path}.`
852
+ : `Successfully replaced text in ${path}.`,
853
+ );
854
+ }
855
+
856
+ const finalContent = await serializeEditFileText(
857
+ absolutePath,
858
+ path,
859
+ bom + restoreLineEndings(normalizedContent, originalEnding),
860
+ );
861
+ const diagnostics = await writethrough(
862
+ absolutePath,
863
+ finalContent,
864
+ signal,
865
+ Bun.file(absolutePath),
866
+ batchRequest,
867
+ dst => (dst === absolutePath ? beginDeferredDiagnosticsForPath(absolutePath) : undefined),
868
+ );
869
+ invalidateFsScanAfterWrite(absolutePath);
870
+
871
+ const diffResult = generateDiffString(originalNormalized, normalizedContent);
872
+ const meta = outputMeta()
873
+ .diagnostics(diagnostics?.summary ?? "", diagnostics?.messages ?? [])
874
+ .get();
875
+ return {
876
+ content: [{ type: "text", text: texts.join("\n") }],
877
+ details: {
878
+ diff: diffResult.diff,
879
+ path: absolutePath,
880
+ firstChangedLine: diffResult.firstChangedLine,
881
+ diagnostics,
882
+ meta,
883
+ oldText: rawContent,
884
+ newText: finalContent,
885
+ },
886
+ };
887
+ }
@@ -523,15 +523,41 @@ async function spawnJsWorker(): Promise<WorkerHandle> {
523
523
  }
524
524
 
525
525
  function wrapBunWorker(worker: Worker): WorkerHandle {
526
+ // Cells share parentPort, so worker→host frames use a port transferred before
527
+ // the first cell. worker "message" events are accepted only until ready.
528
+ const channel = new MessageChannel();
529
+ const hostPort = channel.port2;
530
+ const queued: WorkerOutbound[] = [];
531
+ let onFrame: ((msg: WorkerOutbound) => void) | undefined;
532
+ let acceptWorkerEvent = true;
533
+ const deliver = (msg: WorkerOutbound): void => {
534
+ if (msg?.type === "ready" || msg?.type === "init-failed") acceptWorkerEvent = false;
535
+ if (onFrame) onFrame(msg);
536
+ else queued.push(msg);
537
+ };
538
+ const onPort = (event: Event): void => {
539
+ deliver((event as MessageEvent).data as WorkerOutbound);
540
+ };
541
+ const onWorker = (event: Event): void => {
542
+ if (!acceptWorkerEvent) return;
543
+ deliver((event as MessageEvent).data as WorkerOutbound);
544
+ };
545
+ hostPort.addEventListener("message", onPort);
546
+ worker.addEventListener("message", onWorker);
547
+ worker.postMessage({ type: "gjc-bind", port: channel.port1 }, { transfer: [channel.port1] });
526
548
  return {
527
549
  mode: "worker",
528
550
  send(msg) {
529
551
  worker.postMessage(msg);
530
552
  },
531
553
  onMessage(handler) {
532
- const wrap = (event: MessageEvent): void => handler(event.data as WorkerOutbound);
533
- worker.addEventListener("message", wrap);
534
- return () => worker.removeEventListener("message", wrap);
554
+ onFrame = handler;
555
+ for (const msg of queued.splice(0)) handler(msg);
556
+ return () => {
557
+ if (onFrame === handler) onFrame = undefined;
558
+ hostPort.removeEventListener("message", onPort);
559
+ worker.removeEventListener("message", onWorker);
560
+ };
535
561
  },
536
562
  async terminate() {
537
563
  worker.terminate();
@@ -1,24 +1,57 @@
1
- import { parentPort } from "node:worker_threads";
1
+ import { type MessagePort, parentPort } from "node:worker_threads";
2
2
  import { WorkerCore } from "./worker-core";
3
3
  import type { Transport, WorkerInbound, WorkerOutbound } from "./worker-protocol";
4
4
 
5
5
  if (!parentPort) throw new Error("js worker-entry: missing parentPort");
6
6
 
7
- const port = parentPort;
8
- const transport: Transport = {
9
- send: (msg: WorkerOutbound) => port.postMessage(msg),
10
- onMessage: handler => {
11
- const wrap = (data: unknown): void => handler(data as WorkerInbound);
12
- port.on("message", wrap);
13
- return () => port.off("message", wrap);
14
- },
15
- close: () => {
16
- try {
17
- port.close();
18
- } catch {
19
- // Already closed.
20
- }
21
- },
22
- };
7
+ const parent = parentPort;
8
+
9
+ function isHostPort(value: unknown): value is MessagePort {
10
+ if (!value || typeof value !== "object") return false;
11
+ const port = value as MessagePort;
12
+ return typeof port.postMessage === "function" && typeof port.on === "function";
13
+ }
14
+
15
+ function readBindPort(data: unknown): MessagePort | undefined {
16
+ if (!data || typeof data !== "object") return undefined;
17
+ const record = data as { type?: unknown; port?: unknown };
18
+ if (record.type !== "gjc-bind" || !isHostPort(record.port)) return undefined;
19
+ return record.port;
20
+ }
23
21
 
24
- new WorkerCore(transport);
22
+ function listen(port: MessagePort): void {
23
+ // Bound before any user cell. A later prototype patch cannot retarget this send.
24
+ const post = port.postMessage.bind(port);
25
+ const transport: Transport = {
26
+ send: (msg: WorkerOutbound) => {
27
+ post(msg);
28
+ },
29
+ onMessage: handler => {
30
+ const wrap = (data: unknown): void => handler(data as WorkerInbound);
31
+ parent.on("message", wrap);
32
+ return () => parent.off("message", wrap);
33
+ },
34
+ close: () => {
35
+ try {
36
+ port.close();
37
+ } catch {
38
+ // Already closed.
39
+ }
40
+ try {
41
+ parent.close();
42
+ } catch {
43
+ // Already closed.
44
+ }
45
+ },
46
+ };
47
+ new WorkerCore(transport);
48
+ }
49
+
50
+ const onParent = (data: unknown): void => {
51
+ const port = readBindPort(data);
52
+ if (!port) return;
53
+ if (data && typeof data === "object") (data as { port?: unknown }).port = undefined;
54
+ parent.off("message", onParent);
55
+ listen(port);
56
+ };
57
+ parent.on("message", onParent);
@@ -912,7 +912,17 @@ async function replaceSessionKernel(
912
912
  const old = session.kernel;
913
913
  const remaining = getRemainingTimeoutMs(options.deadlineMs);
914
914
  const ownerIds = new Set(session.ownerIds);
915
- await shutdownOrRetainKernel(old, ownerIds, remaining === undefined ? undefined : Math.max(0, remaining));
915
+ try {
916
+ await shutdownOrRetainKernel(old, ownerIds, remaining === undefined ? undefined : Math.max(0, remaining));
917
+ } catch (error) {
918
+ // Suppress unconfirmed shutdown error ONLY when we have positive evidence the
919
+ // process actually exited (exitCode is set). Without that evidence, propagate the error
920
+ // so we don't start a replacement while the old subprocess is still running.
921
+ // See https://github.com/yeachan-heo/gajae-code/issues/6569
922
+ const hasConfirmedExit = old.getExitCode() !== null;
923
+ if (!hasConfirmedExit || !(error instanceof Error) || error.name !== "PythonKernelShutdownUnconfirmedError")
924
+ throw error;
925
+ }
916
926
  throwIfExecutionCancelled(options);
917
927
  if (sessions.get(session.sessionId) !== session) {
918
928
  throw new PythonExecutionCancelledError(false);
@@ -1076,7 +1086,6 @@ export function disposeKernelSessionsByOwner(ownerId: string): Promise<void> {
1076
1086
  const toShutdown: PythonSession[] = [];
1077
1087
  const initializersToAbort: InitializingPythonSession[] = [];
1078
1088
  for (const entry of initializing) {
1079
- entry.retirementOwnerIds.add(ownerId);
1080
1089
  let hasOwnerRequest = false;
1081
1090
  for (const request of entry.requests) {
1082
1091
  if (request.ownerId === ownerId && !request.signal?.aborted) {
@@ -1089,6 +1098,7 @@ export function disposeKernelSessionsByOwner(ownerId: string): Promise<void> {
1089
1098
  if (entry.ownerIds.size > 0) removeKernelOwner(entry.kernel, ownerId);
1090
1099
  }
1091
1100
  if (![...entry.ownerIds].some(id => id !== ownerId)) {
1101
+ entry.retirementOwnerIds.add(ownerId);
1092
1102
  entry.cancelled ??= new PythonExecutionCancelledError(false);
1093
1103
  initializersToAbort.push(entry);
1094
1104
  }
@@ -1137,10 +1147,10 @@ export function disposeKernelSessionsByOwner(ownerId: string): Promise<void> {
1137
1147
  return (async () => {
1138
1148
  await Promise.all([
1139
1149
  ...requests.map(request => request.completion),
1140
- ...initializing.map(entry => entry.promise.catch(() => undefined)),
1150
+ ...initializersToAbort.map(entry => entry.promise.catch(() => undefined)),
1141
1151
  ]);
1142
1152
  const failures: unknown[] = [];
1143
- for (const entry of initializing)
1153
+ for (const entry of initializersToAbort)
1144
1154
  if (entry.cleanupFailed) {
1145
1155
  if (entry.kernel) retainKernelForOwners(entry.kernel, new Set([ownerId]));
1146
1156
  failures.push(entry.cleanupError);