jeopi-cli 16.2.30 → 16.4.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 (44) hide show
  1. package/CHANGELOG.md +32 -0
  2. package/dist/cli.js +2824 -2736
  3. package/dist/types/config/settings-schema.d.ts +32 -2
  4. package/dist/types/goals/runtime.d.ts +16 -0
  5. package/dist/types/goals/verifier.d.ts +9 -0
  6. package/dist/types/memories/index.d.ts +9 -0
  7. package/dist/types/memory-backend/types.d.ts +11 -0
  8. package/dist/types/session/agent-session.d.ts +22 -7
  9. package/dist/types/slash-commands/helpers/skill-verify.d.ts +5 -0
  10. package/dist/types/task/model-tier-classifier.d.ts +8 -0
  11. package/dist/types/tools/builtin-names.d.ts +1 -1
  12. package/dist/types/tools/index.d.ts +1 -0
  13. package/dist/types/tools/learn.d.ts +4 -0
  14. package/dist/types/tools/path-write-lock.d.ts +10 -0
  15. package/dist/types/tools/vision-verify.d.ts +34 -0
  16. package/package.json +12 -12
  17. package/src/autolearn/controller.ts +21 -5
  18. package/src/autoresearch/index.ts +60 -6
  19. package/src/config/settings-schema.ts +38 -2
  20. package/src/edit/index.ts +2 -1
  21. package/src/goals/runtime.ts +51 -0
  22. package/src/goals/verifier.ts +100 -0
  23. package/src/internal-urls/docs-index.generated.txt +2 -2
  24. package/src/memories/index.ts +10 -2
  25. package/src/memory-backend/local-backend.ts +34 -0
  26. package/src/memory-backend/types.ts +11 -0
  27. package/src/prompts/agents/designer.md +1 -0
  28. package/src/prompts/agents/goal-verifier.md +51 -0
  29. package/src/prompts/tools/learn.md +2 -0
  30. package/src/prompts/tools/vision-verify-system.md +16 -0
  31. package/src/prompts/tools/vision-verify.md +20 -0
  32. package/src/sdk.ts +2 -0
  33. package/src/session/agent-session.ts +51 -8
  34. package/src/slash-commands/builtin-registry.ts +9 -0
  35. package/src/slash-commands/helpers/skill-verify.ts +171 -0
  36. package/src/task/agents.ts +2 -0
  37. package/src/task/index.ts +26 -1
  38. package/src/task/model-tier-classifier.ts +43 -0
  39. package/src/tools/builtin-names.ts +1 -0
  40. package/src/tools/index.ts +4 -0
  41. package/src/tools/learn.ts +16 -1
  42. package/src/tools/path-write-lock.ts +44 -0
  43. package/src/tools/vision-verify.ts +347 -0
  44. package/src/tools/write.ts +12 -9
@@ -66,6 +66,7 @@ import { SearchToolBm25Tool } from "./search-tool-bm25";
66
66
  import { loadSshTool } from "./ssh";
67
67
  import { isSubagentToolEnabled, SubagentTool } from "./subagent";
68
68
  import { type TodoPhase, TodoTool } from "./todo";
69
+ import { VisionVerifyTool } from "./vision-verify";
69
70
  import { WriteTool } from "./write";
70
71
  import { YieldTool } from "./yield";
71
72
 
@@ -106,6 +107,7 @@ export * from "./ssh";
106
107
  export * from "./subagent";
107
108
  export * from "./todo";
108
109
  export * from "./tts";
110
+ export * from "./vision-verify";
109
111
  export * from "./write";
110
112
  export * from "./yield";
111
113
 
@@ -466,6 +468,7 @@ export const BUILTIN_TOOLS: Record<BuiltinToolName, ToolFactory> = {
466
468
  grep: s => new GrepTool(s),
467
469
  lsp: LspTool.createIf,
468
470
  inspect_image: s => new InspectImageTool(s),
471
+ vision_verify: s => new VisionVerifyTool(s),
469
472
  browser: s => new BrowserTool(s),
470
473
  checkpoint: CheckpointTool.createIf,
471
474
  rewind: RewindTool.createIf,
@@ -621,6 +624,7 @@ export async function createTools(session: ToolSession, toolNames?: string[]): P
621
624
  if (name === "ast_grep") return session.settings.get("astGrep.enabled");
622
625
  if (name === "ast_edit") return session.settings.get("astEdit.enabled");
623
626
  if (name === "inspect_image") return session.settings.get("inspect_image.enabled");
627
+ if (name === "vision_verify") return session.settings.get("vision_verify.enabled");
624
628
  if (name === "web_search") return session.settings.get("web_search.enabled");
625
629
  // search_tool_bm25 is allowed when either legacy mcp.discoveryMode or new tools.discoveryMode is active.
626
630
  if (name === "search_tool_bm25") return discoveryActive;
@@ -9,6 +9,12 @@ import type { ToolSession } from ".";
9
9
  const learnSchema = type({
10
10
  memory: type("string").describe("the durable, self-contained lesson to remember (what, when, why)"),
11
11
  "context?": type("string").describe("optional source context for the lesson"),
12
+ "verified?": type("boolean").describe(
13
+ "true only if you independently re-checked this claim against the repo (ran a command, read a file, ran a test) rather than merely inferring it",
14
+ ),
15
+ "evidence?": type("string").describe(
16
+ "the short check that confirmed the claim (a command, a file path, a test name); only meaningful when verified is true",
17
+ ),
12
18
  "skill?": type({
13
19
  action: "'create' | 'update'",
14
20
  name: type("string").describe("kebab-case skill name"),
@@ -65,6 +71,8 @@ export class LearnTool implements AgentTool<typeof learnSchema> {
65
71
  cwd: state.session.sessionManager.getCwd(),
66
72
  context: params.context ?? null,
67
73
  tool: "learn",
74
+ verified: params.verified ?? false,
75
+ evidence: params.evidence ?? null,
68
76
  },
69
77
  scope: "bank",
70
78
  extract: true,
@@ -81,7 +89,14 @@ export class LearnTool implements AgentTool<typeof learnSchema> {
81
89
  } else if (backend === "local") {
82
90
  const result = await localBackend.save?.(
83
91
  { agentDir: this.session.settings.getAgentDir(), cwd: this.session.settings.getCwd() },
84
- { content: params.memory, context: params.context, source: "coding-agent-learn", importance: 0.8 },
92
+ {
93
+ content: params.memory,
94
+ context: params.context,
95
+ source: "coding-agent-learn",
96
+ importance: 0.8,
97
+ verified: params.verified,
98
+ evidence: params.evidence,
99
+ },
85
100
  );
86
101
  if (!result || result.stored === 0) {
87
102
  throw new Error("Lesson was empty after sanitization; nothing stored.");
@@ -0,0 +1,44 @@
1
+ import type { WritethroughCallback } from "../lsp";
2
+
3
+ /**
4
+ * Non-isolated concurrent subagents (`task.isolation.mode="none"`, the
5
+ * default) share the parent session's cwd with no other collision-avoidance
6
+ * mechanism: two sibling subagents editing the same file can each perform an
7
+ * unguarded read-modify-write, and the interleaving of their disk writes can
8
+ * silently drop one of them. This wrapper does not prevent LOGICAL conflicts
9
+ * (two agents making incompatible edits to the same file) — it only
10
+ * serializes the physical writethrough call per destination path, so one
11
+ * write can no longer be lost mid-flight to another write racing the same
12
+ * path.
13
+ */
14
+
15
+ /** Per-path write chains serializing the writethrough callback for a given destination. */
16
+ const pathWriteChains = new Map<string, Promise<unknown>>();
17
+
18
+ /**
19
+ * Wrap a {@link WritethroughCallback} so concurrent writes to the same `dst`
20
+ * are serialized instead of racing. `dst` is used as-is for the chain key:
21
+ * every caller (edit's hashline/patch/replace paths, and the write tool)
22
+ * resolves `dst` to an absolute path via `resolvePlanPath`/`resolveToCwd`
23
+ * before invoking the writethrough, so the same logical file always arrives
24
+ * under the same string.
25
+ */
26
+ export function withPathWriteLock(callback: WritethroughCallback): WritethroughCallback {
27
+ return async (dst, content, signal, file, batch, getDeferred) => {
28
+ // Serialize the writethrough per path: parallel edits to the same file
29
+ // (sibling subagents sharing a cwd, or two shared tool calls in one
30
+ // turn) would otherwise let concurrent disk writes race and drop one.
31
+ const run = (pathWriteChains.get(dst) ?? Promise.resolve()).then(() =>
32
+ callback(dst, content, signal, file, batch, getDeferred),
33
+ );
34
+ const guarded = run.catch(() => {});
35
+ pathWriteChains.set(dst, guarded);
36
+ try {
37
+ return await run;
38
+ } finally {
39
+ // Drop the entry once this write is the chain tail, so the map does
40
+ // not retain one promise per distinct path for the process lifetime.
41
+ if (pathWriteChains.get(dst) === guarded) pathWriteChains.delete(dst);
42
+ }
43
+ };
44
+ }
@@ -0,0 +1,347 @@
1
+ import { type } from "arktype";
2
+ import type { AgentTool, AgentToolContext, AgentToolResult, AgentToolUpdateCallback } from "jeopi-agent-core";
3
+ import { instrumentedCompleteSimple, resolveTelemetry } from "jeopi-agent-core";
4
+ import { type Api, completeSimple, type ImageContent, type Model, type ToolExample } from "jeopi-ai";
5
+ import { prompt } from "jeopi-utils";
6
+ import { extractTextContent } from "../commit/utils";
7
+
8
+ import { expandRoleAlias, getModelMatchPreferences, resolveModelFromString } from "../config/model-resolver";
9
+ import visionVerifyDescription from "../prompts/tools/vision-verify.md" with { type: "text" };
10
+ import visionVerifySystemPromptTemplate from "../prompts/tools/vision-verify-system.md" with { type: "text" };
11
+ import {
12
+ ImageInputTooLargeError,
13
+ type LoadedImageInput,
14
+ loadImageAttachmentInput,
15
+ loadImageInput,
16
+ MAX_IMAGE_INPUT_BYTES,
17
+ webpExclusionForModel,
18
+ } from "../utils/image-loading";
19
+ import type { ToolSession } from "./index";
20
+ import { ToolError } from "./tool-errors";
21
+
22
+ const visionVerifySchema = type({
23
+ goal: type("string").describe("what the UI/artifact should look like or achieve"),
24
+ screenshot: type("string").describe(
25
+ "path to the current screenshot, or an attachment reference like inspect_image accepts",
26
+ ),
27
+ "baseline?": type("string").describe(
28
+ "optional path to a PRIOR screenshot of the same view, for regression comparison",
29
+ ),
30
+ "+": "reject",
31
+ });
32
+
33
+ export type VisionVerifyParams = typeof visionVerifySchema.infer;
34
+
35
+ interface ImageAttachmentReference {
36
+ index: number;
37
+ }
38
+
39
+ const IMAGE_ATTACHMENT_REFERENCE_REGEX =
40
+ /^\s*(?:\[?Image #([1-9]\d*)(?:,[^\]\n]*)?\]?|(?:attachment|image):\/\/([1-9]\d*))\s*$/i;
41
+
42
+ function parseImageAttachmentReference(path: string): ImageAttachmentReference | null {
43
+ const match = IMAGE_ATTACHMENT_REFERENCE_REGEX.exec(path);
44
+ if (!match) return null;
45
+ const rawIndex = match[1] ?? match[2];
46
+ if (!rawIndex) return null;
47
+ return { index: Number(rawIndex) };
48
+ }
49
+
50
+ function formatAvailableImageAttachments(attachments: readonly { label: string; uri: string }[]): string {
51
+ if (attachments.length === 0) return "none";
52
+ return attachments.map(attachment => `${attachment.label} -> ${attachment.uri}`).join(", ");
53
+ }
54
+
55
+ async function loadAttachmentReferenceInput(options: {
56
+ path: string;
57
+ reference: ImageAttachmentReference;
58
+ attachments: readonly { label: string; uri: string; image: ImageContent }[];
59
+ autoResize: boolean;
60
+ excludeWebP: boolean | undefined;
61
+ }): Promise<LoadedImageInput | null> {
62
+ const attachment = options.attachments[options.reference.index - 1];
63
+ if (!attachment) {
64
+ const available = formatAvailableImageAttachments(options.attachments);
65
+ if (options.attachments.length === 0) {
66
+ throw new ToolError(
67
+ `No image attachments are available in this turn. path="${options.path}" must be a readable file path or attachment URI.`,
68
+ );
69
+ }
70
+ throw new ToolError(
71
+ `Could not resolve image attachment '${options.path}'. Available image attachments: ${available}. Pass an attachment URI or a readable filesystem path.`,
72
+ );
73
+ }
74
+ return loadImageAttachmentInput({
75
+ image: attachment.image,
76
+ label: attachment.label,
77
+ uri: attachment.uri,
78
+ autoResize: options.autoResize,
79
+ maxBytes: MAX_IMAGE_INPUT_BYTES,
80
+ excludeWebP: options.excludeWebP,
81
+ });
82
+ }
83
+
84
+ /** Resolve a `screenshot`/`baseline` param to a loaded image, honoring attachment references the same way `inspect_image` does. */
85
+ async function resolveVisionVerifyImageInput(options: {
86
+ path: string;
87
+ session: ToolSession;
88
+ autoResize: boolean;
89
+ excludeWebP: boolean | undefined;
90
+ }): Promise<LoadedImageInput | null> {
91
+ const attachmentReference = parseImageAttachmentReference(options.path);
92
+ if (attachmentReference) {
93
+ return loadAttachmentReferenceInput({
94
+ path: options.path,
95
+ reference: attachmentReference,
96
+ attachments: options.session.getImageAttachments?.() ?? [],
97
+ autoResize: options.autoResize,
98
+ excludeWebP: options.excludeWebP,
99
+ });
100
+ }
101
+ return loadImageInput({
102
+ path: options.path,
103
+ cwd: options.session.cwd,
104
+ autoResize: options.autoResize,
105
+ maxBytes: MAX_IMAGE_INPUT_BYTES,
106
+ excludeWebP: options.excludeWebP,
107
+ });
108
+ }
109
+
110
+ async function loadVisionVerifyImage(options: {
111
+ path: string;
112
+ label: string;
113
+ session: ToolSession;
114
+ autoResize: boolean;
115
+ excludeWebP: boolean | undefined;
116
+ }): Promise<LoadedImageInput> {
117
+ let imageInput: LoadedImageInput | null;
118
+ try {
119
+ imageInput = await resolveVisionVerifyImageInput(options);
120
+ } catch (error) {
121
+ if (error instanceof ImageInputTooLargeError) {
122
+ throw new ToolError(error.message);
123
+ }
124
+ throw error;
125
+ }
126
+ if (!imageInput) {
127
+ throw new ToolError(
128
+ `vision_verify only supports PNG, JPEG, GIF, and WEBP files detected by file content (${options.label}="${options.path}").`,
129
+ );
130
+ }
131
+ return imageInput;
132
+ }
133
+
134
+ /** Structured verdict the model is instructed to return. */
135
+ interface VisionVerifyVerdict {
136
+ matches: boolean;
137
+ summary: string;
138
+ gaps: string[];
139
+ }
140
+
141
+ function isVisionVerifyVerdict(value: unknown): value is VisionVerifyVerdict {
142
+ if (!value || typeof value !== "object") return false;
143
+ const candidate = value as Record<string, unknown>;
144
+ return (
145
+ typeof candidate.matches === "boolean" &&
146
+ typeof candidate.summary === "string" &&
147
+ Array.isArray(candidate.gaps) &&
148
+ candidate.gaps.every(gap => typeof gap === "string")
149
+ );
150
+ }
151
+
152
+ function parseVisionVerifyVerdict(text: string): VisionVerifyVerdict {
153
+ let parsed: unknown;
154
+ try {
155
+ const trimmed = text.trim();
156
+ const jsonText = trimmed.startsWith("{") ? trimmed : (trimmed.match(/\{[\s\S]*\}/)?.[0] ?? trimmed);
157
+ parsed = JSON.parse(jsonText);
158
+ } catch {
159
+ throw new ToolError(`vision_verify model response was not valid JSON. Raw response: ${text}`, {
160
+ rawResponse: text,
161
+ });
162
+ }
163
+ if (!isVisionVerifyVerdict(parsed)) {
164
+ throw new ToolError(
165
+ `vision_verify model response did not match the expected {matches, summary, gaps} shape. Raw response: ${text}`,
166
+ { rawResponse: text },
167
+ );
168
+ }
169
+ return parsed;
170
+ }
171
+
172
+ function renderVisionVerifySummary(verdict: VisionVerifyVerdict): string {
173
+ const lines = [verdict.matches ? "Matches: yes" : "Matches: no", verdict.summary];
174
+ if (verdict.gaps.length > 0) {
175
+ lines.push("Gaps:");
176
+ for (const gap of verdict.gaps) lines.push(`- ${gap}`);
177
+ }
178
+ return lines.join("\n");
179
+ }
180
+
181
+ export interface VisionVerifyToolDetails {
182
+ model: string;
183
+ matches: boolean;
184
+ gaps: string[];
185
+ }
186
+
187
+ export class VisionVerifyTool implements AgentTool<typeof visionVerifySchema, VisionVerifyToolDetails> {
188
+ readonly name = "vision_verify";
189
+ readonly approval = "read" as const;
190
+ readonly label = "VisionVerify";
191
+ readonly loadMode = "discoverable";
192
+ readonly summary = "Check a screenshot/artifact against a stated goal with a vision model";
193
+ readonly description: string;
194
+ readonly parameters = visionVerifySchema;
195
+ readonly strict = false;
196
+
197
+ readonly examples: readonly ToolExample<typeof visionVerifySchema.infer>[] = [
198
+ {
199
+ caption: "Verify a UI change matches the task goal",
200
+ call: {
201
+ goal: "Settings page shows a 'Save' button that is disabled until a field changes",
202
+ screenshot: "screenshots/settings-after.png",
203
+ },
204
+ },
205
+ {
206
+ caption: "Regression check against a prior known-good state",
207
+ call: {
208
+ goal: "Header layout unchanged aside from the new notification icon",
209
+ screenshot: "screenshots/header-after.png",
210
+ baseline: "screenshots/header-before.png",
211
+ },
212
+ },
213
+ ];
214
+
215
+ constructor(
216
+ private readonly session: ToolSession,
217
+ private readonly completeImageRequest: typeof completeSimple = completeSimple,
218
+ ) {
219
+ this.description = prompt.render(visionVerifyDescription);
220
+ }
221
+
222
+ async execute(
223
+ _toolCallId: string,
224
+ params: VisionVerifyParams,
225
+ signal?: AbortSignal,
226
+ _onUpdate?: AgentToolUpdateCallback<VisionVerifyToolDetails>,
227
+ _context?: AgentToolContext,
228
+ ): Promise<AgentToolResult<VisionVerifyToolDetails>> {
229
+ if (this.session.settings.get("images.blockImages")) {
230
+ throw new ToolError(
231
+ "Image submission is disabled by settings (images.blockImages=true). Disable it to use vision_verify.",
232
+ );
233
+ }
234
+
235
+ const modelRegistry = this.session.modelRegistry;
236
+ if (!modelRegistry) {
237
+ throw new ToolError("Model registry is unavailable for vision_verify.");
238
+ }
239
+
240
+ const availableModels = modelRegistry.getAvailable();
241
+ if (availableModels.length === 0) {
242
+ throw new ToolError("No models available for vision_verify.");
243
+ }
244
+
245
+ const matchPreferences = getModelMatchPreferences(this.session.settings);
246
+ const resolvePattern = (pattern: string | undefined): Model<Api> | undefined => {
247
+ if (!pattern) return undefined;
248
+ const expanded = expandRoleAlias(pattern, this.session.settings);
249
+ return resolveModelFromString(expanded, availableModels, matchPreferences);
250
+ };
251
+
252
+ const activeModelPattern = this.session.getActiveModelString?.() ?? this.session.getModelString?.();
253
+ const model =
254
+ resolvePattern("pi/vision") ??
255
+ resolvePattern("pi/default") ??
256
+ resolvePattern(activeModelPattern) ??
257
+ availableModels[0];
258
+ if (!model) {
259
+ throw new ToolError("Unable to resolve a model for vision_verify.");
260
+ }
261
+
262
+ if (!model.input.includes("image")) {
263
+ throw new ToolError(
264
+ `Resolved model ${model.provider}/${model.id} does not support image input. Configure a vision-capable model for modelRoles.vision.`,
265
+ );
266
+ }
267
+
268
+ const apiKey = await modelRegistry.getApiKey(model);
269
+ if (!apiKey) {
270
+ throw new ToolError(
271
+ `No API key available for ${model.provider}/${model.id}. Configure credentials for this provider or choose another vision-capable model.`,
272
+ );
273
+ }
274
+
275
+ const autoResize = this.session.settings.get("images.autoResize");
276
+ const excludeWebP = webpExclusionForModel(model);
277
+
278
+ const currentImage = await loadVisionVerifyImage({
279
+ path: params.screenshot,
280
+ label: "screenshot",
281
+ session: this.session,
282
+ autoResize,
283
+ excludeWebP,
284
+ });
285
+ const baselineImage = params.baseline
286
+ ? await loadVisionVerifyImage({
287
+ path: params.baseline,
288
+ label: "baseline",
289
+ session: this.session,
290
+ autoResize,
291
+ excludeWebP,
292
+ })
293
+ : undefined;
294
+
295
+ const userContent: Array<{ type: "text"; text: string } | { type: "image"; data: string; mimeType: string }> = [];
296
+ if (baselineImage) {
297
+ userContent.push({ type: "text", text: "Baseline (prior):" });
298
+ userContent.push({ type: "image", data: baselineImage.data, mimeType: baselineImage.mimeType });
299
+ userContent.push({ type: "text", text: "Current:" });
300
+ }
301
+ userContent.push({ type: "image", data: currentImage.data, mimeType: currentImage.mimeType });
302
+ userContent.push({ type: "text", text: `Goal: ${params.goal}` });
303
+
304
+ const telemetry = resolveTelemetry(this.session.getTelemetry?.(), this.session.getSessionId?.() ?? undefined);
305
+ const response = await instrumentedCompleteSimple(
306
+ model,
307
+ {
308
+ systemPrompt: [prompt.render(visionVerifySystemPromptTemplate)],
309
+ messages: [
310
+ {
311
+ role: "user",
312
+ content: userContent,
313
+ timestamp: Date.now(),
314
+ },
315
+ ],
316
+ },
317
+ {
318
+ apiKey: modelRegistry.resolver(model, this.session.getSessionId?.() ?? undefined),
319
+ signal,
320
+ },
321
+ { telemetry, oneshotKind: "vision_verify", completeImpl: this.completeImageRequest },
322
+ );
323
+
324
+ if (response.stopReason === "error") {
325
+ throw new ToolError(response.errorMessage ?? "vision_verify request failed.");
326
+ }
327
+ if (response.stopReason === "aborted") {
328
+ throw new ToolError("vision_verify request aborted.");
329
+ }
330
+
331
+ const text = extractTextContent(response);
332
+ if (!text) {
333
+ throw new ToolError("vision_verify model returned no text output.");
334
+ }
335
+
336
+ const verdict = parseVisionVerifyVerdict(text);
337
+
338
+ return {
339
+ content: [{ type: "text", text: renderVisionVerifySummary(verdict) }],
340
+ details: {
341
+ model: `${model.provider}/${model.id}`,
342
+ matches: verdict.matches,
343
+ gaps: verdict.gaps,
344
+ },
345
+ };
346
+ }
347
+ }
@@ -41,6 +41,7 @@ import {
41
41
  import { invalidateFsScanAfterWrite } from "./fs-cache-invalidation";
42
42
  import { type OutputMeta, outputMeta } from "./output-meta";
43
43
  import { formatPathRelativeToCwd, isInternalUrlPath, pathTargetsSsh, peelWriteUrlSelector } from "./path-utils";
44
+ import { withPathWriteLock } from "./path-write-lock";
44
45
  import { enforcePlanModeWrite, resolvePlanPath, unwrapHashlineHeaderPath } from "./plan-mode-guard";
45
46
  import {
46
47
  cachedRenderedString,
@@ -322,15 +323,17 @@ export class WriteTool implements AgentTool<typeof writeSchema, WriteToolDetails
322
323
  const enableFormat = enableLsp && session.settings.get("lsp.formatOnWrite");
323
324
  const enableDiagnostics = enableLsp && session.settings.get("lsp.diagnosticsOnWrite");
324
325
  const dedup = enableDiagnostics && session.settings.get("lsp.diagnosticsDeduplicate");
325
- this.#writethrough = enableLsp
326
- ? createLspWritethrough(session.cwd, {
327
- enableFormat,
328
- enableDiagnostics,
329
- transformDiagnostics: dedup
330
- ? (path, result) => getDiagnosticsLedger(session).reduce(path, result)
331
- : undefined,
332
- })
333
- : writethroughNoop;
326
+ this.#writethrough = withPathWriteLock(
327
+ enableLsp
328
+ ? createLspWritethrough(session.cwd, {
329
+ enableFormat,
330
+ enableDiagnostics,
331
+ transformDiagnostics: dedup
332
+ ? (path, result) => getDiagnosticsLedger(session).reduce(path, result)
333
+ : undefined,
334
+ })
335
+ : writethroughNoop,
336
+ );
334
337
  this.description = prompt.render(writeDescription);
335
338
  }
336
339