@gajae-code/coding-agent 0.11.3 → 0.11.5

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 (213) hide show
  1. package/CHANGELOG.md +47 -0
  2. package/dist/types/cli/notify-cli.d.ts +5 -1
  3. package/dist/types/cli/worktree-cli.d.ts +8 -25
  4. package/dist/types/commands/gc.d.ts +4 -0
  5. package/dist/types/commands/worktree.d.ts +7 -32
  6. package/dist/types/config/settings-schema.d.ts +20 -0
  7. package/dist/types/export/html/template.generated.d.ts +1 -1
  8. package/dist/types/gjc-runtime/deep-interview-state.d.ts +28 -0
  9. package/dist/types/gjc-runtime/gc-runtime.d.ts +9 -0
  10. package/dist/types/gjc-runtime/launch-tmux.d.ts +3 -0
  11. package/dist/types/gjc-runtime/managed-owner-admission.d.ts +42 -0
  12. package/dist/types/gjc-runtime/managed-owner-supervisor.d.ts +41 -0
  13. package/dist/types/gjc-runtime/state-argv.d.ts +38 -0
  14. package/dist/types/gjc-runtime/tmux-owner-isolation.d.ts +9 -0
  15. package/dist/types/gjc-runtime/ultragoal-owner-loss-recovery.d.ts +94 -0
  16. package/dist/types/gjc-runtime/ultragoal-runtime.d.ts +1 -0
  17. package/dist/types/harness-control-plane/owner.d.ts +3 -1
  18. package/dist/types/internal-urls/local-protocol.d.ts +18 -0
  19. package/dist/types/main.d.ts +2 -2
  20. package/dist/types/modes/components/ansi-display-validator.d.ts +2 -0
  21. package/dist/types/modes/components/custom-editor.d.ts +8 -3
  22. package/dist/types/modes/components/notifications-settings-editor.d.ts +4 -0
  23. package/dist/types/modes/components/session-observer-overlay.d.ts +3 -1
  24. package/dist/types/modes/components/settings-selector.d.ts +2 -0
  25. package/dist/types/modes/components/tool-transcript-format.d.ts +77 -0
  26. package/dist/types/modes/components/transcript-viewer-overlay.d.ts +3 -0
  27. package/dist/types/modes/controllers/goal-mode-controller.d.ts +8 -1
  28. package/dist/types/modes/controllers/input-controller.d.ts +8 -2
  29. package/dist/types/modes/controllers/selector-controller.d.ts +15 -0
  30. package/dist/types/modes/shared/agent-wire/workflow-gate-broker.d.ts +3 -0
  31. package/dist/types/modes/shared/agent-wire/workflow-gate-types.d.ts +16 -0
  32. package/dist/types/plan-mode/approved-plan.d.ts +1 -0
  33. package/dist/types/runtime-mcp/manager.d.ts +2 -0
  34. package/dist/types/runtime-mcp/oauth-discovery.d.ts +12 -1
  35. package/dist/types/sdk/broker/broker.d.ts +1 -0
  36. package/dist/types/sdk/broker/discovery.d.ts +10 -0
  37. package/dist/types/sdk/broker/ensure.d.ts +24 -1
  38. package/dist/types/sdk/broker/internal.d.ts +3 -0
  39. package/dist/types/sdk/broker/session-index.d.ts +12 -0
  40. package/dist/types/sdk/broker/state-version.d.ts +2 -1
  41. package/dist/types/sdk/bus/chat-daemon-cli.d.ts +3 -1
  42. package/dist/types/sdk/bus/chat-daemon-control.d.ts +39 -4
  43. package/dist/types/sdk/bus/config-commands.d.ts +8 -0
  44. package/dist/types/sdk/bus/config.d.ts +22 -0
  45. package/dist/types/sdk/bus/index.d.ts +162 -2
  46. package/dist/types/sdk/bus/notification-service.d.ts +71 -8
  47. package/dist/types/sdk/bus/session-control.d.ts +11 -8
  48. package/dist/types/sdk/bus/telegram-daemon-cli.d.ts +5 -0
  49. package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +6 -3
  50. package/dist/types/sdk/bus/telegram-daemon-control.d.ts +23 -4
  51. package/dist/types/sdk/bus/telegram-daemon.d.ts +214 -20
  52. package/dist/types/sdk/session.d.ts +16 -1
  53. package/dist/types/secrets/index.d.ts +4 -2
  54. package/dist/types/secrets/obfuscator.d.ts +1 -1
  55. package/dist/types/session/agent-session.d.ts +31 -2
  56. package/dist/types/session/artifacts.d.ts +30 -8
  57. package/dist/types/session/internal/managed-session-scope.d.ts +7 -0
  58. package/dist/types/session/internal/managed-session-storage.d.ts +63 -6
  59. package/dist/types/session/session-manager.d.ts +88 -18
  60. package/dist/types/session/session-storage.d.ts +27 -0
  61. package/dist/types/session/streaming-output.d.ts +4 -0
  62. package/dist/types/task/executor.d.ts +9 -0
  63. package/dist/types/tool-discovery/tool-index.d.ts +2 -0
  64. package/dist/types/tools/ask.d.ts +87 -12
  65. package/dist/types/tools/bash-skill-urls.d.ts +2 -2
  66. package/dist/types/tools/browser/screenshot-format.d.ts +8 -0
  67. package/dist/types/tools/browser/tab-protocol.d.ts +11 -0
  68. package/dist/types/tools/browser/tab-supervisor.d.ts +1 -1
  69. package/dist/types/tools/browser/tab-worker-path-resolver.d.ts +2 -0
  70. package/dist/types/tools/browser/tab-worker-smoke.d.ts +16 -0
  71. package/dist/types/tools/index.d.ts +4 -0
  72. package/dist/types/tools/path-utils.d.ts +9 -14
  73. package/dist/types/tools/render-utils.d.ts +1 -8
  74. package/dist/types/tools/resource-gc.d.ts +14 -0
  75. package/dist/types/utils/image-loading.d.ts +23 -0
  76. package/dist/types/utils/image-resize.d.ts +2 -0
  77. package/dist/types/utils/pasted-image-loading.d.ts +39 -0
  78. package/dist/types/utils/pasted-image-path.d.ts +21 -19
  79. package/dist/types/web/scrapers/html-to-markdown.d.ts +5 -0
  80. package/dist/types/web/scrapers/types.d.ts +1 -5
  81. package/package.json +25 -9
  82. package/scripts/generate-sdk-operation-inventory.ts +2 -0
  83. package/scripts/run-threat-model-check.ts +60 -0
  84. package/src/cli/notify-cli.ts +40 -18
  85. package/src/cli/worktree-cli.ts +9 -287
  86. package/src/cli.ts +10 -9
  87. package/src/commands/gc.ts +11 -1
  88. package/src/commands/sdk.ts +10 -4
  89. package/src/commands/worktree.ts +10 -53
  90. package/src/config/model-profiles.ts +15 -15
  91. package/src/config/settings-schema.ts +17 -0
  92. package/src/daemon/runtime.ts +2 -2
  93. package/src/defaults/gjc/skills/deep-interview/SKILL.md +70 -49
  94. package/src/defaults/gjc/skills/ralplan/SKILL.md +0 -6
  95. package/src/defaults/gjc/skills/team/SKILL.md +7 -9
  96. package/src/defaults/gjc/skills/ultragoal/SKILL.md +0 -2
  97. package/src/eval/js/shared/runtime.ts +1 -1
  98. package/src/eval/py/index.ts +2 -2
  99. package/src/export/html/template.generated.ts +1 -1
  100. package/src/extensibility/plugins/marketplace/manager.ts +94 -10
  101. package/src/gjc-runtime/deep-interview-recorder.ts +7 -0
  102. package/src/gjc-runtime/deep-interview-runtime.ts +10 -0
  103. package/src/gjc-runtime/deep-interview-state.ts +127 -3
  104. package/src/gjc-runtime/gc-render.ts +19 -1
  105. package/src/gjc-runtime/gc-runtime.ts +68 -3
  106. package/src/gjc-runtime/launch-tmux.ts +61 -6
  107. package/src/gjc-runtime/managed-owner-admission.ts +352 -0
  108. package/src/gjc-runtime/managed-owner-supervisor.ts +162 -0
  109. package/src/gjc-runtime/state-argv.ts +292 -0
  110. package/src/gjc-runtime/state-runtime.ts +80 -222
  111. package/src/gjc-runtime/team-runtime.ts +1 -1
  112. package/src/gjc-runtime/tmux-owner-isolation.ts +117 -0
  113. package/src/gjc-runtime/ultragoal-owner-loss-recovery.ts +328 -0
  114. package/src/gjc-runtime/ultragoal-runtime.ts +14 -0
  115. package/src/harness-control-plane/owner.ts +59 -3
  116. package/src/hindsight/client.ts +65 -2
  117. package/src/internal-urls/agent-protocol.ts +76 -5
  118. package/src/internal-urls/docs-index.generated.ts +7 -6
  119. package/src/internal-urls/local-protocol.ts +416 -18
  120. package/src/lsp/index.ts +4 -88
  121. package/src/main.ts +37 -13
  122. package/src/modes/acp/acp-agent.ts +8 -0
  123. package/src/modes/components/ansi-display-validator.ts +54 -0
  124. package/src/modes/components/custom-editor.ts +103 -32
  125. package/src/modes/components/custom-provider-wizard.ts +26 -1
  126. package/src/modes/components/notifications-settings-editor.ts +26 -2
  127. package/src/modes/components/session-observer-overlay.ts +40 -37
  128. package/src/modes/components/settings-selector.ts +3 -1
  129. package/src/modes/components/tool-transcript-format.ts +393 -0
  130. package/src/modes/components/transcript-viewer-overlay.ts +63 -1
  131. package/src/modes/controllers/goal-mode-controller.ts +20 -1
  132. package/src/modes/controllers/input-controller.ts +98 -37
  133. package/src/modes/controllers/plan-mode-controller.ts +2 -0
  134. package/src/modes/controllers/runtime-mcp-command-controller.ts +3 -0
  135. package/src/modes/controllers/selector-controller.ts +71 -9
  136. package/src/modes/interactive-mode.ts +14 -2
  137. package/src/modes/shared/agent-wire/deep-interview-gate.ts +31 -1
  138. package/src/modes/shared/agent-wire/workflow-gate-broker.ts +17 -1
  139. package/src/modes/shared/agent-wire/workflow-gate-schema.ts +4 -2
  140. package/src/modes/shared/agent-wire/workflow-gate-types.ts +120 -3
  141. package/src/plan-mode/approved-plan.ts +3 -1
  142. package/src/prompts/agents/architect.md +1 -0
  143. package/src/prompts/agents/critic.md +1 -0
  144. package/src/prompts/system/custom-system-prompt.md +1 -1
  145. package/src/prompts/system/system-prompt.md +2 -1
  146. package/src/prompts/tools/lsp.md +3 -3
  147. package/src/runtime-mcp/manager.ts +16 -1
  148. package/src/runtime-mcp/oauth-discovery.ts +116 -26
  149. package/src/sdk/broker/broker.ts +139 -32
  150. package/src/sdk/broker/discovery.ts +125 -0
  151. package/src/sdk/broker/ensure.ts +103 -19
  152. package/src/sdk/broker/internal.ts +10 -0
  153. package/src/sdk/broker/session-index.ts +309 -38
  154. package/src/sdk/broker/state-version.ts +5 -2
  155. package/src/sdk/bus/chat-daemon-cli.ts +72 -10
  156. package/src/sdk/bus/chat-daemon-control.ts +526 -130
  157. package/src/sdk/bus/chat-daemon-runtime.ts +1 -0
  158. package/src/sdk/bus/config-commands.ts +22 -0
  159. package/src/sdk/bus/config.ts +37 -0
  160. package/src/sdk/bus/index.ts +384 -92
  161. package/src/sdk/bus/lifecycle-control-runtime.ts +53 -3
  162. package/src/sdk/bus/notification-service.ts +284 -62
  163. package/src/sdk/bus/recent-activity.ts +14 -5
  164. package/src/sdk/bus/session-control.ts +150 -44
  165. package/src/sdk/bus/telegram-daemon-cli.ts +20 -14
  166. package/src/sdk/bus/telegram-daemon-contract.ts +6 -3
  167. package/src/sdk/bus/telegram-daemon-control.ts +294 -76
  168. package/src/sdk/bus/telegram-daemon.ts +1842 -281
  169. package/src/sdk/protocol/operation-inventory.generated.json +11 -0
  170. package/src/sdk/session.ts +155 -27
  171. package/src/secrets/index.ts +53 -10
  172. package/src/secrets/obfuscator.ts +21 -18
  173. package/src/secrets/regex.ts +2 -0
  174. package/src/session/agent-session.ts +495 -253
  175. package/src/session/artifacts.ts +188 -23
  176. package/src/session/context-estimation.ts +7 -2
  177. package/src/session/internal/managed-session-scope.ts +215 -39
  178. package/src/session/internal/managed-session-storage.ts +1152 -46
  179. package/src/session/session-manager.ts +1055 -148
  180. package/src/session/session-storage.ts +109 -11
  181. package/src/session/streaming-output.ts +6 -0
  182. package/src/task/executor.ts +71 -26
  183. package/src/task/index.ts +63 -20
  184. package/src/tool-discovery/tool-index.ts +18 -0
  185. package/src/tools/ask.ts +329 -57
  186. package/src/tools/bash-allowed-prefixes.ts +79 -29
  187. package/src/tools/bash-skill-urls.ts +14 -3
  188. package/src/tools/bash.ts +2 -2
  189. package/src/tools/browser/launch.ts +3 -1
  190. package/src/tools/browser/readable.ts +1 -1
  191. package/src/tools/browser/screenshot-format.ts +34 -0
  192. package/src/tools/browser/tab-protocol.ts +3 -0
  193. package/src/tools/browser/tab-supervisor.ts +125 -122
  194. package/src/tools/browser/tab-worker-entry.ts +33 -14
  195. package/src/tools/browser/tab-worker-path-resolver.ts +67 -0
  196. package/src/tools/browser/tab-worker-smoke.ts +87 -0
  197. package/src/tools/browser/tab-worker.ts +7 -5
  198. package/src/tools/index.ts +4 -0
  199. package/src/tools/path-utils.ts +42 -35
  200. package/src/tools/plan-mode-guard.ts +1 -0
  201. package/src/tools/read.ts +7 -1
  202. package/src/tools/render-utils.ts +1 -27
  203. package/src/tools/resource-gc.ts +123 -34
  204. package/src/tools/sqlite-reader.ts +3 -2
  205. package/src/tools/subagent.ts +2 -0
  206. package/src/utils/image-loading.ts +85 -34
  207. package/src/utils/image-resize.ts +16 -9
  208. package/src/utils/pasted-image-loading.ts +514 -0
  209. package/src/utils/pasted-image-path.ts +223 -98
  210. package/src/web/scrapers/html-to-markdown.ts +68 -0
  211. package/src/web/scrapers/readthedocs.ts +1 -1
  212. package/src/web/scrapers/types.ts +1 -68
  213. package/src/web/search/providers/openai-compatible.ts +76 -23
@@ -1,14 +1,35 @@
1
+ import { createHash, randomUUID } from "node:crypto";
2
+ import * as fsSync from "node:fs";
1
3
  import * as fs from "node:fs/promises";
2
4
  import * as os from "node:os";
3
5
  import * as path from "node:path";
6
+ import { exactRemoveDirectoryTree, type NativeDirectoryTreeSnapshot, snapshotDirectoryTree } from "@gajae-code/natives";
4
7
  import { isEnoent } from "@gajae-code/utils";
5
8
  import { AgentRegistry } from "../registry/agent-registry";
6
9
  import { parseInternalUrl } from "./parse";
7
10
  import { validateRelativePath } from "./skill-protocol";
8
11
  import type { InternalResource, InternalUrl, ProtocolHandler } from "./types";
9
12
 
13
+ export interface ManagedLegacyLocalMigrationEntry {
14
+ readonly relativePath: string;
15
+ readonly kind: "directory" | "file";
16
+ readonly bytes?: Uint8Array;
17
+ readonly sha256?: string;
18
+ }
19
+
20
+ /** Opaque authority for a managed session's legacy `local` artifact tree. */
21
+ export interface ManagedLegacyLocalMigrationSource {
22
+ capture(): Promise<{
23
+ readonly snapshot: NativeDirectoryTreeSnapshot;
24
+ readonly entries: readonly ManagedLegacyLocalMigrationEntry[];
25
+ } | null>;
26
+ retire(snapshot: NativeDirectoryTreeSnapshot): void;
27
+ }
28
+
10
29
  export interface LocalProtocolOptions {
11
30
  getArtifactsDir?: () => string | null;
31
+ isManagedDestination?: () => boolean;
32
+ getManagedLegacyLocalMigrationSource?: () => ManagedLegacyLocalMigrationSource | null;
12
33
  getSessionId?: () => string | null;
13
34
  }
14
35
 
@@ -45,6 +66,7 @@ async function listFilesRecursively(rootPath: string): Promise<string[]> {
45
66
  const entries = await fs.readdir(absoluteDir, { withFileTypes: true });
46
67
 
47
68
  for (const entry of entries) {
69
+ if (entry.name === LEGACY_MIGRATION_MARKER) continue;
48
70
  const entryPath = path.join(relativeDir, entry.name);
49
71
  if (entry.isDirectory()) {
50
72
  pending.push(entryPath);
@@ -108,20 +130,404 @@ function extractRelativePath(url: InternalUrl): string {
108
130
  return decoded;
109
131
  }
110
132
 
111
- export function resolveLocalRoot(options: LocalProtocolOptions): string {
133
+ function safeSessionId(options: LocalProtocolOptions): string {
134
+ const sessionId = (options.getSessionId?.() ?? "session").replace(/[^a-zA-Z0-9_.-]/g, "_");
135
+ return sessionId === "" || sessionId === "." || sessionId === ".." ? "session" : sessionId;
136
+ }
137
+
138
+ async function assertDirectoryNotSymlink(directoryPath: string): Promise<void> {
139
+ const stat = await fs.lstat(directoryPath);
140
+ if (!stat.isDirectory() || stat.isSymbolicLink()) {
141
+ throw new Error("Unsafe local:// root");
142
+ }
143
+ }
144
+
145
+ const LEGACY_MIGRATION_MARKER = ".gjc-local-legacy-migrated-v1";
146
+ const MAX_LEGACY_LOCAL_BYTES = 64 * 1024 * 1024;
147
+
148
+ interface LegacyEntrySnapshot {
149
+ readonly relativePath: string;
150
+ readonly dev: bigint;
151
+ readonly ino: bigint;
152
+ readonly size: bigint;
153
+ readonly digest?: string;
154
+ readonly bytes?: Buffer;
155
+ }
156
+ interface LegacyIdentityStat {
157
+ readonly dev: bigint;
158
+ readonly ino: bigint;
159
+ readonly size: bigint;
160
+ }
161
+
162
+ function matchesSnapshot(stat: LegacyIdentityStat, snapshot: LegacyEntrySnapshot): boolean {
163
+ return stat.dev === snapshot.dev && stat.ino === snapshot.ino && stat.size === snapshot.size;
164
+ }
165
+
166
+ async function snapshotDirectory(directoryPath: string, relativePath: string): Promise<LegacyEntrySnapshot> {
167
+ const stat = await fs.lstat(directoryPath, { bigint: true });
168
+ if (!stat.isDirectory() || stat.isSymbolicLink()) throw new Error("Unsafe legacy local:// migration source");
169
+ return { relativePath, dev: stat.dev, ino: stat.ino, size: stat.size };
170
+ }
171
+
172
+ async function snapshotRegularFile(filePath: string, relativePath: string): Promise<LegacyEntrySnapshot> {
173
+ const before = await fs.lstat(filePath, { bigint: true });
174
+ if (!before.isFile() || before.isSymbolicLink()) throw new Error("Unsafe legacy local:// migration source");
175
+ const flags = fsSync.constants.O_RDONLY | (process.platform === "win32" ? 0 : (fsSync.constants.O_NOFOLLOW ?? 0));
176
+ const handle = await fs.open(filePath, flags);
177
+ try {
178
+ const opened = await handle.stat({ bigint: true });
179
+ const identity = { relativePath, dev: before.dev, ino: before.ino, size: before.size };
180
+ if (!opened.isFile() || !matchesSnapshot(opened, identity))
181
+ throw new Error("Legacy local:// migration source changed during capture");
182
+ const bytes = await handle.readFile();
183
+ const after = await fs.lstat(filePath, { bigint: true });
184
+ if (!matchesSnapshot(after, identity)) throw new Error("Legacy local:// migration source changed during capture");
185
+ return { ...identity, digest: createHash("sha256").update(bytes).digest("hex"), bytes };
186
+ } finally {
187
+ await handle.close();
188
+ }
189
+ }
190
+
191
+ function manifestsMatch(left: readonly LegacyEntrySnapshot[], right: readonly LegacyEntrySnapshot[]): boolean {
192
+ return (
193
+ left.length === right.length &&
194
+ left.every(
195
+ (entry, index) =>
196
+ entry.relativePath === right[index]?.relativePath &&
197
+ entry.dev === right[index]?.dev &&
198
+ entry.ino === right[index]?.ino &&
199
+ entry.size === right[index]?.size &&
200
+ entry.digest === right[index]?.digest,
201
+ )
202
+ );
203
+ }
204
+
205
+ async function captureLegacyManifest(root: string): Promise<readonly LegacyEntrySnapshot[]> {
206
+ const manifest: LegacyEntrySnapshot[] = [];
207
+ let copiedBytes = 0n;
208
+ const captureDirectory = async (directoryPath: string, relativePath: string): Promise<void> => {
209
+ const directory = await snapshotDirectory(directoryPath, relativePath);
210
+ manifest.push(directory);
211
+ const entries = (await fs.readdir(directoryPath, { withFileTypes: true })).sort((a, b) =>
212
+ a.name.localeCompare(b.name),
213
+ );
214
+ for (const entry of entries) {
215
+ const entryPath = path.join(directoryPath, entry.name);
216
+ const entryRelativePath = path.join(relativePath, entry.name);
217
+ if (entry.name === LEGACY_MIGRATION_MARKER) throw new Error("Unsafe legacy local:// migration source");
218
+ if (entry.isDirectory()) await captureDirectory(entryPath, entryRelativePath);
219
+ else {
220
+ const file = await snapshotRegularFile(entryPath, entryRelativePath);
221
+ copiedBytes += file.size;
222
+ if (copiedBytes > BigInt(MAX_LEGACY_LOCAL_BYTES))
223
+ throw new Error("Legacy local:// migration exceeds the safe size limit");
224
+ manifest.push(file);
225
+ }
226
+ }
227
+ const after = await fs.lstat(directoryPath, { bigint: true });
228
+ if (!matchesSnapshot(after, directory))
229
+ throw new Error("Legacy local:// migration source changed during capture");
230
+ };
231
+ await captureDirectory(root, "");
232
+ return manifest;
233
+ }
234
+
235
+ async function copyLegacyManifest(
236
+ source: string,
237
+ destination: string,
238
+ manifest: readonly LegacyEntrySnapshot[],
239
+ ): Promise<void> {
240
+ for (const entry of manifest) {
241
+ if (entry.relativePath === "") {
242
+ await fs.mkdir(destination, { mode: 0o700 });
243
+ continue;
244
+ }
245
+ const from = path.join(source, entry.relativePath);
246
+ const to = path.join(destination, entry.relativePath);
247
+ if (entry.digest === undefined) {
248
+ await fs.mkdir(to, { mode: 0o700 });
249
+ continue;
250
+ }
251
+ const captured = await snapshotRegularFile(from, entry.relativePath);
252
+ if (captured.digest !== entry.digest || !matchesSnapshot(captured, entry))
253
+ throw new Error("Legacy local:// migration source changed during capture");
254
+ if (entry.bytes === undefined) throw new Error("Legacy local:// migration snapshot is incomplete");
255
+ await fs.writeFile(to, entry.bytes, { mode: 0o600, flag: "wx" });
256
+ const copied = await fs.readFile(to);
257
+ if (createHash("sha256").update(copied).digest("hex") !== entry.digest)
258
+ throw new Error("Legacy local:// migration destination verification failed");
259
+ }
260
+ }
261
+
262
+ async function retireLegacyTree(legacyRoot: string, manifest: readonly LegacyEntrySnapshot[]): Promise<void> {
263
+ const root = manifest[0];
264
+ if (root?.relativePath !== "") throw new Error("Legacy local:// migration manifest has no root");
265
+ const before = await fs.lstat(legacyRoot, { bigint: true });
266
+ if (!matchesSnapshot(before, root)) throw new Error("Legacy local:// migration source changed during retirement");
267
+ const captured = snapshotDirectoryTree(legacyRoot);
268
+ if (!captured.ok || !captured.snapshot) {
269
+ throw new Error(`Legacy local:// migration retirement snapshot failed: ${captured.code ?? "unknown"}`);
270
+ }
271
+ if (captured.snapshot.entries.length !== manifest.length) {
272
+ throw new Error("Legacy local:// migration retirement manifest changed");
273
+ }
274
+ const expected = new Map(manifest.map(entry => [entry.relativePath.replaceAll(path.sep, "/"), entry]));
275
+ for (const entry of captured.snapshot.entries) {
276
+ const original = expected.get(entry.relativePath);
277
+ if (
278
+ !original ||
279
+ entry.dev !== original.dev.toString() ||
280
+ entry.ino !== original.ino.toString() ||
281
+ entry.size !== original.size.toString() ||
282
+ (entry.kind === "file" ? entry.sha256 !== original.digest : original.digest !== undefined)
283
+ ) {
284
+ throw new Error("Legacy local:// migration retirement authority differs from copied manifest");
285
+ }
286
+ }
287
+ const removed = exactRemoveDirectoryTree(legacyRoot, captured.snapshot);
288
+ if (!removed.ok) throw new Error(`Legacy local:// migration retirement failed: ${removed.code}`);
289
+ }
290
+ async function migrateManagedLegacyLocal(
291
+ source: ManagedLegacyLocalMigrationSource,
292
+ localRoot: string,
293
+ scratchParent: string,
294
+ marker: string,
295
+ ): Promise<void> {
296
+ const captured = await source.capture();
297
+ if (!captured) {
298
+ await fs.writeFile(marker, "absent\n", { mode: 0o600, flag: "wx" });
299
+ return;
300
+ }
301
+ let copiedBytes = 0;
302
+ for (const entry of captured.entries) {
303
+ if (entry.kind === "file") {
304
+ if (!entry.bytes || !entry.sha256) throw new Error("Legacy local:// migration snapshot is incomplete");
305
+ copiedBytes += entry.bytes.byteLength;
306
+ if (copiedBytes > MAX_LEGACY_LOCAL_BYTES)
307
+ throw new Error("Legacy local:// migration exceeds the safe size limit");
308
+ }
309
+ }
310
+ const staging = path.join(scratchParent, `.gjc-local-migration-${randomUUID()}`);
311
+ const installed: Array<{ readonly path: string; readonly dev: bigint; readonly ino: bigint }> = [];
312
+ let installedMarker: { readonly dev: bigint; readonly ino: bigint } | null = null;
313
+ try {
314
+ await fs.mkdir(staging, { mode: 0o700 });
315
+ for (const entry of captured.entries) {
316
+ if (entry.relativePath === "") continue;
317
+ const target = path.join(staging, entry.relativePath);
318
+ if (entry.kind === "directory") await fs.mkdir(target, { mode: 0o700 });
319
+ else {
320
+ await fs.writeFile(target, entry.bytes!, { mode: 0o600, flag: "wx" });
321
+ const copied = await fs.readFile(target);
322
+ if (createHash("sha256").update(copied).digest("hex") !== entry.sha256)
323
+ throw new Error("Legacy local:// migration destination verification failed");
324
+ }
325
+ }
326
+ for (const entry of await fs.readdir(staging)) {
327
+ const destination = path.join(localRoot, entry);
328
+ try {
329
+ await fs.lstat(destination);
330
+ throw new Error("Legacy local:// migration destination is ambiguous");
331
+ } catch (error) {
332
+ if (!isEnoent(error)) throw error;
333
+ }
334
+ await fs.rename(path.join(staging, entry), destination);
335
+ const identity = await fs.lstat(destination, { bigint: true });
336
+ installed.push({ path: destination, dev: identity.dev, ino: identity.ino });
337
+ }
338
+ await fs.writeFile(marker, "verified\n", { mode: 0o600, flag: "wx" });
339
+ const markerIdentity = await fs.lstat(marker, { bigint: true });
340
+ installedMarker = { dev: markerIdentity.dev, ino: markerIdentity.ino };
341
+ source.retire(captured.snapshot);
342
+ } catch (error) {
343
+ const currentMarker = await fs.lstat(marker, { bigint: true }).catch(() => null);
344
+ if (installedMarker && currentMarker?.dev === installedMarker.dev && currentMarker.ino === installedMarker.ino)
345
+ await fs.rm(marker, { force: true });
346
+ for (const destination of installed.reverse()) {
347
+ const current = await fs.lstat(destination.path, { bigint: true }).catch(() => null);
348
+ if (current?.dev === destination.dev && current.ino === destination.ino)
349
+ await fs.rm(destination.path, { recursive: true, force: true });
350
+ }
351
+ throw error;
352
+ } finally {
353
+ await fs.rm(staging, { recursive: true, force: true });
354
+ }
355
+ }
356
+
357
+ async function readMigrationMarker(marker: string): Promise<boolean> {
358
+ try {
359
+ const snapshot = await snapshotRegularFile(marker, LEGACY_MIGRATION_MARKER);
360
+ if (snapshot.size > 32n || snapshot.bytes === undefined) throw new Error("Unsafe local:// migration marker");
361
+ const value = snapshot.bytes.toString("utf8");
362
+ if (value !== "verified\n" && value !== "absent\n") throw new Error("Unsafe local:// migration marker");
363
+ return true;
364
+ } catch (error) {
365
+ if (isEnoent(error)) return false;
366
+ throw error;
367
+ }
368
+ }
369
+
370
+ async function migrateLegacyLocal(
371
+ options: LocalProtocolOptions,
372
+ localRoot: string,
373
+ scratchParent: string,
374
+ ): Promise<void> {
375
+ const marker = path.join(localRoot, LEGACY_MIGRATION_MARKER);
376
+ if (await readMigrationMarker(marker)) return;
377
+ const managedSource = options.getManagedLegacyLocalMigrationSource?.();
378
+ if (managedSource) {
379
+ await migrateManagedLegacyLocal(managedSource, localRoot, scratchParent, marker);
380
+ return;
381
+ }
112
382
  const artifactsDir = options.getArtifactsDir?.();
113
- if (artifactsDir) {
114
- return path.resolve(artifactsDir, "local");
383
+ if (!artifactsDir) {
384
+ await fs.writeFile(marker, "absent\n", { mode: 0o600, flag: "wx" });
385
+ return;
386
+ }
387
+ const legacyRoot = path.resolve(artifactsDir, "local");
388
+ let manifest: readonly LegacyEntrySnapshot[];
389
+ try {
390
+ manifest = await captureLegacyManifest(legacyRoot);
391
+ } catch (error) {
392
+ if (isEnoent(error)) {
393
+ await fs.writeFile(marker, "absent\n", { mode: 0o600, flag: "wx" });
394
+ return;
395
+ }
396
+ throw error;
115
397
  }
398
+ const staging = path.join(scratchParent, `.gjc-local-migration-${randomUUID()}`);
399
+ try {
400
+ await copyLegacyManifest(legacyRoot, staging, manifest);
401
+ const verifiedManifest = await captureLegacyManifest(legacyRoot);
402
+ if (!manifestsMatch(verifiedManifest, manifest))
403
+ throw new Error("Legacy local:// migration source changed during capture");
404
+ for (const entry of await fs.readdir(staging)) {
405
+ try {
406
+ await fs.lstat(path.join(localRoot, entry));
407
+ throw new Error("Legacy local:// migration destination is ambiguous");
408
+ } catch (error) {
409
+ if (!isEnoent(error)) throw error;
410
+ }
411
+ await fs.rename(path.join(staging, entry), path.join(localRoot, entry));
412
+ }
413
+ await retireLegacyTree(legacyRoot, manifest);
414
+ await fs.writeFile(marker, "verified\n", { mode: 0o600, flag: "wx" });
415
+ } finally {
416
+ await fs.rm(staging, { recursive: true, force: true });
417
+ }
418
+ }
419
+
420
+ const initializedLocalRoots = new Set<string>();
421
+
422
+ /**
423
+ * Completes the mandatory legacy migration gate for a local root.
424
+ * Call and await this before using the synchronous path resolver for reads or writes.
425
+ */
426
+ function explicitLocalRoot(options: LocalProtocolOptions): string | null {
427
+ if (options.isManagedDestination?.()) return null;
428
+ const artifactsDir = options.getArtifactsDir?.();
429
+ return artifactsDir ? path.resolve(artifactsDir, "local") : null;
430
+ }
116
431
 
117
- const sessionId = options.getSessionId?.() ?? "session";
118
- const safeSessionId = sessionId.replace(/[^a-zA-Z0-9_.-]/g, "_");
119
- return path.join(os.tmpdir(), "gjc-local", safeSessionId);
432
+ async function initializeExplicitLocalRoot(localRoot: string): Promise<string> {
433
+ await fs.mkdir(path.dirname(localRoot), { recursive: true });
434
+ await assertDirectoryNotSymlink(path.dirname(localRoot));
435
+ await fs.mkdir(localRoot, { recursive: true });
436
+ await assertDirectoryNotSymlink(localRoot);
437
+ return await fs.realpath(localRoot);
438
+ }
439
+
440
+ function initializeExplicitLocalRootSync(localRoot: string): string {
441
+ fsSync.mkdirSync(path.dirname(localRoot), { recursive: true });
442
+ const parentStat = fsSync.lstatSync(path.dirname(localRoot));
443
+ if (!parentStat.isDirectory() || parentStat.isSymbolicLink()) throw new Error("Unsafe local:// root");
444
+ fsSync.mkdirSync(localRoot, { recursive: true });
445
+ const rootStat = fsSync.lstatSync(localRoot);
446
+ if (!rootStat.isDirectory() || rootStat.isSymbolicLink()) throw new Error("Unsafe local:// root");
447
+ return fsSync.realpathSync(localRoot);
448
+ }
449
+
450
+ export async function initializeLocalRoot(options: LocalProtocolOptions): Promise<string> {
451
+ const explicitRoot = explicitLocalRoot(options);
452
+ if (explicitRoot) return await initializeExplicitLocalRoot(explicitRoot);
453
+ const localRoot = path.resolve(resolveLocalRoot(options));
454
+ const scratchParent = path.dirname(localRoot);
455
+
456
+ await fs.mkdir(scratchParent, { recursive: true, mode: 0o700 });
457
+ await assertDirectoryNotSymlink(scratchParent);
458
+ try {
459
+ await fs.mkdir(localRoot, { mode: 0o700 });
460
+ } catch (error) {
461
+ if (error instanceof Error && "code" in error && error.code === "EEXIST") {
462
+ await assertDirectoryNotSymlink(localRoot);
463
+ } else {
464
+ throw error;
465
+ }
466
+ }
467
+ await assertDirectoryNotSymlink(localRoot);
468
+ await migrateLegacyLocal(options, localRoot, scratchParent);
469
+
470
+ const resolvedScratchParent = await fs.realpath(scratchParent);
471
+ const resolvedLocalRoot = await fs.realpath(localRoot);
472
+ ensureWithinRoot(resolvedLocalRoot, resolvedScratchParent);
473
+ if (resolvedLocalRoot === resolvedScratchParent) {
474
+ throw new Error("Unsafe local:// root");
475
+ }
476
+ initializedLocalRoots.add(resolvedLocalRoot);
477
+ return resolvedLocalRoot;
478
+ }
479
+
480
+ function initializeLocalRootSyncWhenLegacyAbsent(options: LocalProtocolOptions, localRoot: string): void {
481
+ const explicitRoot = explicitLocalRoot(options);
482
+ if (explicitRoot) {
483
+ initializeExplicitLocalRootSync(explicitRoot);
484
+ return;
485
+ }
486
+ if (initializedLocalRoots.has(localRoot)) return;
487
+ const scratchParent = path.dirname(localRoot);
488
+ fsSync.mkdirSync(scratchParent, { recursive: true, mode: 0o700 });
489
+ const parentStat = fsSync.lstatSync(scratchParent);
490
+ if (!parentStat.isDirectory() || parentStat.isSymbolicLink()) throw new Error("Unsafe local:// root");
491
+ try {
492
+ fsSync.mkdirSync(localRoot, { mode: 0o700 });
493
+ } catch (error) {
494
+ if ((error as NodeJS.ErrnoException).code !== "EEXIST") throw error;
495
+ }
496
+ const rootStat = fsSync.lstatSync(localRoot);
497
+ if (!rootStat.isDirectory() || rootStat.isSymbolicLink()) throw new Error("Unsafe local:// root");
498
+ const marker = path.join(localRoot, LEGACY_MIGRATION_MARKER);
499
+ try {
500
+ const value = fsSync.readFileSync(marker, "utf8");
501
+ if (value !== "verified\n" && value !== "absent\n") throw new Error("Unsafe local:// migration marker");
502
+ initializedLocalRoots.add(localRoot);
503
+ return;
504
+ } catch (error) {
505
+ if (!isEnoent(error)) throw error;
506
+ }
507
+ if (options.getManagedLegacyLocalMigrationSource?.()) {
508
+ throw new Error("local:// legacy migration must complete before path resolution");
509
+ }
510
+ const artifactsDir = options.getArtifactsDir?.();
511
+ if (artifactsDir && fsSync.existsSync(path.resolve(artifactsDir, "local"))) {
512
+ throw new Error("local:// legacy migration must complete before path resolution");
513
+ }
514
+ try {
515
+ fsSync.writeFileSync(marker, "absent\n", { mode: 0o600, flag: "wx" });
516
+ } catch (error) {
517
+ if ((error as NodeJS.ErrnoException).code !== "EEXIST") throw error;
518
+ if (fsSync.readFileSync(marker, "utf8") !== "absent\n") throw new Error("Unsafe local:// migration marker");
519
+ }
520
+ initializedLocalRoots.add(localRoot);
521
+ }
522
+
523
+ export function resolveLocalRoot(options: LocalProtocolOptions): string {
524
+ return explicitLocalRoot(options) ?? path.join(os.tmpdir(), "gjc-local", safeSessionId(options));
120
525
  }
121
526
 
122
527
  export function resolveLocalUrlToPath(input: string | InternalUrl, options: LocalProtocolOptions): string {
123
528
  const url = typeof input === "string" ? parseLocalUrl(input) : input;
124
529
  const localRoot = path.resolve(resolveLocalRoot(options));
530
+ initializeLocalRootSyncWhenLegacyAbsent(options, localRoot);
125
531
  const relativePath = extractRelativePath(url);
126
532
 
127
533
  if (!relativePath) {
@@ -177,6 +583,7 @@ export class LocalProtocolHandler implements ProtocolHandler {
177
583
  static resetOverrideForTests(): void {
178
584
  LocalProtocolHandler.#override = undefined;
179
585
  LocalProtocolHandler.#ownedOverrides = [];
586
+ initializedLocalRoots.clear();
180
587
  }
181
588
 
182
589
  /**
@@ -199,6 +606,8 @@ export class LocalProtocolHandler implements ProtocolHandler {
199
606
  if (!sessionManager) return undefined;
200
607
  return {
201
608
  getArtifactsDir: () => sessionManager.getArtifactsDir(),
609
+ isManagedDestination: () => sessionManager.isManagedDestination(),
610
+ getManagedLegacyLocalMigrationSource: () => sessionManager.getManagedLegacyLocalMigrationSource(),
202
611
  getSessionId: () => sessionManager.getSessionId(),
203
612
  };
204
613
  }
@@ -209,18 +618,7 @@ export class LocalProtocolHandler implements ProtocolHandler {
209
618
  throw new Error("No session - local:// unavailable");
210
619
  }
211
620
 
212
- const localRoot = path.resolve(resolveLocalRoot(opts));
213
- await fs.mkdir(localRoot, { recursive: true });
214
-
215
- let resolvedRoot: string;
216
- try {
217
- resolvedRoot = await fs.realpath(localRoot);
218
- } catch (error) {
219
- if (isEnoent(error)) {
220
- throw new Error("Unable to initialize local:// root");
221
- }
222
- throw error;
223
- }
621
+ const resolvedRoot = await initializeLocalRoot(opts);
224
622
 
225
623
  const relativePath = extractRelativePath(url);
226
624
  const targetPath = relativePath ? path.resolve(resolvedRoot, relativePath) : resolvedRoot;
package/src/lsp/index.ts CHANGED
@@ -461,88 +461,6 @@ async function waitForDiagnostics(
461
461
  return getAcceptedDiagnostics(client.diagnostics.get(uri), expectedDocumentVersion, allowUnversioned) ?? [];
462
462
  }
463
463
 
464
- /** Project type detection result */
465
- interface ProjectType {
466
- type: "rust" | "typescript" | "go" | "python" | "unknown";
467
- command?: string[];
468
- description: string;
469
- }
470
-
471
- /** Detect project type from root markers */
472
- function detectProjectType(cwd: string): ProjectType {
473
- // Check for Rust (Cargo.toml)
474
- if (fs.existsSync(path.join(cwd, "Cargo.toml"))) {
475
- return { type: "rust", command: ["cargo", "check", "--message-format=short"], description: "Rust (cargo check)" };
476
- }
477
-
478
- // Check for TypeScript (tsconfig.json)
479
- if (fs.existsSync(path.join(cwd, "tsconfig.json"))) {
480
- return { type: "typescript", command: ["npx", "tsc", "--noEmit"], description: "TypeScript (tsc --noEmit)" };
481
- }
482
-
483
- // Check for Go (go.mod)
484
- if (fs.existsSync(path.join(cwd, "go.mod"))) {
485
- return { type: "go", command: ["go", "build", "./..."], description: "Go (go build)" };
486
- }
487
-
488
- // Check for Python (pyproject.toml or pyrightconfig.json)
489
- if (fs.existsSync(path.join(cwd, "pyproject.toml")) || fs.existsSync(path.join(cwd, "pyrightconfig.json"))) {
490
- return { type: "python", command: ["pyright"], description: "Python (pyright)" };
491
- }
492
-
493
- return { type: "unknown", description: "Unknown project type" };
494
- }
495
-
496
- /** Run workspace diagnostics command and parse output */
497
- async function runWorkspaceDiagnostics(
498
- cwd: string,
499
- signal?: AbortSignal,
500
- ): Promise<{ output: string; projectType: ProjectType }> {
501
- throwIfAborted(signal);
502
- const projectType = detectProjectType(cwd);
503
- if (!projectType.command) {
504
- return {
505
- output: `Cannot detect project type. Supported: Rust (Cargo.toml), TypeScript (tsconfig.json), Go (go.mod), Python (pyproject.toml)`,
506
- projectType,
507
- };
508
- }
509
- const proc = Bun.spawn(projectType.command, {
510
- cwd,
511
- stdout: "pipe",
512
- stderr: "pipe",
513
- windowsHide: true,
514
- });
515
- const abortHandler = () => {
516
- proc.kill();
517
- };
518
- if (signal) {
519
- signal.addEventListener("abort", abortHandler, { once: true });
520
- }
521
-
522
- try {
523
- const [stdout, stderr] = await Promise.all([new Response(proc.stdout).text(), new Response(proc.stderr).text()]);
524
- await proc.exited;
525
- throwIfAborted(signal);
526
- const combined = (stdout + stderr).trim();
527
- if (!combined) {
528
- return { output: "No issues found", projectType };
529
- }
530
- // Limit output length
531
- const lines = combined.split("\n");
532
- if (lines.length > 50) {
533
- return { output: `${lines.slice(0, 50).join("\n")}\n... and ${lines.length - 50} more lines`, projectType };
534
- }
535
- return { output: combined, projectType };
536
- } catch (e) {
537
- if (signal?.aborted) {
538
- throw new ToolAbortError();
539
- }
540
- return { output: `Failed to run ${projectType.command.join(" ")}: ${e}`, projectType };
541
- } finally {
542
- signal?.removeEventListener("abort", abortHandler);
543
- }
544
- }
545
-
546
464
  /** Result from getDiagnosticsForFile */
547
465
  export interface FileDiagnosticsResult {
548
466
  /** Name of the LSP server used (if available) */
@@ -1235,16 +1153,14 @@ export class LspTool implements AgentTool<typeof lspSchema, LspToolDetails, Them
1235
1153
  // Diagnostics can be batch or single-file - queries all applicable servers
1236
1154
  if (action === "diagnostics") {
1237
1155
  if (file === "*") {
1238
- // `*` => run workspace diagnostics across all configured servers
1239
- const result = await runWorkspaceDiagnostics(this.session.cwd, signal);
1240
1156
  return {
1241
1157
  content: [
1242
1158
  {
1243
1159
  type: "text",
1244
- text: `Workspace diagnostics (${result.projectType.description}):\n${result.output}`,
1160
+ text: "Workspace build diagnostics are unavailable via lsp. Use a concrete file path or glob for language-server diagnostics, and use an execution-authorized tool to run build or typecheck commands.",
1245
1161
  },
1246
1162
  ],
1247
- details: { action, success: true, request: params },
1163
+ details: { action, success: false, request: params },
1248
1164
  };
1249
1165
  }
1250
1166
 
@@ -1253,7 +1169,7 @@ export class LspTool implements AgentTool<typeof lspSchema, LspToolDetails, Them
1253
1169
  content: [
1254
1170
  {
1255
1171
  type: "text",
1256
- text: "Error: file parameter required. Use `*` for workspace-wide diagnostics or a path/glob for specific files.",
1172
+ text: "Error: file parameter required. Use a concrete file path or glob for diagnostics.",
1257
1173
  },
1258
1174
  ],
1259
1175
  details: { action, success: false, request: params },
@@ -1281,7 +1197,7 @@ export class LspTool implements AgentTool<typeof lspSchema, LspToolDetails, Them
1281
1197
  const allServerNames = new Set<string>();
1282
1198
  if (truncatedGlobTargets) {
1283
1199
  results.push(
1284
- `${theme.status.warning} Pattern matched more than ${MAX_GLOB_DIAGNOSTIC_TARGETS} files; showing first ${MAX_GLOB_DIAGNOSTIC_TARGETS}. Narrow the glob or use workspace diagnostics.`,
1200
+ `${theme.status.warning} Pattern matched more than ${MAX_GLOB_DIAGNOSTIC_TARGETS} files; showing first ${MAX_GLOB_DIAGNOSTIC_TARGETS}. Narrow the glob for broader coverage.`,
1285
1201
  );
1286
1202
  }
1287
1203