open-agents-ai 0.187.554 → 0.187.556

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.
package/dist/index.js CHANGED
@@ -2877,6 +2877,43 @@ Annotated files:`);
2877
2877
  }
2878
2878
  });
2879
2879
 
2880
+ // packages/execution/dist/tools/edit-metadata.js
2881
+ import { createHash } from "node:crypto";
2882
+ function contentHash(content) {
2883
+ return createHash("sha256").update(content, "utf8").digest("hex");
2884
+ }
2885
+ function normalizeExpectedHash(value2) {
2886
+ if (typeof value2 !== "string")
2887
+ return null;
2888
+ const raw = value2.trim();
2889
+ if (!raw)
2890
+ return null;
2891
+ const stripped = raw.replace(/^sha256:/i, "").toLowerCase();
2892
+ return /^[a-f0-9]{64}$/.test(stripped) ? stripped : null;
2893
+ }
2894
+ function extractExpectedHash(args) {
2895
+ return normalizeExpectedHash(args["expected_hash"] ?? args["expectedHash"] ?? args["before_hash"] ?? args["beforeHash"]);
2896
+ }
2897
+ function hashMismatchMessage(filePath, expectedHash, actualHash) {
2898
+ return [
2899
+ `Refusing to edit ${filePath}: expected_hash does not match current file content.`,
2900
+ `expected_hash: ${expectedHash}`,
2901
+ `current_hash: ${actualHash}`,
2902
+ `Re-read the file and rebuild the edit from the current content before retrying.`
2903
+ ].join("\n");
2904
+ }
2905
+ function fileContextHeader(filePath, content, range) {
2906
+ const totalLines = content.split("\n").length;
2907
+ const hash = contentHash(content);
2908
+ const shown = range && (range.offset !== void 0 || range.limit !== void 0) ? ` showing=${range.offset ?? 1}-${range.limit ? (range.offset ?? 1) + range.limit - 1 : "end"}` : "";
2909
+ return `[FILE CONTEXT path=${filePath} sha256=${hash} lines=${totalLines}${shown}]`;
2910
+ }
2911
+ var init_edit_metadata = __esm({
2912
+ "packages/execution/dist/tools/edit-metadata.js"() {
2913
+ "use strict";
2914
+ }
2915
+ });
2916
+
2880
2917
  // packages/execution/dist/tools/file-read.js
2881
2918
  import { readFile } from "node:fs/promises";
2882
2919
  import { resolve as resolve2 } from "node:path";
@@ -2964,6 +3001,7 @@ var init_file_read = __esm({
2964
3001
  "packages/execution/dist/tools/file-read.js"() {
2965
3002
  "use strict";
2966
3003
  init_semantic_map();
3004
+ init_edit_metadata();
2967
3005
  SIG_PATTERNS = [
2968
3006
  /^\s*(export\s+)?(async\s+)?function\s+\w+/,
2969
3007
  /^\s*(export\s+)?(abstract\s+)?class\s+\w+/,
@@ -3019,10 +3057,15 @@ var init_file_read = __esm({
3019
3057
  const startIdx = Math.max(0, (offset ?? 1) - 1);
3020
3058
  lines = lines.slice(startIdx, limit ? startIdx + limit : void 0);
3021
3059
  const numbered2 = lines.map((line, i2) => `${String(startIdx + i2 + 1).padStart(6)} | ${line}`).join("\n");
3060
+ const hash = contentHash(content);
3022
3061
  return {
3023
3062
  success: true,
3024
- output: numbered2,
3025
- durationMs: performance.now() - start2
3063
+ output: `${fileContextHeader(filePath, content, { offset, limit })}
3064
+ ${numbered2}`,
3065
+ durationMs: performance.now() - start2,
3066
+ mutated: false,
3067
+ mutatedFiles: [],
3068
+ beforeHash: hash
3026
3069
  };
3027
3070
  }
3028
3071
  touchFile(this.workingDir, filePath);
@@ -3031,15 +3074,23 @@ var init_file_read = __esm({
3031
3074
  const notes2 = getFileNotes(this.workingDir, filePath);
3032
3075
  return {
3033
3076
  success: true,
3034
- output: buildStructuralPreview(lines, filePath, maxLines) + (notes2 ? "\n\n" + notes2 : ""),
3035
- durationMs: performance.now() - start2
3077
+ output: `${fileContextHeader(filePath, content)}
3078
+ ${buildStructuralPreview(lines, filePath, maxLines)}${notes2 ? "\n\n" + notes2 : ""}`,
3079
+ durationMs: performance.now() - start2,
3080
+ mutated: false,
3081
+ mutatedFiles: [],
3082
+ beforeHash: contentHash(content)
3036
3083
  };
3037
3084
  }
3038
3085
  const numbered = lines.map((line, i2) => `${String(i2 + 1).padStart(6)} | ${line}`).join("\n");
3039
3086
  return {
3040
3087
  success: true,
3041
- output: numbered,
3042
- durationMs: performance.now() - start2
3088
+ output: `${fileContextHeader(filePath, content)}
3089
+ ${numbered}`,
3090
+ durationMs: performance.now() - start2,
3091
+ mutated: false,
3092
+ mutatedFiles: [],
3093
+ beforeHash: contentHash(content)
3043
3094
  };
3044
3095
  } catch (error) {
3045
3096
  const errMsg = error instanceof Error ? error.message : String(error);
@@ -3223,6 +3274,7 @@ var init_file_write = __esm({
3223
3274
  "packages/execution/dist/tools/file-write.js"() {
3224
3275
  "use strict";
3225
3276
  init_change_log();
3277
+ init_edit_metadata();
3226
3278
  FileWriteTool = class {
3227
3279
  name = "file_write";
3228
3280
  description = "Write content to a file, creating directories as needed";
@@ -3230,7 +3282,15 @@ var init_file_write = __esm({
3230
3282
  type: "object",
3231
3283
  properties: {
3232
3284
  path: { type: "string", description: "Absolute or relative file path" },
3233
- content: { type: "string", description: "File content to write" }
3285
+ content: { type: "string", description: "File content to write" },
3286
+ overwrite: {
3287
+ type: "boolean",
3288
+ description: "Required when overwriting an existing file with different content."
3289
+ },
3290
+ expected_hash: {
3291
+ type: "string",
3292
+ description: "SHA-256 hash from the most recent file_read. Required with overwrite=true for existing files."
3293
+ }
3234
3294
  },
3235
3295
  required: ["path", "content"]
3236
3296
  };
@@ -3241,6 +3301,8 @@ var init_file_write = __esm({
3241
3301
  async execute(args) {
3242
3302
  const filePath = extractWritePath(args);
3243
3303
  const content = args["content"] ?? args["text"] ?? args["data"];
3304
+ const overwrite = args["overwrite"] === true || args["overwriteExisting"] === true;
3305
+ const expectedHash = extractExpectedHash(args);
3244
3306
  const start2 = performance.now();
3245
3307
  if (!filePath) {
3246
3308
  return {
@@ -3261,14 +3323,57 @@ var init_file_write = __esm({
3261
3323
  try {
3262
3324
  const fullPath = resolve3(this.workingDir, filePath);
3263
3325
  const isNew = !existsSync6(fullPath);
3326
+ const newHash = contentHash(content);
3264
3327
  if (!isNew) {
3265
3328
  try {
3266
3329
  const existing = await readFile2(fullPath, "utf-8");
3330
+ const beforeHash = contentHash(existing);
3267
3331
  if (existing === content) {
3268
3332
  return {
3269
3333
  success: true,
3270
3334
  output: `[NO-OP — file ${filePath} already contains these exact bytes (${content.length}B). Skipped redundant write. If you intended to make a change, the content is identical to disk — you may be replaying an earlier plan. Update todo_write and proceed to the next pending task.]`,
3271
- durationMs: performance.now() - start2
3335
+ durationMs: performance.now() - start2,
3336
+ mutated: false,
3337
+ mutatedFiles: [],
3338
+ noop: true,
3339
+ beforeHash,
3340
+ afterHash: beforeHash
3341
+ };
3342
+ }
3343
+ if (!overwrite) {
3344
+ return {
3345
+ success: false,
3346
+ output: "",
3347
+ error: `Refusing to overwrite existing file ${filePath} without overwrite=true. Use file_edit/file_patch for targeted changes, or re-read the file and call file_write with overwrite=true and expected_hash=${beforeHash}.`,
3348
+ durationMs: performance.now() - start2,
3349
+ mutated: false,
3350
+ mutatedFiles: [],
3351
+ beforeHash,
3352
+ afterHash: beforeHash
3353
+ };
3354
+ }
3355
+ if (!expectedHash) {
3356
+ return {
3357
+ success: false,
3358
+ output: "",
3359
+ error: `Refusing to overwrite existing file ${filePath} without expected_hash. Re-read the file first; file_read returns a sha256 header to pass as expected_hash.`,
3360
+ durationMs: performance.now() - start2,
3361
+ mutated: false,
3362
+ mutatedFiles: [],
3363
+ beforeHash,
3364
+ afterHash: beforeHash
3365
+ };
3366
+ }
3367
+ if (expectedHash !== beforeHash) {
3368
+ return {
3369
+ success: false,
3370
+ output: "",
3371
+ error: hashMismatchMessage(filePath, expectedHash, beforeHash),
3372
+ durationMs: performance.now() - start2,
3373
+ mutated: false,
3374
+ mutatedFiles: [],
3375
+ beforeHash,
3376
+ afterHash: beforeHash
3272
3377
  };
3273
3378
  }
3274
3379
  } catch {
@@ -3284,8 +3389,13 @@ var init_file_write = __esm({
3284
3389
  });
3285
3390
  return {
3286
3391
  success: true,
3287
- output: `Written ${content.length} bytes to ${fullPath}`,
3288
- durationMs: performance.now() - start2
3392
+ output: `${isNew ? "Created" : "Overwrote"} ${content.length} bytes at ${fullPath} (sha256 ${newHash})`,
3393
+ durationMs: performance.now() - start2,
3394
+ mutated: true,
3395
+ mutatedFiles: [filePath],
3396
+ noop: false,
3397
+ beforeHash: isNew ? void 0 : expectedHash ?? void 0,
3398
+ afterHash: newHash
3289
3399
  };
3290
3400
  } catch (error) {
3291
3401
  return {
@@ -4211,6 +4321,7 @@ var init_file_edit = __esm({
4211
4321
  "use strict";
4212
4322
  init_change_log();
4213
4323
  init_edit_snippet_finder();
4324
+ init_edit_metadata();
4214
4325
  FileEditTool = class {
4215
4326
  name = "file_edit";
4216
4327
  description = "Make a precise edit to a file by replacing an exact string match. The old_string must be unique in the file unless replace_all is true. Use replace_all to rename variables or change repeated patterns throughout the file.";
@@ -4229,6 +4340,10 @@ var init_file_edit = __esm({
4229
4340
  replace_all: {
4230
4341
  type: "boolean",
4231
4342
  description: "Replace ALL occurrences instead of just the first. Use for variable renames, import path changes, etc. Default: false"
4343
+ },
4344
+ expected_hash: {
4345
+ type: "string",
4346
+ description: "Optional SHA-256 from the most recent file_read. If provided, edit is refused when the file changed."
4232
4347
  }
4233
4348
  },
4234
4349
  required: ["path", "old_string", "new_string"]
@@ -4242,6 +4357,7 @@ var init_file_edit = __esm({
4242
4357
  const oldString = args["old_string"] ?? args["oldString"] ?? args["search"] ?? args["find"];
4243
4358
  const newString = args["new_string"] ?? args["newString"] ?? args["replace"] ?? args["replacement"];
4244
4359
  const replaceAll = args["replace_all"] === true || args["replaceAll"] === true;
4360
+ const expectedHash = extractExpectedHash(args);
4245
4361
  const start2 = performance.now();
4246
4362
  if (!filePath) {
4247
4363
  return {
@@ -4270,6 +4386,19 @@ var init_file_edit = __esm({
4270
4386
  try {
4271
4387
  const fullPath = resolve6(this.workingDir, filePath);
4272
4388
  const content = await readFile3(fullPath, "utf-8");
4389
+ const beforeHash = contentHash(content);
4390
+ if (expectedHash && expectedHash !== beforeHash) {
4391
+ return {
4392
+ success: false,
4393
+ output: "",
4394
+ error: hashMismatchMessage(filePath, expectedHash, beforeHash),
4395
+ durationMs: performance.now() - start2,
4396
+ mutated: false,
4397
+ mutatedFiles: [],
4398
+ beforeHash,
4399
+ afterHash: beforeHash
4400
+ };
4401
+ }
4273
4402
  const occurrences = countOccurrences(content, oldString);
4274
4403
  if (occurrences === 0) {
4275
4404
  const snippet = findClosestSnippet(content, oldString, 5);
@@ -4289,7 +4418,11 @@ Use the EXACT current content above to construct a working old_string. Do not re
4289
4418
  success: false,
4290
4419
  output: "",
4291
4420
  error: errorMsg,
4292
- durationMs: performance.now() - start2
4421
+ durationMs: performance.now() - start2,
4422
+ mutated: false,
4423
+ mutatedFiles: [],
4424
+ beforeHash,
4425
+ afterHash: beforeHash
4293
4426
  };
4294
4427
  }
4295
4428
  if (!replaceAll && occurrences > 1) {
@@ -4308,7 +4441,11 @@ ${s2.content}`;
4308
4441
  ${snippets}
4309
4442
 
4310
4443
  Add UNIQUE surrounding context (a function name, distinctive comment, or unique line above/below) to disambiguate. Or set replace_all=true if you want all ${occurrences} occurrences replaced identically.`,
4311
- durationMs: performance.now() - start2
4444
+ durationMs: performance.now() - start2,
4445
+ mutated: false,
4446
+ mutatedFiles: [],
4447
+ beforeHash,
4448
+ afterHash: beforeHash
4312
4449
  };
4313
4450
  }
4314
4451
  let updated;
@@ -4322,19 +4459,40 @@ Add UNIQUE surrounding context (a function name, distinctive comment, or unique
4322
4459
  editedLines = [lineNumber];
4323
4460
  updated = content.slice(0, index) + newString + content.slice(index + oldString.length);
4324
4461
  }
4462
+ const afterHash = contentHash(updated);
4463
+ const diff = buildCompactDiff(oldString, newString);
4464
+ if (afterHash === beforeHash) {
4465
+ return {
4466
+ success: true,
4467
+ output: `[NO-OP — ${filePath} already matches the requested file_edit at ${editedLines.length === 1 ? `line ${editedLines[0]}` : `${editedLines.length} locations`}.]`,
4468
+ durationMs: performance.now() - start2,
4469
+ mutated: false,
4470
+ mutatedFiles: [],
4471
+ diff,
4472
+ noop: true,
4473
+ beforeHash,
4474
+ afterHash
4475
+ };
4476
+ }
4325
4477
  await writeFile2(fullPath, updated, "utf-8");
4326
4478
  recordChange(this.workingDir, {
4327
4479
  tool: "file_edit",
4328
4480
  file: filePath,
4329
4481
  lineRange: editedLines.length > 0 ? [editedLines[0], editedLines[editedLines.length - 1]] : void 0,
4330
4482
  summary: replaceAll ? `Replaced ${editedLines.length} occurrences in ${filePath}` : `Edited ${filePath} at line ${editedLines[0]}`,
4331
- diff: buildCompactDiff(oldString, newString)
4483
+ diff
4332
4484
  });
4333
4485
  const linesInfo = editedLines.length === 1 ? `line ${editedLines[0]}` : `${editedLines.length} locations (lines ${editedLines.join(", ")})`;
4334
4486
  return {
4335
4487
  success: true,
4336
- output: `Edited ${filePath} at ${linesInfo}`,
4337
- durationMs: performance.now() - start2
4488
+ output: `Edited ${filePath} at ${linesInfo} (sha256 ${beforeHash} → ${afterHash})`,
4489
+ durationMs: performance.now() - start2,
4490
+ mutated: true,
4491
+ mutatedFiles: [filePath],
4492
+ diff,
4493
+ noop: false,
4494
+ beforeHash,
4495
+ afterHash
4338
4496
  };
4339
4497
  } catch (error) {
4340
4498
  return {
@@ -4889,7 +5047,7 @@ var init_explore_tools = __esm({
4889
5047
  memory_write: "Store a fact in persistent memory",
4890
5048
  memory_search: "Search all memories by relevance",
4891
5049
  batch_edit: "Apply multiple file edits atomically",
4892
- file_patch: "Apply unified diff patches to files",
5050
+ file_patch: "Apply version-checked line-range patches to files",
4893
5051
  git_info: "Get git status, branch, recent commits",
4894
5052
  codebase_map: "Generate overview of project structure",
4895
5053
  diagnostic: "Run project diagnostics (build, test, lint)",
@@ -5829,6 +5987,7 @@ var init_batch_edit = __esm({
5829
5987
  "use strict";
5830
5988
  init_change_log();
5831
5989
  init_edit_snippet_finder();
5990
+ init_edit_metadata();
5832
5991
  BatchEditTool = class {
5833
5992
  name = "batch_edit";
5834
5993
  description = "Make multiple precise edits across one or more files in a single call. More efficient than calling file_edit repeatedly. Each edit replaces an exact string match with uniqueness validation. Edits are applied in order within each file. Set replace_all on individual edits for bulk renames.";
@@ -5856,6 +6015,10 @@ var init_batch_edit = __esm({
5856
6015
  replace_all: {
5857
6016
  type: "boolean",
5858
6017
  description: "Replace all occurrences (for renames). Default: false"
6018
+ },
6019
+ expected_hash: {
6020
+ type: "string",
6021
+ description: "SHA-256 from the most recent file_read for this file. All edits for a file must use the same hash if provided."
5859
6022
  }
5860
6023
  },
5861
6024
  required: ["path", "old_string", "new_string"]
@@ -5888,15 +6051,33 @@ var init_batch_edit = __esm({
5888
6051
  old_string: edit.old_string,
5889
6052
  new_string: edit.new_string,
5890
6053
  replace_all: edit.replace_all,
6054
+ expected_hash: edit.expected_hash,
5891
6055
  relPath: edit.path
5892
6056
  });
5893
6057
  }
5894
6058
  const results = [];
5895
6059
  let successCount = 0;
5896
6060
  let failCount = 0;
6061
+ const pendingWrites = [];
5897
6062
  for (const [fullPath, fileEdits] of byFile) {
5898
6063
  try {
5899
6064
  let content = await readFile7(fullPath, "utf-8");
6065
+ const beforeHash = contentHash(content);
6066
+ const expectedHashes = new Set(fileEdits.map((edit) => extractExpectedHash({ expected_hash: edit.expected_hash })).filter((h) => !!h));
6067
+ if (expectedHashes.size > 1) {
6068
+ results.push(`ERROR: ${fileEdits[0].relPath}: conflicting expected_hash values in one batch`);
6069
+ failCount += fileEdits.length;
6070
+ continue;
6071
+ }
6072
+ const expectedHash = expectedHashes.values().next().value;
6073
+ if (expectedHash && expectedHash !== beforeHash) {
6074
+ results.push(hashMismatchMessage(fileEdits[0].relPath, expectedHash, beforeHash));
6075
+ failCount += fileEdits.length;
6076
+ continue;
6077
+ }
6078
+ const originalContent = content;
6079
+ let fileSuccessCount = 0;
6080
+ let fileFailed = false;
5900
6081
  for (const edit of fileEdits) {
5901
6082
  const occurrences = countOccurrences2(content, edit.old_string);
5902
6083
  if (occurrences === 0) {
@@ -5911,7 +6092,8 @@ ${snippet.content}
5911
6092
  results.push(`SKIP: old_string not found in ${edit.relPath} (file empty or binary)`);
5912
6093
  }
5913
6094
  failCount++;
5914
- continue;
6095
+ fileFailed = true;
6096
+ break;
5915
6097
  }
5916
6098
  if (!edit.replace_all && occurrences > 1) {
5917
6099
  const offsets = findMatchOffsetsBatch(content, edit.old_string).slice(0, 4);
@@ -5924,7 +6106,8 @@ ${s2.content}`;
5924
6106
  results.push(`AMBIGUOUS: old_string has ${occurrences} matches in ${edit.relPath}.${snippets}
5925
6107
  Add UNIQUE surrounding context, or use replace_all=true.`);
5926
6108
  failCount++;
5927
- continue;
6109
+ fileFailed = true;
6110
+ break;
5928
6111
  }
5929
6112
  if (edit.replace_all) {
5930
6113
  content = content.split(edit.old_string).join(edit.new_string);
@@ -5935,26 +6118,55 @@ ${s2.content}`;
5935
6118
  content = content.slice(0, index) + edit.new_string + content.slice(index + edit.old_string.length);
5936
6119
  results.push(`EDIT: ${edit.relPath} line ${lineNumber}`);
5937
6120
  }
5938
- successCount++;
6121
+ fileSuccessCount++;
5939
6122
  }
5940
- await writeFile4(fullPath, content, "utf-8");
5941
- recordChange(this.workingDir, {
5942
- tool: "batch_edit",
5943
- file: fileEdits[0].relPath,
5944
- summary: `Batch: ${fileEdits.length} edit(s) in ${fileEdits[0].relPath}`
6123
+ if (fileFailed) {
6124
+ content = originalContent;
6125
+ continue;
6126
+ }
6127
+ const afterHash = contentHash(content);
6128
+ if (afterHash === beforeHash) {
6129
+ results.push(`NO-OP: ${fileEdits[0].relPath} batch produced no disk changes`);
6130
+ continue;
6131
+ }
6132
+ pendingWrites.push({
6133
+ fullPath,
6134
+ relPath: fileEdits[0].relPath,
6135
+ content,
6136
+ beforeHash,
6137
+ afterHash,
6138
+ editCount: fileSuccessCount
5945
6139
  });
6140
+ successCount += fileSuccessCount;
5946
6141
  } catch (error) {
5947
6142
  results.push(`ERROR: ${fullPath}: ${error instanceof Error ? error.message : String(error)}`);
5948
6143
  failCount++;
5949
6144
  }
5950
6145
  }
5951
- const summary = `${successCount} edit(s) applied, ${failCount} failed across ${byFile.size} file(s)`;
6146
+ if (failCount === 0) {
6147
+ for (const write2 of pendingWrites) {
6148
+ await writeFile4(write2.fullPath, write2.content, "utf-8");
6149
+ recordChange(this.workingDir, {
6150
+ tool: "batch_edit",
6151
+ file: write2.relPath,
6152
+ summary: `Batch: ${write2.editCount} edit(s) in ${write2.relPath}`
6153
+ });
6154
+ }
6155
+ }
6156
+ const appliedCount = failCount === 0 ? successCount : 0;
6157
+ const summary = `${appliedCount} edit(s) applied, ${failCount} failed across ${byFile.size} file(s)` + (failCount > 0 ? " — atomic batch aborted; no files modified" : "");
5952
6158
  return {
5953
6159
  success: failCount === 0,
5954
6160
  output: `${summary}
5955
6161
  ${results.join("\n")}`,
5956
6162
  error: failCount > 0 ? `${failCount} edit(s) failed` : void 0,
5957
- durationMs: performance.now() - start2
6163
+ durationMs: performance.now() - start2,
6164
+ mutated: failCount === 0 && pendingWrites.length > 0,
6165
+ mutatedFiles: failCount === 0 ? pendingWrites.map((w) => w.relPath) : [],
6166
+ noop: failCount === 0 && pendingWrites.length === 0,
6167
+ partial: false,
6168
+ beforeHash: pendingWrites.length === 1 ? pendingWrites[0].beforeHash : void 0,
6169
+ afterHash: pendingWrites.length === 1 ? pendingWrites[0].afterHash : void 0
5958
6170
  };
5959
6171
  }
5960
6172
  };
@@ -5969,6 +6181,7 @@ var init_file_patch = __esm({
5969
6181
  "packages/execution/dist/tools/file-patch.js"() {
5970
6182
  "use strict";
5971
6183
  init_change_log();
6184
+ init_edit_metadata();
5972
6185
  FilePatchTool = class {
5973
6186
  name = "file_patch";
5974
6187
  description = "Edit specific line ranges in a file. More precise than string matching for large files. Modes: 'replace' replaces lines start_line..end_line with new_content, 'insert_before' inserts before start_line, 'insert_after' inserts after start_line, 'delete' removes lines start_line..end_line. Use dry_run to preview changes.";
@@ -5989,7 +6202,19 @@ var init_file_patch = __esm({
5989
6202
  },
5990
6203
  new_content: {
5991
6204
  type: "string",
5992
- description: "Replacement content (for replace mode) or content to insert (for insert modes). Not needed for delete mode."
6205
+ description: "Replacement content (for replace mode) or content to insert (for insert modes). Required for replace and insert modes. Not needed for delete mode."
6206
+ },
6207
+ expected_hash: {
6208
+ type: "string",
6209
+ description: "SHA-256 from the most recent file_read. Required for insert modes and accepted for all modes."
6210
+ },
6211
+ expected_old_content: {
6212
+ type: "string",
6213
+ description: "Exact current content in start_line..end_line. Required for replace/delete unless expected_hash is provided."
6214
+ },
6215
+ allow_empty_content: {
6216
+ type: "boolean",
6217
+ description: "Set true only when an intentional blank replacement/insert is required."
5993
6218
  },
5994
6219
  mode: {
5995
6220
  type: "string",
@@ -6011,9 +6236,13 @@ var init_file_patch = __esm({
6011
6236
  const filePath = args["path"];
6012
6237
  const startLine = args["start_line"];
6013
6238
  const endLine = args["end_line"] ?? startLine;
6014
- const newContent = args["new_content"] ?? "";
6015
6239
  const mode = args["mode"] ?? "replace";
6016
6240
  const dryRun = args["dry_run"] === true;
6241
+ const hasNewContent = typeof args["new_content"] === "string";
6242
+ const newContent = hasNewContent ? args["new_content"] : "";
6243
+ const expectedHash = extractExpectedHash(args);
6244
+ const expectedOldContent = typeof args["expected_old_content"] === "string" ? args["expected_old_content"] : void 0;
6245
+ const allowEmptyContent = args["allow_empty_content"] === true;
6017
6246
  const start2 = performance.now();
6018
6247
  try {
6019
6248
  if (!Number.isInteger(startLine) || startLine < 1) {
@@ -6034,8 +6263,21 @@ var init_file_patch = __esm({
6034
6263
  }
6035
6264
  const fullPath = resolve12(this.workingDir, filePath);
6036
6265
  const content = await readFile8(fullPath, "utf-8");
6266
+ const beforeHash = contentHash(content);
6037
6267
  const lines = content.split("\n");
6038
6268
  const totalLines = lines.length;
6269
+ if (expectedHash && expectedHash !== beforeHash) {
6270
+ return {
6271
+ success: false,
6272
+ output: "",
6273
+ error: hashMismatchMessage(filePath, expectedHash, beforeHash),
6274
+ durationMs: performance.now() - start2,
6275
+ mutated: false,
6276
+ mutatedFiles: [],
6277
+ beforeHash,
6278
+ afterHash: beforeHash
6279
+ };
6280
+ }
6039
6281
  if (startLine > totalLines) {
6040
6282
  return {
6041
6283
  success: false,
@@ -6047,6 +6289,60 @@ var init_file_patch = __esm({
6047
6289
  const effectiveEnd = Math.min(endLine, totalLines);
6048
6290
  const startIdx = startLine - 1;
6049
6291
  const endIdx = effectiveEnd;
6292
+ const oldTargetContent = lines.slice(startIdx, endIdx).join("\n");
6293
+ if ((mode === "replace" || mode === "delete") && !expectedHash && expectedOldContent === void 0) {
6294
+ return {
6295
+ success: false,
6296
+ output: "",
6297
+ error: `file_patch ${mode} requires targeted context: pass expected_hash from file_read or expected_old_content copied exactly from lines ${startLine}-${effectiveEnd}.`,
6298
+ durationMs: performance.now() - start2,
6299
+ mutated: false,
6300
+ mutatedFiles: [],
6301
+ beforeHash,
6302
+ afterHash: beforeHash
6303
+ };
6304
+ }
6305
+ if ((mode === "insert_before" || mode === "insert_after") && !expectedHash) {
6306
+ return {
6307
+ success: false,
6308
+ output: "",
6309
+ error: `file_patch ${mode} requires expected_hash from the most recent file_read so line numbers are versioned.`,
6310
+ durationMs: performance.now() - start2,
6311
+ mutated: false,
6312
+ mutatedFiles: [],
6313
+ beforeHash,
6314
+ afterHash: beforeHash
6315
+ };
6316
+ }
6317
+ if (expectedOldContent !== void 0 && expectedOldContent !== oldTargetContent) {
6318
+ return {
6319
+ success: false,
6320
+ output: "",
6321
+ error: `Refusing to patch ${filePath}: expected_old_content does not match current lines ${startLine}-${effectiveEnd}.
6322
+
6323
+ Current content:
6324
+ ${oldTargetContent}
6325
+
6326
+ Re-read the target range and retry with exact current content.`,
6327
+ durationMs: performance.now() - start2,
6328
+ mutated: false,
6329
+ mutatedFiles: [],
6330
+ beforeHash,
6331
+ afterHash: beforeHash
6332
+ };
6333
+ }
6334
+ if ((mode === "replace" || mode === "insert_before" || mode === "insert_after") && (!hasNewContent || newContent.length === 0 && !allowEmptyContent)) {
6335
+ return {
6336
+ success: false,
6337
+ output: "",
6338
+ error: `file_patch mode=${mode} requires non-empty new_content. Use mode="delete" for removal, or set allow_empty_content=true for an intentional blank edit.`,
6339
+ durationMs: performance.now() - start2,
6340
+ mutated: false,
6341
+ mutatedFiles: [],
6342
+ beforeHash,
6343
+ afterHash: beforeHash
6344
+ };
6345
+ }
6050
6346
  const newLines = newContent.length > 0 ? newContent.split("\n") : [];
6051
6347
  let resultLines;
6052
6348
  let description;
@@ -6110,10 +6406,31 @@ ${newSection}`;
6110
6406
  output: `[DRY RUN] ${diff}
6111
6407
 
6112
6408
  File NOT modified. Remove dry_run to apply.`,
6113
- durationMs: performance.now() - start2
6409
+ durationMs: performance.now() - start2,
6410
+ mutated: false,
6411
+ mutatedFiles: [],
6412
+ diff,
6413
+ dryRun: true,
6414
+ beforeHash,
6415
+ afterHash: beforeHash
6114
6416
  };
6115
6417
  }
6116
- await writeFile5(fullPath, resultLines.join("\n"), "utf-8");
6418
+ const updatedContent = resultLines.join("\n");
6419
+ const afterHash = contentHash(updatedContent);
6420
+ if (afterHash === beforeHash) {
6421
+ return {
6422
+ success: true,
6423
+ output: `[NO-OP — ${filePath}: ${description}; resulting content is identical to disk.]`,
6424
+ durationMs: performance.now() - start2,
6425
+ mutated: false,
6426
+ mutatedFiles: [],
6427
+ diff,
6428
+ noop: true,
6429
+ beforeHash,
6430
+ afterHash
6431
+ };
6432
+ }
6433
+ await writeFile5(fullPath, updatedContent, "utf-8");
6117
6434
  recordChange(this.workingDir, {
6118
6435
  tool: "file_patch",
6119
6436
  file: filePath,
@@ -6123,10 +6440,16 @@ File NOT modified. Remove dry_run to apply.`,
6123
6440
  });
6124
6441
  return {
6125
6442
  success: true,
6126
- output: `${filePath}: ${description} (${totalLines} → ${resultLines.length} total lines)
6443
+ output: `${filePath}: ${description} (${totalLines} → ${resultLines.length} total lines, sha256 ${beforeHash} → ${afterHash})
6127
6444
 
6128
6445
  ${diff}`,
6129
- durationMs: performance.now() - start2
6446
+ durationMs: performance.now() - start2,
6447
+ mutated: true,
6448
+ mutatedFiles: [filePath],
6449
+ diff,
6450
+ noop: false,
6451
+ beforeHash,
6452
+ afterHash
6130
6453
  };
6131
6454
  } catch (error) {
6132
6455
  return {
@@ -6706,7 +7029,7 @@ var init_git_info = __esm({
6706
7029
  });
6707
7030
 
6708
7031
  // packages/execution/dist/tools/jibberlink.js
6709
- import { createCipheriv, createDecipheriv, createHash, randomBytes as randomBytes5 } from "node:crypto";
7032
+ import { createCipheriv, createDecipheriv, createHash as createHash2, randomBytes as randomBytes5 } from "node:crypto";
6710
7033
  function crc16(bytes) {
6711
7034
  let crc = 65535;
6712
7035
  for (let i2 = 0; i2 < bytes.length; i2++) {
@@ -6719,7 +7042,7 @@ function crc16(bytes) {
6719
7042
  }
6720
7043
  function deriveRoomKey(roomId, secret = "jibberlink-v1") {
6721
7044
  const material = `${secret}\0${roomId}`;
6722
- return createHash("sha256").update(material).digest();
7045
+ return createHash2("sha256").update(material).digest();
6723
7046
  }
6724
7047
  function aesGcmEncrypt(key, plaintext) {
6725
7048
  if (key.length !== 32)
@@ -6805,7 +7128,7 @@ __export(nexus_exports, {
6805
7128
  import { readFile as readFile9, writeFile as writeFile6, mkdir as mkdir3, chmod, unlink, readdir as readdir2, open as fsOpen, copyFile as copyFile2 } from "node:fs/promises";
6806
7129
  import { existsSync as existsSync14, readFileSync as readFileSync12, watch as fsWatchLocal } from "node:fs";
6807
7130
  import { resolve as resolve13, join as join18 } from "node:path";
6808
- import { randomBytes as randomBytes6, createCipheriv as createCipheriv2, createDecipheriv as createDecipheriv2, scryptSync, createHash as createHash2 } from "node:crypto";
7131
+ import { randomBytes as randomBytes6, createCipheriv as createCipheriv2, createDecipheriv as createDecipheriv2, scryptSync, createHash as createHash3 } from "node:crypto";
6809
7132
  import { execSync as execSync8, spawn as spawn2 } from "node:child_process";
6810
7133
  import { hostname, userInfo, homedir as homedir4 } from "node:os";
6811
7134
  function readBundledDependencySpec(packageName, fallback) {
@@ -12087,7 +12410,7 @@ process.on('SIGINT', () => process.emit('SIGTERM'));
12087
12410
  // =========================================================================
12088
12411
  async doConnect(args) {
12089
12412
  await this.ensureDir();
12090
- const currentScriptHash = createHash2("sha256").update(DAEMON_SCRIPT).digest("hex").slice(0, 16);
12413
+ const currentScriptHash = createHash3("sha256").update(DAEMON_SCRIPT).digest("hex").slice(0, 16);
12091
12414
  const existingPid = this.getDaemonPid();
12092
12415
  if (existingPid) {
12093
12416
  let processAlive = false;
@@ -12693,7 +13016,7 @@ process.on('SIGINT', () => process.emit('SIGTERM'));
12693
13016
  } catch {
12694
13017
  const privKey = randomBytes6(32);
12695
13018
  privKeyHex = privKey.toString("hex");
12696
- address = "0x" + createHash2("sha256").update(privKey).digest("hex").slice(0, 40);
13019
+ address = "0x" + createHash3("sha256").update(privKey).digest("hex").slice(0, 40);
12697
13020
  privKey.fill(0);
12698
13021
  }
12699
13022
  const salt = randomBytes6(32);
@@ -12755,7 +13078,7 @@ process.on('SIGINT', () => process.emit('SIGTERM'));
12755
13078
  } catch {
12756
13079
  const privKey = randomBytes6(32);
12757
13080
  privKeyHex = privKey.toString("hex");
12758
- address = "0x" + createHash2("sha256").update(privKey).digest("hex").slice(0, 40);
13081
+ address = "0x" + createHash3("sha256").update(privKey).digest("hex").slice(0, 40);
12759
13082
  privKey.fill(0);
12760
13083
  }
12761
13084
  const salt = randomBytes6(32);
@@ -13278,7 +13601,7 @@ process.on('SIGINT', () => process.emit('SIGTERM'));
13278
13601
  }
13279
13602
  }
13280
13603
  const nonce = randomBytes6(16).toString("hex");
13281
- const hash = createHash2("sha256").update(`${modelName}:${nonce}:${Date.now()}`).digest("hex");
13604
+ const hash = createHash3("sha256").update(`${modelName}:${nonce}:${Date.now()}`).digest("hex");
13282
13605
  const bar = (s2) => "█".repeat(Math.round(s2 / 5)) + "░".repeat(20 - Math.round(s2 / 5));
13283
13606
  const mem = vramMb > 24e3 ? 95 : vramMb > 16e3 ? 80 : vramMb > 8e3 ? 60 : vramMb > 0 ? 40 : 20;
13284
13607
  await this.ensureDir();
@@ -13483,6 +13806,10 @@ Process error: ${err.message}`;
13483
13806
  return null;
13484
13807
  return toInfo(entry);
13485
13808
  }
13809
+ getLatestTask() {
13810
+ const entry = Array.from(this.tasks.values()).at(-1);
13811
+ return entry ? toInfo(entry) : null;
13812
+ }
13486
13813
  getOutput(id, tail) {
13487
13814
  const entry = this.tasks.get(id);
13488
13815
  if (!entry)
@@ -13573,7 +13900,7 @@ Process error: ${err.message}`;
13573
13900
  workingDir;
13574
13901
  manager;
13575
13902
  name = "background_run";
13576
- description = "Run a shell command in the background. Returns a task ID immediately. Use task_status to check progress and task_output to read results. Useful for long-running commands (builds, tests) that shouldn't block the agent.";
13903
+ description = `Run a shell command in the background. Returns a task ID immediately. Use task_status(task_id="...") to check progress and task_output(task_id="...") to read results. Useful for long-running commands (builds, tests) that shouldn't block the agent.`;
13577
13904
  parameters = {
13578
13905
  type: "object",
13579
13906
  properties: {
@@ -13598,7 +13925,7 @@ Process error: ${err.message}`;
13598
13925
  success: true,
13599
13926
  output: `Background task started: ${taskId}
13600
13927
  Command: ${command}
13601
- Use task_status or task_output to check progress.`,
13928
+ Use task_status(task_id="${taskId}") or task_output(task_id="${taskId}") to check progress.`,
13602
13929
  durationMs: Date.now() - start2
13603
13930
  };
13604
13931
  }
@@ -13650,32 +13977,43 @@ Exit code: ${task.exitCode ?? "N/A"}`,
13650
13977
  TaskOutputTool = class {
13651
13978
  manager;
13652
13979
  name = "task_output";
13653
- description = "Read the output of a background task. Returns stdout+stderr combined. Use tail parameter to get only the last N lines.";
13980
+ description = "Read the output of a background task. Returns stdout+stderr combined. If task_id is omitted, uses the most recently created task. Use tail parameter to get only the last N lines.";
13654
13981
  parameters = {
13655
13982
  type: "object",
13656
13983
  properties: {
13657
- task_id: { type: "string", description: "Task ID to read output from" },
13984
+ task_id: { type: "string", description: "Task ID to read output from (optional — falls back to most recent task)" },
13658
13985
  tail: { type: "number", description: "Only return the last N lines (optional)" }
13659
- },
13660
- required: ["task_id"]
13986
+ }
13661
13987
  };
13662
13988
  constructor(manager) {
13663
13989
  this.manager = manager;
13664
13990
  }
13665
13991
  async execute(args) {
13666
13992
  const start2 = Date.now();
13667
- const taskId = String(args["task_id"] ?? "");
13993
+ const explicitTaskId = typeof args["task_id"] === "string" ? args["task_id"].trim() : "";
13994
+ const fallbackTask = explicitTaskId ? null : this.manager.getLatestTask();
13995
+ const taskId = explicitTaskId || fallbackTask?.id || "";
13668
13996
  const tail = typeof args["tail"] === "number" ? args["tail"] : void 0;
13669
13997
  const output = this.manager.getOutput(taskId, tail);
13670
13998
  if (output === null) {
13999
+ if (!taskId) {
14000
+ return {
14001
+ success: false,
14002
+ output: "",
14003
+ error: "task_id is required when no background tasks exist yet",
14004
+ durationMs: Date.now() - start2
14005
+ };
14006
+ }
13671
14007
  return { success: false, output: "", error: `Task not found: ${taskId}`, durationMs: Date.now() - start2 };
13672
14008
  }
13673
14009
  const task = this.manager.getTask(taskId);
13674
14010
  const header = task ? `[${task.status}${task.exitCode !== null ? `, exit ${task.exitCode}` : ""}]
14011
+ ` : "";
14012
+ const fallbackNote = explicitTaskId ? "" : fallbackTask ? `[fallback to ${taskId}]
13675
14013
  ` : "";
13676
14014
  return {
13677
14015
  success: true,
13678
- output: header + (output || "(no output yet)"),
14016
+ output: fallbackNote + header + (output || "(no output yet)"),
13679
14017
  durationMs: Date.now() - start2
13680
14018
  };
13681
14019
  }
@@ -89943,7 +90281,7 @@ var require_auto = __commonJS({
89943
90281
  // ../node_modules/acme-client/src/client.js
89944
90282
  var require_client = __commonJS({
89945
90283
  "../node_modules/acme-client/src/client.js"(exports, module) {
89946
- var { createHash: createHash20 } = __require("crypto");
90284
+ var { createHash: createHash21 } = __require("crypto");
89947
90285
  var { getPemBodyAsB64u } = require_crypto();
89948
90286
  var { log: log22 } = require_logger();
89949
90287
  var HttpClient = require_http();
@@ -90254,14 +90592,14 @@ var require_client = __commonJS({
90254
90592
  */
90255
90593
  async getChallengeKeyAuthorization(challenge) {
90256
90594
  const jwk = this.http.getJwk();
90257
- const keysum = createHash20("sha256").update(JSON.stringify(jwk));
90595
+ const keysum = createHash21("sha256").update(JSON.stringify(jwk));
90258
90596
  const thumbprint = keysum.digest("base64url");
90259
90597
  const result = `${challenge.token}.${thumbprint}`;
90260
90598
  if (challenge.type === "http-01") {
90261
90599
  return result;
90262
90600
  }
90263
90601
  if (challenge.type === "dns-01") {
90264
- return createHash20("sha256").update(result).digest("base64url");
90602
+ return createHash21("sha256").update(result).digest("base64url");
90265
90603
  }
90266
90604
  if (challenge.type === "tls-alpn-01") {
90267
90605
  return result;
@@ -232941,7 +233279,7 @@ var require_websocket2 = __commonJS({
232941
233279
  var http6 = __require("http");
232942
233280
  var net5 = __require("net");
232943
233281
  var tls2 = __require("tls");
232944
- var { randomBytes: randomBytes25, createHash: createHash20 } = __require("crypto");
233282
+ var { randomBytes: randomBytes25, createHash: createHash21 } = __require("crypto");
232945
233283
  var { Duplex: Duplex3, Readable } = __require("stream");
232946
233284
  var { URL: URL3 } = __require("url");
232947
233285
  var PerMessageDeflate2 = require_permessage_deflate2();
@@ -233601,7 +233939,7 @@ var require_websocket2 = __commonJS({
233601
233939
  abortHandshake(websocket, socket, "Invalid Upgrade header");
233602
233940
  return;
233603
233941
  }
233604
- const digest3 = createHash20("sha1").update(key + GUID).digest("base64");
233942
+ const digest3 = createHash21("sha1").update(key + GUID).digest("base64");
233605
233943
  if (res.headers["sec-websocket-accept"] !== digest3) {
233606
233944
  abortHandshake(websocket, socket, "Invalid Sec-WebSocket-Accept header");
233607
233945
  return;
@@ -233968,7 +234306,7 @@ var require_websocket_server = __commonJS({
233968
234306
  var EventEmitter13 = __require("events");
233969
234307
  var http6 = __require("http");
233970
234308
  var { Duplex: Duplex3 } = __require("stream");
233971
- var { createHash: createHash20 } = __require("crypto");
234309
+ var { createHash: createHash21 } = __require("crypto");
233972
234310
  var extension2 = require_extension2();
233973
234311
  var PerMessageDeflate2 = require_permessage_deflate2();
233974
234312
  var subprotocol2 = require_subprotocol();
@@ -234269,7 +234607,7 @@ var require_websocket_server = __commonJS({
234269
234607
  );
234270
234608
  }
234271
234609
  if (this._state > RUNNING) return abortHandshake(socket, 503);
234272
- const digest3 = createHash20("sha1").update(key + GUID).digest("base64");
234610
+ const digest3 = createHash21("sha1").update(key + GUID).digest("base64");
234273
234611
  const headers = [
234274
234612
  "HTTP/1.1 101 Switching Protocols",
234275
234613
  "Upgrade: websocket",
@@ -247076,13 +247414,13 @@ Justification: ${justification || "(none provided)"}`,
247076
247414
  }
247077
247415
  const snapshot = JSON.stringify(this.selfState, null, 2);
247078
247416
  try {
247079
- const { createHash: createHash20 } = await import("node:crypto");
247417
+ const { createHash: createHash21 } = await import("node:crypto");
247080
247418
  const snapshotDir = join27(this.cwd, ".oa", "identity", "snapshots");
247081
247419
  await mkdir6(snapshotDir, { recursive: true });
247082
247420
  const version4 = this.selfState.version;
247083
247421
  const snapshotPath = join27(snapshotDir, `v${version4}.json`);
247084
247422
  await writeFile11(snapshotPath, snapshot, "utf8");
247085
- const hash = createHash20("sha256").update(snapshot).digest("hex");
247423
+ const hash = createHash21("sha256").update(snapshot).digest("hex");
247086
247424
  await writeFile11(join27(this.cwd, ".oa", "identity", "latest-hash.txt"), hash, "utf8");
247087
247425
  let ipfsCid = "";
247088
247426
  try {
@@ -247215,8 +247553,8 @@ New: ${newNarrative.slice(0, 200)}...`,
247215
247553
  }
247216
247554
  // ── Helpers ──────────────────────────────────────────────────────────────
247217
247555
  createDefaultState() {
247218
- const { createHash: createHash20 } = __require("node:crypto");
247219
- const machineId = createHash20("sha256").update(this.cwd).digest("hex").slice(0, 12);
247556
+ const { createHash: createHash21 } = __require("node:crypto");
247557
+ const machineId = createHash21("sha256").update(this.cwd).digest("hex").slice(0, 12);
247220
247558
  return {
247221
247559
  self_id: `oa-${machineId}`,
247222
247560
  version: 1,
@@ -247298,9 +247636,9 @@ New: ${newNarrative.slice(0, 200)}...`,
247298
247636
  let cid;
247299
247637
  if (this.selfState.version > prevVersion) {
247300
247638
  try {
247301
- const { createHash: createHash20 } = await import("node:crypto");
247639
+ const { createHash: createHash21 } = await import("node:crypto");
247302
247640
  const stateJson = JSON.stringify(this.selfState);
247303
- const hash = createHash20("sha256").update(stateJson).digest("hex").slice(0, 32);
247641
+ const hash = createHash21("sha256").update(stateJson).digest("hex").slice(0, 32);
247304
247642
  const cidsPath = join27(this.cwd, ".oa", "identity", "cids.json");
247305
247643
  const cidsData = { latest: "", hash, version: this.selfState.version };
247306
247644
  try {
@@ -253007,7 +253345,7 @@ import { execSync as execSync22, exec as execCb, spawnSync as spawnSync2 } from
253007
253345
  import { readFile as readFile16, writeFile as writeFile17, mkdir as mkdir12 } from "node:fs/promises";
253008
253346
  import { resolve as resolve24, join as join41 } from "node:path";
253009
253347
  import { homedir as homedir10 } from "node:os";
253010
- import { randomBytes as randomBytes11, createHash as createHash3 } from "node:crypto";
253348
+ import { randomBytes as randomBytes11, createHash as createHash4 } from "node:crypto";
253011
253349
  function isValidCron(expr) {
253012
253350
  const parts = expr.trim().split(/\s+/);
253013
253351
  if (parts.length !== 5)
@@ -253330,7 +253668,7 @@ var init_scheduler = __esm({
253330
253668
  }
253331
253669
  const scope = String(args["scope"] ?? "local");
253332
253670
  const fingerprint = `${resolve24(this.workingDir)}|${task}|${cronExpr}|${scope}`;
253333
- const id = `sched-${createHash3("sha1").update(fingerprint).digest("hex").slice(0, 8)}`;
253671
+ const id = `sched-${createHash4("sha1").update(fingerprint).digest("hex").slice(0, 8)}`;
253334
253672
  const oneShot = Boolean(args["one_shot"]);
253335
253673
  const maxRuns = typeof args["max_runs"] === "number" ? args["max_runs"] : void 0;
253336
253674
  const newTask = {
@@ -256873,7 +257211,7 @@ var init_import_graph = __esm({
256873
257211
  import { createRequire as __createRequireGlob } from "node:module";
256874
257212
  import ignore from "ignore";
256875
257213
  import { readFile as readFile21, stat as stat4 } from "node:fs/promises";
256876
- import { createHash as createHash4 } from "node:crypto";
257214
+ import { createHash as createHash5 } from "node:crypto";
256877
257215
  import { join as join49, relative as relative4, extname as extname8, basename as basename11 } from "node:path";
256878
257216
  var __requireGlob, glob2, DEFAULT_EXCLUDE, LANGUAGE_MAP, CodebaseIndexer;
256879
257217
  var init_codebase_indexer = __esm({
@@ -256941,7 +257279,7 @@ var init_codebase_indexer = __esm({
256941
257279
  if (fileStat.size > this.config.maxFileSize)
256942
257280
  continue;
256943
257281
  const content = await readFile21(fullPath);
256944
- const hash = createHash4("sha256").update(content).digest("hex");
257282
+ const hash = createHash5("sha256").update(content).digest("hex");
256945
257283
  const ext = extname8(relativePath);
256946
257284
  indexed.push({
256947
257285
  path: fullPath,
@@ -501260,7 +501598,7 @@ var init_ts_morph_parser = __esm({
501260
501598
 
501261
501599
  // packages/indexer/dist/code-graph-db.js
501262
501600
  import { createRequire as createRequire2 } from "node:module";
501263
- import { createHash as createHash5 } from "node:crypto";
501601
+ import { createHash as createHash6 } from "node:crypto";
501264
501602
  import { mkdirSync as mkdirSync12, readFileSync as readFileSync26 } from "node:fs";
501265
501603
  import { join as join50, dirname as dirname14, extname as extname9 } from "node:path";
501266
501604
  function loadDatabaseCtor() {
@@ -501332,7 +501670,7 @@ function extractFileImports(content, filePath) {
501332
501670
  return imports.map((p2) => p2.replace(/\.(js|ts|jsx|tsx|mjs|cjs)$/, ""));
501333
501671
  }
501334
501672
  function hashContent(content) {
501335
- return createHash5("sha1").update(content).digest("hex").slice(0, 16);
501673
+ return createHash6("sha1").update(content).digest("hex").slice(0, 16);
501336
501674
  }
501337
501675
  function detectLanguage(filePath) {
501338
501676
  return EXT_TO_LANG[extname9(filePath)] ?? "unknown";
@@ -509952,7 +510290,7 @@ var init_environment_snapshot = __esm({
509952
510290
  import { execSync as execSync42 } from "node:child_process";
509953
510291
  import { existsSync as existsSync47, mkdirSync as mkdirSync24, writeFileSync as writeFileSync22, readFileSync as readFileSync39, readdirSync as readdirSync14, unlinkSync as unlinkSync11 } from "node:fs";
509954
510292
  import { join as join64, basename as basename12 } from "node:path";
509955
- import { createHash as createHash6 } from "node:crypto";
510293
+ import { createHash as createHash7 } from "node:crypto";
509956
510294
  function isYouTubeUrl2(url) {
509957
510295
  return /(?:youtube\.com\/(?:watch|shorts|live|embed|v\/)|youtu\.be\/)/i.test(url);
509958
510296
  }
@@ -509980,7 +510318,7 @@ function ensureFfmpeg() {
509980
510318
  function imageHash(imagePath) {
509981
510319
  try {
509982
510320
  const data = readFileSync39(imagePath);
509983
- return createHash6("md5").update(data).digest("hex").slice(0, 12);
510321
+ return createHash7("md5").update(data).digest("hex").slice(0, 12);
509984
510322
  } catch {
509985
510323
  return "unknown";
509986
510324
  }
@@ -514004,14 +514342,14 @@ var init_artifact_inspector = __esm({
514004
514342
  // packages/orchestrator/dist/lesson-bank.js
514005
514343
  import { existsSync as existsSync53, mkdirSync as mkdirSync27, appendFileSync as appendFileSync2, readFileSync as readFileSync44 } from "node:fs";
514006
514344
  import { join as join69, dirname as dirname19 } from "node:path";
514007
- import { createHash as createHash7 } from "node:crypto";
514345
+ import { createHash as createHash8 } from "node:crypto";
514008
514346
  function tokenize2(text) {
514009
514347
  if (!text)
514010
514348
  return [];
514011
514349
  return text.toLowerCase().split(TOKENIZE_RE).filter((t2) => t2.length >= 3).slice(0, 80);
514012
514350
  }
514013
514351
  function shortHash(s2) {
514014
- return createHash7("sha256").update(s2).digest("hex").slice(0, 16);
514352
+ return createHash8("sha256").update(s2).digest("hex").slice(0, 16);
514015
514353
  }
514016
514354
  function solicit(args) {
514017
514355
  const { taskGoal, stem, reflections, successOutputPreview } = args;
@@ -517476,7 +517814,7 @@ var init_pprRetrieval = __esm({
517476
517814
  import { join as join75 } from "node:path";
517477
517815
  import { mkdirSync as mkdirSync29, existsSync as existsSync59 } from "node:fs";
517478
517816
  import { randomUUID as randomUUID8 } from "node:crypto";
517479
- import { createHash as createHash8 } from "node:crypto";
517817
+ import { createHash as createHash9 } from "node:crypto";
517480
517818
  function readEpisodeAffect2(metadata) {
517481
517819
  if (!metadata || typeof metadata !== "object")
517482
517820
  return null;
@@ -517682,7 +518020,7 @@ var init_episodeStore = __esm({
517682
518020
  insert(ep) {
517683
518021
  const id = randomUUID8();
517684
518022
  const now = Date.now();
517685
- const contentHash = createHash8("sha256").update(ep.content).digest("hex").slice(0, 16);
518023
+ const contentHash2 = createHash9("sha256").update(ep.content).digest("hex").slice(0, 16);
517686
518024
  const modality = ep.modality ?? "text";
517687
518025
  const rawImportance = ep.importance ?? autoImportance(ep.toolName ?? null, modality, ep.content);
517688
518026
  const modulated = ep.emotionalState ? modulateImportance(sanitizeImportance(rawImportance), ep.emotionalState) : sanitizeImportance(rawImportance);
@@ -517691,13 +518029,13 @@ var init_episodeStore = __esm({
517691
518029
  return "";
517692
518030
  }
517693
518031
  const decayClass = ep.decayClass ?? autoDecayClass(ep.toolName ?? null, modality, ep.content);
517694
- const existing = this.db.prepare("SELECT id FROM episodes WHERE content_hash = ? AND session_id = ? LIMIT 1").get(contentHash, ep.sessionId ?? null);
518032
+ const existing = this.db.prepare("SELECT id FROM episodes WHERE content_hash = ? AND session_id = ? LIMIT 1").get(contentHash2, ep.sessionId ?? null);
517695
518033
  if (existing)
517696
518034
  return existing.id;
517697
518035
  this.db.prepare(`
517698
518036
  INSERT INTO episodes (id, timestamp, session_id, task_id, turn_number, modality, tool_name, content, content_hash, metadata, importance, decay_class, strength)
517699
518037
  VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 1.0)
517700
- `).run(id, now, ep.sessionId ?? null, ep.taskId ?? null, ep.turnNumber ?? null, modality, ep.toolName ?? null, ep.content, contentHash, ep.metadata ? JSON.stringify(ep.metadata) : null, importance, decayClass);
518038
+ `).run(id, now, ep.sessionId ?? null, ep.taskId ?? null, ep.turnNumber ?? null, modality, ep.toolName ?? null, ep.content, contentHash2, ep.metadata ? JSON.stringify(ep.metadata) : null, importance, decayClass);
517701
518039
  if (this.graph) {
517702
518040
  try {
517703
518041
  const episode = this.get(id);
@@ -522251,7 +522589,7 @@ var init_memoryStageContext = __esm({
522251
522589
  });
522252
522590
 
522253
522591
  // packages/memory/dist/sessionGist.js
522254
- import { createHash as createHash9 } from "node:crypto";
522592
+ import { createHash as createHash10 } from "node:crypto";
522255
522593
  function inferDomain(input) {
522256
522594
  const blob = [
522257
522595
  input.goal,
@@ -522276,7 +522614,7 @@ function inferDomain(input) {
522276
522614
  return ranked[0][0];
522277
522615
  }
522278
522616
  function computeGoalHash(goal) {
522279
- return createHash9("sha256").update(goal.trim().toLowerCase()).digest("hex").slice(0, 16);
522617
+ return createHash10("sha256").update(goal.trim().toLowerCase()).digest("hex").slice(0, 16);
522280
522618
  }
522281
522619
  function clip(text, n2) {
522282
522620
  if (!text)
@@ -522487,12 +522825,12 @@ var init_toolOutcomes = __esm({
522487
522825
  });
522488
522826
 
522489
522827
  // packages/memory/dist/stagnationRecipes.js
522490
- import { createHash as createHash10 } from "node:crypto";
522828
+ import { createHash as createHash11 } from "node:crypto";
522491
522829
  function fingerprintSignature(fp) {
522492
522830
  const normClusters = (fp.errorClusters ?? []).map((s2) => (s2 || "").toLowerCase().replace(/[0-9]+/g, "N").replace(/\s+/g, " ").trim()).filter(Boolean).sort();
522493
522831
  const tool = (fp.stuckTool ?? "").toLowerCase().trim();
522494
522832
  const blob = `tool=${tool};clusters=${normClusters.join("|")}`;
522495
- return createHash10("sha256").update(blob).digest("hex").slice(0, 16);
522833
+ return createHash11("sha256").update(blob).digest("hex").slice(0, 16);
522496
522834
  }
522497
522835
  function crystallize(store2, input) {
522498
522836
  const sig = fingerprintSignature(input.fingerprint);
@@ -522549,7 +522887,7 @@ var init_stagnationRecipes = __esm({
522549
522887
  });
522550
522888
 
522551
522889
  // packages/memory/dist/codebaseMap.js
522552
- import { createHash as createHash11, randomUUID as randomUUID12 } from "node:crypto";
522890
+ import { createHash as createHash12, randomUUID as randomUUID12 } from "node:crypto";
522553
522891
  function freshNodeId() {
522554
522892
  return randomUUID12();
522555
522893
  }
@@ -522563,7 +522901,7 @@ var init_codebaseMap = __esm({
522563
522901
  touchCount = /* @__PURE__ */ new Map();
522564
522902
  constructor(db, repoRoot, commitSha) {
522565
522903
  this.db = db;
522566
- this.repoFp = createHash11("sha256").update(`${repoRoot}::${commitSha ?? "no-commit"}`).digest("hex").slice(0, 16);
522904
+ this.repoFp = createHash12("sha256").update(`${repoRoot}::${commitSha ?? "no-commit"}`).digest("hex").slice(0, 16);
522567
522905
  this.ensureSchema();
522568
522906
  }
522569
522907
  ensureSchema() {
@@ -522699,7 +523037,7 @@ var init_codebaseMap = __esm({
522699
523037
  }
522700
523038
  /** Stable composite id: `<kind>:<sha16(path)>` so insert ON CONFLICT works. */
522701
523039
  idFor(kind, path11) {
522702
- const h = createHash11("sha256").update(`${this.repoFp}:${kind}:${path11}`).digest("hex").slice(0, 24);
523040
+ const h = createHash12("sha256").update(`${this.repoFp}:${kind}:${path11}`).digest("hex").slice(0, 24);
522703
523041
  return `${kind}-${h}`;
522704
523042
  }
522705
523043
  };
@@ -524758,7 +525096,7 @@ import { existsSync as existsSync66, readFileSync as readFileSync54, statSync as
524758
525096
  import { execSync as execSync45 } from "node:child_process";
524759
525097
  import { homedir as homedir22, platform as platform2, arch as arch2, totalmem as totalmem2, freemem as freemem2, hostname as hostname3 } from "node:os";
524760
525098
  import { join as join82 } from "node:path";
524761
- import { createHash as createHash12 } from "node:crypto";
525099
+ import { createHash as createHash13 } from "node:crypto";
524762
525100
  function capturePreflightSnapshot(workingDir) {
524763
525101
  const warnings = [];
524764
525102
  const configFingerprints = {};
@@ -524925,7 +525263,7 @@ function expandPath(p2) {
524925
525263
  return p2;
524926
525264
  }
524927
525265
  function sha2563(s2) {
524928
- return createHash12("sha256").update(s2).digest("hex").slice(0, 16);
525266
+ return createHash13("sha256").update(s2).digest("hex").slice(0, 16);
524929
525267
  }
524930
525268
  function freeDiskBytes(path11 = "/tmp") {
524931
525269
  try {
@@ -524997,6 +525335,9 @@ function classifyShellIntent(cmd) {
524997
525335
  if (tokens.length === 0)
524998
525336
  return { klass: "other", verb: "", tool: "" };
524999
525337
  const first2 = tokens[0].toLowerCase();
525338
+ if (first2 === "test" || first2 === "[" || first2 === "[[") {
525339
+ return { klass: "read", verb: "test", tool: first2 };
525340
+ }
525000
525341
  const isRunner = RUNNERS.has(first2);
525001
525342
  let verbToken;
525002
525343
  if (isRunner && tokens.length >= 2) {
@@ -526876,14 +527217,14 @@ Your NEXT tool call MUST be EXACTLY ONE of:
526876
527217
  • file_write — create a new file
526877
527218
  • file_edit — modify an existing file (find/replace)
526878
527219
  • batch_edit — multiple find/replace edits in one call
526879
- • file_patch — apply a unified diff
527220
+ • file_patch — apply a version-checked line-range patch
526880
527221
 
526881
527222
  These are the ONLY four tools that count as creative edits. The following do NOT count and will NOT satisfy this directive:
526882
527223
  • todo_write (only updates the todo list, not the filesystem)
526883
527224
  • memory_write (writes to memory store, not the project)
526884
527225
  • list_directory / file_read / file_explore / grep_search / shell
526885
527226
 
526886
- Pick the SMALLEST concrete deliverable from the spec — typically the project entry point or the file most other modules depend on. Write a stub or skeleton if the full implementation is too large; you can iterate later. Do NOT issue another read or todo update before producing the next file_write/file_edit/batch_edit/file_patch.`;
527227
+ Pick the SMALLEST concrete deliverable from the spec — typically the project entry point or the file most other modules depend on. Write a complete, version-checked slice that changes disk state; you can iterate later. Do NOT issue another read or todo update before producing the next file_write/file_edit/batch_edit/file_patch.`;
526887
527228
  messages2.push({ role: "system", content: reg61Msg });
526888
527229
  const _cyclePart = cycleLabel ? ` (${cycleLabel})` : "";
526889
527230
  this.emit({
@@ -526918,8 +527259,9 @@ Pick the SMALLEST concrete deliverable from the spec — typically the project e
526918
527259
  const _editTools = /* @__PURE__ */ new Set(["file_write", "file_edit", "batch_edit", "file_patch"]);
526919
527260
  if (!_editTools.has(tc.name))
526920
527261
  return null;
526921
- const _editPath = tc.arguments?.["path"] ?? "";
526922
- if (!_editPath || this._decomp2MainContextFiles.has(_editPath))
527262
+ const _editPaths = this._extractToolTargetPaths(tc.name, tc.arguments);
527263
+ const _editPath = _editPaths.find((p2) => !this._decomp2MainContextFiles.has(p2)) ?? "";
527264
+ if (!_editPath)
526923
527265
  return null;
526924
527266
  const _filesList = [...this._decomp2MainContextFiles].slice(0, 8).map((p2) => ` • ${p2}`).join("\n");
526925
527267
  const _moreFiles = this._decomp2MainContextFiles.size > 8 ? `
@@ -526979,21 +527321,18 @@ Pick the SMALLEST concrete deliverable from the spec — typically the project e
526979
527321
  _trackDecomp2(tc, result, turn) {
526980
527322
  if (process.env["OA_DISABLE_DECOMP2"] === "1")
526981
527323
  return;
526982
- if (result && result.success !== false) {
526983
- const _editTools = /* @__PURE__ */ new Set(["file_write", "file_edit", "batch_edit", "file_patch"]);
526984
- if (_editTools.has(tc.name)) {
526985
- const _editPath = tc.arguments?.["path"] ?? "";
526986
- if (_editPath && typeof _editPath === "string") {
526987
- this._decomp2MainContextFiles.add(_editPath);
526988
- const DECOMP2_FILE_SPREAD_THRESHOLD = 5;
526989
- if (!this._decomp2GateActive && this._decomp2MainContextFiles.size >= DECOMP2_FILE_SPREAD_THRESHOLD && this._decomp2SubAgentCalls === 0) {
526990
- this._decomp2GateActive = true;
526991
- this.emit({
526992
- type: "status",
526993
- content: `DECOMP-2 NEW-FILE GATE ACTIVATED — ${this._decomp2MainContextFiles.size} distinct files edited in main context, 0 sub_agent calls; further edits to NEW files will be blocked until sub_agent is invoked`,
526994
- timestamp: (/* @__PURE__ */ new Date()).toISOString()
526995
- });
526996
- }
527324
+ if (this._isRealProjectMutation(tc.name, result)) {
527325
+ const _editPaths = this._extractToolTargetPaths(tc.name, tc.arguments, result);
527326
+ for (const _editPath of _editPaths) {
527327
+ this._decomp2MainContextFiles.add(_editPath);
527328
+ const DECOMP2_FILE_SPREAD_THRESHOLD = 5;
527329
+ if (!this._decomp2GateActive && this._decomp2MainContextFiles.size >= DECOMP2_FILE_SPREAD_THRESHOLD && this._decomp2SubAgentCalls === 0) {
527330
+ this._decomp2GateActive = true;
527331
+ this.emit({
527332
+ type: "status",
527333
+ content: `DECOMP-2 NEW-FILE GATE ACTIVATED — ${this._decomp2MainContextFiles.size} distinct files edited in main context, 0 sub_agent calls; further edits to NEW files will be blocked until sub_agent is invoked`,
527334
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
527335
+ });
526997
527336
  }
526998
527337
  }
526999
527338
  }
@@ -527101,6 +527440,31 @@ Pick the SMALLEST concrete deliverable from the spec — typically the project e
527101
527440
  return null;
527102
527441
  }
527103
527442
  }
527443
+ _isProjectEditTool(toolName) {
527444
+ return toolName === "file_write" || toolName === "file_edit" || toolName === "batch_edit" || toolName === "file_patch";
527445
+ }
527446
+ _extractToolTargetPaths(toolName, args, result) {
527447
+ const fromResult = Array.isArray(result?.mutatedFiles) ? result.mutatedFiles.filter((p3) => typeof p3 === "string" && p3.trim().length > 0) : [];
527448
+ if (fromResult.length > 0)
527449
+ return [...new Set(fromResult)];
527450
+ if (toolName === "batch_edit" && Array.isArray(args?.["edits"])) {
527451
+ const paths = args["edits"].map((edit) => edit && typeof edit === "object" ? edit["path"] : null).filter((p3) => typeof p3 === "string" && p3.trim().length > 0);
527452
+ return [...new Set(paths)];
527453
+ }
527454
+ const p2 = args?.["path"] ?? args?.["file_path"] ?? args?.["file"] ?? args?.["filename"] ?? args?.["filepath"] ?? args?.["filePath"];
527455
+ return typeof p2 === "string" && p2.trim().length > 0 ? [p2.trim()] : [];
527456
+ }
527457
+ _isRealProjectMutation(toolName, result) {
527458
+ if (!this._isProjectEditTool(toolName))
527459
+ return false;
527460
+ if (!result || result.success === false)
527461
+ return false;
527462
+ if (typeof result.mutated === "boolean")
527463
+ return result.mutated;
527464
+ if (result.dryRun || result.noop || result.partial)
527465
+ return false;
527466
+ return true;
527467
+ }
527104
527468
  /** Track the turn index of the last todo_write call so the reminder
527105
527469
  * path can compute `turnsSinceLastTodoWrite` cheaply without walking
527106
527470
  * the entire messages array. Reset on run(). */
@@ -527765,7 +528129,7 @@ ${body}`;
527765
528129
  * converging — particularly common when a build error names a specific
527766
528130
  * file the agent thinks needs more work.
527767
528131
  *
527768
- * Suggests targeted alternatives (file_edit/edit_block) and "good enough"
528132
+ * Suggests targeted alternatives (file_edit/file_patch) and "good enough"
527769
528133
  * recognition. Returns null when no churn is happening.
527770
528134
  */
527771
528135
  _renderWriteChurnBlock(turn) {
@@ -527789,7 +528153,7 @@ ${body}`;
527789
528153
  for (const c9 of top) {
527790
528154
  lines.push(` • ${c9.path} — ${c9.writes} writes (most recent ${c9.ageTurns} turn${c9.ageTurns === 1 ? "" : "s"} ago)`);
527791
528155
  }
527792
- lines.push("Refining the same file again and again rarely converges. If you have a specific change in mind, use file_edit or edit_block for targeted changes. If you're trying different approaches, read the file once with file_read to see current state, then decide. If the current version is reasonable but a downstream tool fails, the bug is probably in the OTHER file (importer, schema, dep) — not this one.");
528156
+ lines.push("Refining the same file again and again rarely converges. If you have a specific change in mind, use file_edit or file_patch for targeted changes. If you're trying different approaches, read the file once with file_read to see current state, then decide. If the current version is reasonable but a downstream tool fails, the bug is probably in the OTHER file (importer, schema, dep) — not this one.");
527793
528157
  lines.push(`(turn ${turn} — this warning auto-clears when no path has ≥3 writes within 10 turns)`);
527794
528158
  return lines.join("\n");
527795
528159
  }
@@ -527810,6 +528174,10 @@ ${body}`;
527810
528174
  return false;
527811
528175
  if (/(^|[^&\d])(>|>>)\s*\S/.test(cmd))
527812
528176
  return false;
528177
+ if (/(^|[^<])<\s*\S/.test(cmd))
528178
+ return false;
528179
+ if (/\bpatch\b/i.test(cmd))
528180
+ return false;
527813
528181
  const MUTATE_BINS = [
527814
528182
  // POSIX file/process mutators
527815
528183
  "rm",
@@ -528013,8 +528381,6 @@ ${body}`;
528013
528381
  "unexpand",
528014
528382
  "diff",
528015
528383
  "cmp",
528016
- "patch",
528017
- // patch with -R or no-args could be mutating; --dry-run only is read
528018
528384
  "echo",
528019
528385
  "printf",
528020
528386
  "pwd",
@@ -528206,10 +528572,7 @@ ${body}`;
528206
528572
  */
528207
528573
  _renderTodoStateBlock(turn) {
528208
528574
  try {
528209
- const { readTodos: readTodos2 } = __require("@open-agents/execution");
528210
- const { getTodoSessionId: getTodoSessionId2 } = __require("@open-agents/execution");
528211
- const sessionId = getTodoSessionId2();
528212
- const todos = readTodos2(sessionId);
528575
+ const todos = this.readSessionTodos();
528213
528576
  if (!todos || todos.length === 0)
528214
528577
  return null;
528215
528578
  const lines = ["[CURRENT TODO PLAN — already in your state, no need to call todo_write to see it]"];
@@ -529399,7 +529762,6 @@ If the hypothesis cannot be tested by a creative edit, ask the human via task_co
529399
529762
  content: `REG-58 NO-WRITE STAGNATION — ${gap} turns since last creative edit (turn ${this._lastFileWriteTurn})${_telSuffix58}`,
529400
529763
  timestamp: (/* @__PURE__ */ new Date()).toISOString()
529401
529764
  });
529402
- this._lastFileWriteTurn = turn;
529403
529765
  }
529404
529766
  const REG60_WINDOW_MS = 60 * 60 * 1e3;
529405
529767
  const REG60_MIN_WRITES = 3;
@@ -529626,8 +529988,11 @@ If this matches your current shape, try it before continuing.`
529626
529988
  if (_readClass.has(c9.name)) {
529627
529989
  _readCount++;
529628
529990
  } else if (_mutationClass.has(c9.name)) {
529629
- if (!_isStale)
529991
+ if (c9.mutated === true) {
529992
+ _mutationCount++;
529993
+ } else if (c9.success === true && c9.mutated !== false && !_isStale) {
529630
529994
  _mutationCount++;
529995
+ }
529631
529996
  } else if (c9.name === "shell") {
529632
529997
  if (c9.success === true && !_isStale) {
529633
529998
  _readCount++;
@@ -529801,6 +530166,14 @@ ${_staleSamples.join("\n")}` : ``,
529801
530166
  for (const c9 of _wtWindow) {
529802
530167
  if (!_wtWriteClass.has(c9.name))
529803
530168
  continue;
530169
+ if (c9.mutated === false)
530170
+ continue;
530171
+ if (Array.isArray(c9.mutatedFiles) && c9.mutatedFiles.length > 0) {
530172
+ for (const p2 of c9.mutatedFiles) {
530173
+ _wtFileCounts.set(p2, (_wtFileCounts.get(p2) ?? 0) + 1);
530174
+ }
530175
+ continue;
530176
+ }
529804
530177
  const _ak = c9.argsKey || "";
529805
530178
  const _m = /(?:^|,)path=([^,]+)/.exec(_ak);
529806
530179
  const _pk = _m ? _m[1].slice(0, 200) : _ak.slice(0, 200);
@@ -530428,8 +530801,8 @@ If you're stuck, try a completely different approach. Do NOT repeat what failed
530428
530801
  if (process.env["OA_DISABLE_ADAPTIVE_RETRIEVAL"] !== "1") {
530429
530802
  const goalForSig = (this._taskState.goal || "").slice(0, 200);
530430
530803
  const recentTools = this._toolSequence.slice(-5).join("|");
530431
- const { createHash: createHash20 } = await import("node:crypto");
530432
- const sig = createHash20("sha256").update(`${goalForSig}::${recentTools}`).digest("hex").slice(0, 16);
530804
+ const { createHash: createHash21 } = await import("node:crypto");
530805
+ const sig = createHash21("sha256").update(`${goalForSig}::${recentTools}`).digest("hex").slice(0, 16);
530433
530806
  if (this._lastPprSig === sig && this._lastPprMemoryLines.length > 0) {
530434
530807
  compacted.push({
530435
530808
  role: "system",
@@ -531005,14 +531378,6 @@ ${memoryLines.join("\n")}`
531005
531378
  "priority_delegate",
531006
531379
  "background_run"
531007
531380
  ]);
531008
- if (REG61_EDIT_TOOLS.has(tc.name) && this._reg61PerpetualGateActive) {
531009
- this._reg61PerpetualGateActive = false;
531010
- this.emit({
531011
- type: "status",
531012
- content: `REG-61 GATE CLEARED — '${tc.name}' satisfied REG-61 directive at turn ${turn}`,
531013
- timestamp: (/* @__PURE__ */ new Date()).toISOString()
531014
- });
531015
- }
531016
531381
  if (this._reg61PerpetualGateActive && !REG61_BYPASS_TOOLS.has(tc.name) && process.env["OA_DISABLE_REG61_COERCE"] !== "1") {
531017
531382
  this.emit({
531018
531383
  type: "tool_call",
@@ -531038,7 +531403,7 @@ ${memoryLines.join("\n")}`
531038
531403
  ``,
531039
531404
  `This is NOT random guessing — it's targeted hypothesis falsification. Reading the same files 5+ times has already proven uninformative; only a state change will move the system.`,
531040
531405
  ``,
531041
- `Issue EXACTLY ONE of: file_write / file_edit / batch_edit / file_patch on a single concrete change. The exact CHOICE of edit matters less than NOT continuing to re-read.`,
531406
+ `Issue EXACTLY ONE of: file_write / file_edit / batch_edit / file_patch on a single concrete change. The edit must actually change disk state; dry-runs and no-ops do not clear this directive.`,
531042
531407
  ``,
531043
531408
  `Allowed bypasses (will not be blocked but will not clear the directive either):`,
531044
531409
  ` • web_search — search the EXACT recurring error string`,
@@ -531055,14 +531420,14 @@ ${memoryLines.join("\n")}`
531055
531420
  ` • file_write — create a new file`,
531056
531421
  ` • file_edit — modify an existing file (find/replace)`,
531057
531422
  ` • batch_edit — multiple find/replace edits in one call`,
531058
- ` • file_patch — apply a unified diff`,
531423
+ ` • file_patch — apply a version-checked line-range patch`,
531059
531424
  ``,
531060
531425
  `These tools are also allowed while the directive is active (will not be blocked, will not clear the gate):`,
531061
531426
  ` • web_search — for genuinely-unknown APIs / error strings`,
531062
531427
  ` • task_complete — to exit if you cannot make any progress`,
531063
531428
  ` • ask_user — to escalate to human (if available)`,
531064
531429
  ``,
531065
- `Until you issue a creative edit, ALL of these will be BLOCKED again on every turn: file_read, file_explore, list_directory, grep_search, shell, todo_write, todo_read, memory_read, memory_write, etc. Pick the smallest concrete change that moves work forward — even a partial stub or a single-line edit counts.`
531430
+ `Until you issue a creative edit that actually changes disk, ALL of these will be BLOCKED again on every turn: file_read, file_explore, list_directory, grep_search, shell, todo_write, todo_read, memory_read, memory_write, etc. Pick the smallest concrete change that moves work forward.`
531066
531431
  ].join("\n");
531067
531432
  this.emit({
531068
531433
  type: "tool_result",
@@ -531514,6 +531879,16 @@ Respond with EXACTLY this structure before your next tool call:
531514
531879
  }
531515
531880
  }
531516
531881
  }
531882
+ const realFileMutation = this._isRealProjectMutation(tc.name, result);
531883
+ const realMutationPaths = realFileMutation ? this._extractToolTargetPaths(tc.name, tc.arguments, result) : [];
531884
+ if (realFileMutation && this._reg61PerpetualGateActive) {
531885
+ this._reg61PerpetualGateActive = false;
531886
+ this.emit({
531887
+ type: "status",
531888
+ content: `REG-61 GATE CLEARED — '${tc.name}' landed real file mutation at turn ${turn}`,
531889
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
531890
+ });
531891
+ }
531517
531892
  const updated = this._argCohorts.get(cohortKey) || { success: 0, failure: 0, lastOutcomeTurn: turn };
531518
531893
  if (result.success)
531519
531894
  updated.success++;
@@ -531522,9 +531897,9 @@ Respond with EXACTLY this structure before your next tool call:
531522
531897
  updated.lastOutcomeTurn = turn;
531523
531898
  this._argCohorts.set(cohortKey, updated);
531524
531899
  try {
531525
- if (tc.name === "file_write" || tc.name === "file_edit" || tc.name === "edit_block" || tc.name === "batch_edit") {
531526
- const p2 = String(tc.arguments?.["path"] ?? tc.arguments?.["file_path"] ?? tc.arguments?.["file"] ?? "");
531527
- if (p2 && result.success) {
531900
+ if (realFileMutation) {
531901
+ const paths = realMutationPaths.length > 0 ? realMutationPaths : this._extractToolTargetPaths(tc.name, tc.arguments, result);
531902
+ for (const p2 of paths) {
531528
531903
  const prev = this._worldFacts.files.get(p2);
531529
531904
  this._worldFacts.files.set(p2, {
531530
531905
  exists: true,
@@ -531534,7 +531909,9 @@ Respond with EXACTLY this structure before your next tool call:
531534
531909
  lastWriteTurn: turn,
531535
531910
  writeCount: (prev?.writeCount ?? 0) + 1
531536
531911
  });
531537
- this._writesSinceLastTodoWrite++;
531912
+ }
531913
+ if (paths.length > 0) {
531914
+ this._writesSinceLastTodoWrite += paths.length;
531538
531915
  if (this._writesSinceLastTodoWrite >= 6 && !this._progressGateActive) {
531539
531916
  this._progressGateActive = true;
531540
531917
  this.emit({
@@ -531907,12 +532284,17 @@ Respond with EXACTLY this structure before your next tool call:
531907
532284
  const lastLog = toolCallLog[toolCallLog.length - 1];
531908
532285
  if (lastLog) {
531909
532286
  lastLog.success = false;
532287
+ lastLog.mutated = false;
532288
+ lastLog.mutatedFiles = [];
531910
532289
  lastLog.outputPreview = errorText.slice(0, 100);
531911
532290
  }
531912
532291
  } else if (result.success) {
531913
532292
  const lastLog = toolCallLog[toolCallLog.length - 1];
531914
- if (lastLog)
532293
+ if (lastLog) {
531915
532294
  lastLog.success = true;
532295
+ lastLog.mutated = realFileMutation;
532296
+ lastLog.mutatedFiles = realMutationPaths;
532297
+ }
531916
532298
  if (tc.name === "shell") {
531917
532299
  const _shellCmd = String(tc.arguments?.["command"] ?? tc.arguments?.["cmd"] ?? "");
531918
532300
  const _typecheckOnly = /\b(--noEmit|--dry-run|--check\b|\bmypy\b|\bruff check\b|\bcargo check\b|\bstylelint --check\b|\bpylint\b(?!.*--exit-zero))\b/i.test(_shellCmd);
@@ -532013,16 +532395,16 @@ Respond with EXACTLY this structure before your next tool call:
532013
532395
  } catch {
532014
532396
  }
532015
532397
  }
532016
- if (["file_write", "file_edit", "file_patch", "batch_edit"].includes(tc.name) && this._patchHistoryStore) {
532398
+ if (realFileMutation && this._patchHistoryStore) {
532017
532399
  try {
532018
- const filePath2 = tc.arguments?.path || tc.arguments?.file_path;
532019
- if (filePath2) {
532400
+ const patchFiles = realMutationPaths.length > 0 ? realMutationPaths : this._extractToolTargetPaths(tc.name, tc.arguments, result);
532401
+ for (const filePath2 of patchFiles) {
532020
532402
  this._patchHistoryStore.insert({
532021
532403
  taskId: this._sessionId,
532022
532404
  sessionId: this._sessionId,
532023
532405
  repoRoot: process.cwd(),
532024
532406
  filePath: filePath2,
532025
- diff: JSON.stringify(tc.arguments).slice(0, 1e3),
532407
+ diff: (result.diff ?? JSON.stringify(tc.arguments)).slice(0, 1e3),
532026
532408
  status: "applied",
532027
532409
  appliedAt: (/* @__PURE__ */ new Date()).toISOString(),
532028
532410
  revertedAt: null,
@@ -532043,13 +532425,13 @@ Respond with EXACTLY this structure before your next tool call:
532043
532425
  }
532044
532426
  }
532045
532427
  }
532046
- const isFileMutation = ["file_write", "file_edit", "batch_edit", "file_patch"].includes(tc.name);
532047
- if (isFileMutation && result.success && dedupHitCount.size > 0) {
532428
+ const isFileMutation = realFileMutation;
532429
+ if (isFileMutation && dedupHitCount.size > 0) {
532048
532430
  dedupHitCount.clear();
532049
532431
  }
532050
- if (isFileMutation && result.success) {
532051
- this._fileWritesSinceLastWorldState++;
532052
- this._fileWritesThisRun++;
532432
+ if (isFileMutation) {
532433
+ this._fileWritesSinceLastWorldState += Math.max(1, realMutationPaths.length);
532434
+ this._fileWritesThisRun += Math.max(1, realMutationPaths.length);
532053
532435
  }
532054
532436
  if (result.success) {
532055
532437
  this._recentFailures = this._recentFailures.filter((f2) => f2.fingerprint !== toolFingerprint);
@@ -532197,14 +532579,14 @@ Respond with EXACTLY this structure before your next tool call:
532197
532579
  timestamp: (/* @__PURE__ */ new Date()).toISOString()
532198
532580
  });
532199
532581
  this._taskState.toolCallCount++;
532200
- if (result && result.success !== false) {
532201
- const creativeTools = ["file_write", "file_edit", "batch_edit", "file_patch"];
532202
- if (creativeTools.includes(tc.name)) {
532203
- this._lastFileWriteTurn = turn;
532582
+ if (realFileMutation) {
532583
+ this._lastFileWriteTurn = turn;
532584
+ const writeEvents = Math.max(1, realMutationPaths.length);
532585
+ for (let i2 = 0; i2 < writeEvents; i2++) {
532204
532586
  this._fileWriteTimestamps.push(Date.now());
532205
- if (this._fileWriteTimestamps.length > 200) {
532206
- this._fileWriteTimestamps.shift();
532207
- }
532587
+ }
532588
+ while (this._fileWriteTimestamps.length > 200) {
532589
+ this._fileWriteTimestamps.shift();
532208
532590
  }
532209
532591
  }
532210
532592
  this._trackDecomp2(tc, result, turn);
@@ -544333,7 +544715,7 @@ var require_websocket3 = __commonJS({
544333
544715
  var http6 = __require("http");
544334
544716
  var net5 = __require("net");
544335
544717
  var tls2 = __require("tls");
544336
- var { randomBytes: randomBytes25, createHash: createHash20 } = __require("crypto");
544718
+ var { randomBytes: randomBytes25, createHash: createHash21 } = __require("crypto");
544337
544719
  var { Duplex: Duplex3, Readable } = __require("stream");
544338
544720
  var { URL: URL3 } = __require("url");
544339
544721
  var PerMessageDeflate2 = require_permessage_deflate3();
@@ -544993,7 +545375,7 @@ var require_websocket3 = __commonJS({
544993
545375
  abortHandshake(websocket, socket, "Invalid Upgrade header");
544994
545376
  return;
544995
545377
  }
544996
- const digest3 = createHash20("sha1").update(key + GUID).digest("base64");
545378
+ const digest3 = createHash21("sha1").update(key + GUID).digest("base64");
544997
545379
  if (res.headers["sec-websocket-accept"] !== digest3) {
544998
545380
  abortHandshake(websocket, socket, "Invalid Sec-WebSocket-Accept header");
544999
545381
  return;
@@ -545360,7 +545742,7 @@ var require_websocket_server2 = __commonJS({
545360
545742
  var EventEmitter13 = __require("events");
545361
545743
  var http6 = __require("http");
545362
545744
  var { Duplex: Duplex3 } = __require("stream");
545363
- var { createHash: createHash20 } = __require("crypto");
545745
+ var { createHash: createHash21 } = __require("crypto");
545364
545746
  var extension2 = require_extension3();
545365
545747
  var PerMessageDeflate2 = require_permessage_deflate3();
545366
545748
  var subprotocol2 = require_subprotocol2();
@@ -545661,7 +546043,7 @@ var require_websocket_server2 = __commonJS({
545661
546043
  );
545662
546044
  }
545663
546045
  if (this._state > RUNNING) return abortHandshake(socket, 503);
545664
- const digest3 = createHash20("sha1").update(key + GUID).digest("base64");
546046
+ const digest3 = createHash21("sha1").update(key + GUID).digest("base64");
545665
546047
  const headers = [
545666
546048
  "HTTP/1.1 101 Switching Protocols",
545667
546049
  "Upgrade: websocket",
@@ -546721,7 +547103,7 @@ __export(oa_directory_exports, {
546721
547103
  import { existsSync as existsSync74, mkdirSync as mkdirSync42, readFileSync as readFileSync61, writeFileSync as writeFileSync38, readdirSync as readdirSync21, statSync as statSync25, unlinkSync as unlinkSync14, openSync as openSync2, closeSync as closeSync2, renameSync as renameSync3 } from "node:fs";
546722
547104
  import { join as join93, relative as relative8, basename as basename14, dirname as dirname27 } from "node:path";
546723
547105
  import { homedir as homedir26 } from "node:os";
546724
- import { createHash as createHash13 } from "node:crypto";
547106
+ import { createHash as createHash14 } from "node:crypto";
546725
547107
  function findGitRoot(startDir) {
546726
547108
  let dir = startDir;
546727
547109
  const visited = /* @__PURE__ */ new Set();
@@ -547086,7 +547468,7 @@ function buildHandoffPrompt(repoRoot) {
547086
547468
  return lines.join("\n");
547087
547469
  }
547088
547470
  function computeDedupeHash(task, savedAt) {
547089
- return createHash13("sha256").update(`${task}|${savedAt}`).digest("hex").slice(0, 16);
547471
+ return createHash14("sha256").update(`${task}|${savedAt}`).digest("hex").slice(0, 16);
547090
547472
  }
547091
547473
  function generateSessionId() {
547092
547474
  const timestamp = Date.now().toString(36);
@@ -575641,7 +576023,7 @@ var init_types = __esm({
575641
576023
  });
575642
576024
 
575643
576025
  // packages/cli/src/tui/p2p/secret-vault.ts
575644
- import { createCipheriv as createCipheriv3, createDecipheriv as createDecipheriv3, randomBytes as randomBytes19, scryptSync as scryptSync2, createHash as createHash14 } from "node:crypto";
576026
+ import { createCipheriv as createCipheriv3, createDecipheriv as createDecipheriv3, randomBytes as randomBytes19, scryptSync as scryptSync2, createHash as createHash15 } from "node:crypto";
575645
576027
  import { readFileSync as readFileSync71, writeFileSync as writeFileSync46, existsSync as existsSync86, mkdirSync as mkdirSync50 } from "node:fs";
575646
576028
  import { dirname as dirname31 } from "node:path";
575647
576029
  var PLACEHOLDER_PREFIX, PLACEHOLDER_SUFFIX, CIPHER_ALGO, SALT_LEN, IV_LEN, KEY_LEN, SecretVault;
@@ -575886,7 +576268,7 @@ var init_secret_vault = __esm({
575886
576268
  /** Generate a deterministic fingerprint of vault contents (for sync verification) */
575887
576269
  fingerprint() {
575888
576270
  const names = Array.from(this.secrets.keys()).sort();
575889
- const hash = createHash14("sha256");
576271
+ const hash = createHash15("sha256");
575890
576272
  for (const name10 of names) {
575891
576273
  hash.update(name10 + ":");
575892
576274
  hash.update(this.secrets.get(name10).value);
@@ -575901,7 +576283,7 @@ var init_secret_vault = __esm({
575901
576283
  // packages/cli/src/tui/p2p/peer-mesh.ts
575902
576284
  import { EventEmitter as EventEmitter7 } from "node:events";
575903
576285
  import { createServer as createServer5 } from "node:http";
575904
- import { randomBytes as randomBytes20, createHash as createHash15, generateKeyPairSync } from "node:crypto";
576286
+ import { randomBytes as randomBytes20, createHash as createHash16, generateKeyPairSync } from "node:crypto";
575905
576287
  var PING_INTERVAL_MS, PEER_TIMEOUT_MS, GOSSIP_INTERVAL_MS, MAX_PEERS, PeerMesh;
575906
576288
  var init_peer_mesh = __esm({
575907
576289
  "packages/cli/src/tui/p2p/peer-mesh.ts"() {
@@ -575918,7 +576300,7 @@ var init_peer_mesh = __esm({
575918
576300
  const { publicKey: publicKey2, privateKey } = generateKeyPairSync("ed25519");
575919
576301
  this.publicKey = publicKey2.export({ type: "spki", format: "der" });
575920
576302
  this.privateKey = privateKey.export({ type: "pkcs8", format: "der" });
575921
- this.peerId = createHash15("sha256").update(this.publicKey).digest("base64url").slice(0, 22);
576303
+ this.peerId = createHash16("sha256").update(this.publicKey).digest("base64url").slice(0, 22);
575922
576304
  this.capabilities = options2.capabilities;
575923
576305
  this.displayName = options2.displayName;
575924
576306
  this._authKey = options2.authKey ?? randomBytes20(24).toString("base64url");
@@ -585025,14 +585407,14 @@ var init_access_policy = __esm({
585025
585407
  });
585026
585408
 
585027
585409
  // packages/cli/src/api/project-preferences.ts
585028
- import { createHash as createHash16 } from "node:crypto";
585410
+ import { createHash as createHash17 } from "node:crypto";
585029
585411
  import { existsSync as existsSync97, mkdirSync as mkdirSync59, readFileSync as readFileSync81, renameSync as renameSync7, writeFileSync as writeFileSync53, unlinkSync as unlinkSync22 } from "node:fs";
585030
585412
  import { homedir as homedir36 } from "node:os";
585031
585413
  import { join as join116, resolve as resolve36 } from "node:path";
585032
585414
  import { randomUUID as randomUUID15 } from "node:crypto";
585033
585415
  function projectKey(root) {
585034
585416
  const canonical = resolve36(root);
585035
- return createHash16("sha256").update(canonical).digest("hex").slice(0, 16);
585417
+ return createHash17("sha256").update(canonical).digest("hex").slice(0, 16);
585036
585418
  }
585037
585419
  function projectDir(root) {
585038
585420
  return join116(PROJECTS_DIR, projectKey(root));
@@ -586210,7 +586592,7 @@ var init_disk_task_output = __esm({
586210
586592
  });
586211
586593
 
586212
586594
  // packages/cli/src/api/http.ts
586213
- import { createHash as createHash17 } from "node:crypto";
586595
+ import { createHash as createHash18 } from "node:crypto";
586214
586596
  function problemDetails(opts) {
586215
586597
  const p2 = {
586216
586598
  type: opts.type ?? "about:blank",
@@ -586273,7 +586655,7 @@ function paginated(items, page2, total) {
586273
586655
  }
586274
586656
  function computeEtag(payload) {
586275
586657
  const json = typeof payload === "string" ? payload : JSON.stringify(payload);
586276
- const hash = createHash17("sha1").update(json).digest("hex").slice(0, 16);
586658
+ const hash = createHash18("sha1").update(json).digest("hex").slice(0, 16);
586277
586659
  return `W/"${hash}"`;
586278
586660
  }
586279
586661
  function checkNotModified(req2, res, etag) {
@@ -600253,7 +600635,7 @@ import { homedir as homedir43 } from "node:os";
600253
600635
  import { spawn as spawn26, execSync as execSync57 } from "node:child_process";
600254
600636
  import { mkdirSync as mkdirSync67, writeFileSync as writeFileSync59, readFileSync as readFileSync89, readdirSync as readdirSync36, existsSync as existsSync108, watch as fsWatch3, renameSync as renameSync8, unlinkSync as unlinkSync24 } from "node:fs";
600255
600637
  import { randomBytes as randomBytes23, randomUUID as randomUUID16 } from "node:crypto";
600256
- import { createHash as createHash19 } from "node:crypto";
600638
+ import { createHash as createHash20 } from "node:crypto";
600257
600639
  function getVersion3() {
600258
600640
  try {
600259
600641
  const thisDir = dirname36(fileURLToPath17(import.meta.url));
@@ -605894,7 +606276,7 @@ function listScheduledTasks() {
605894
606276
  const schedule = String(t2?.schedule || t2?.cron || t2?.when || "");
605895
606277
  const enabled2 = typeof t2?.enabled === "boolean" ? t2.enabled : true;
605896
606278
  const realId = typeof t2?.id === "string" && t2.id ? t2.id : null;
605897
- const fallbackId = createHash19("sha1").update(`${file}#${i2}`).digest("hex").slice(0, 16);
606279
+ const fallbackId = createHash20("sha1").update(`${file}#${i2}`).digest("hex").slice(0, 16);
605898
606280
  const uid = realId || fallbackId;
605899
606281
  const key = `${uid}`;
605900
606282
  if (seen.has(key)) return;
@@ -606011,8 +606393,8 @@ function deleteScheduledById(id) {
606011
606393
  if (id) candidates.push(id);
606012
606394
  if (typeof entry?.id === "string" && entry.id && !candidates.includes(entry.id)) candidates.push(entry.id);
606013
606395
  try {
606014
- const { createHash: createHash20 } = require3("node:crypto");
606015
- const fallback = createHash20("sha1").update(`${target.file}#${target.index}`).digest("hex").slice(0, 16);
606396
+ const { createHash: createHash21 } = require3("node:crypto");
606397
+ const fallback = createHash21("sha1").update(`${target.file}#${target.index}`).digest("hex").slice(0, 16);
606016
606398
  if (!candidates.includes(fallback)) candidates.push(fallback);
606017
606399
  } catch {
606018
606400
  }
@@ -608186,7 +608568,7 @@ function createSubAgentTool(config, repoRoot, ctxWindowSize) {
608186
608568
  success: true,
608187
608569
  output: `Sub-agent started in background: ${taskId}
608188
608570
  Task: ${task}
608189
- Use task_status("${taskId}") or task_output("${taskId}") to check progress.`
608571
+ Use task_status(task_id="${taskId}") or task_output(task_id="${taskId}") to check progress.`
608190
608572
  };
608191
608573
  }
608192
608574
  if (onViewStatus) onViewStatus(agentId, "running");
@@ -610102,13 +610484,10 @@ async function startInteractive(config, repoPath) {
610102
610484
  } catch {
610103
610485
  }
610104
610486
  }
610105
- if (!isResumed && isFirstRun() && config.backendType === "ollama") {
610106
- const needsSetup = !await isModelAvailable(config);
610107
- if (needsSetup) {
610108
- const setupModel = await runSetupWizard(config);
610109
- const freshConfig = loadConfig();
610110
- config = { ...config, ...freshConfig, model: setupModel ?? freshConfig.model };
610111
- }
610487
+ if (!isResumed && await shouldRunFirstRunSetup(config)) {
610488
+ const setupModel = await runSetupWizard(config);
610489
+ const freshConfig = loadConfig();
610490
+ config = { ...config, ...freshConfig, model: setupModel ?? freshConfig.model };
610112
610491
  }
610113
610492
  let carouselPhrases = null;
610114
610493
  try {
@@ -613679,13 +614058,13 @@ NEW TASK: ${fullInput}`;
613679
614058
  writeContent(() => renderError2(errMsg));
613680
614059
  if (failureStore) {
613681
614060
  try {
613682
- const { createHash: createHash20 } = await import("node:crypto");
614061
+ const { createHash: createHash21 } = await import("node:crypto");
613683
614062
  failureStore.insert({
613684
614063
  taskId: "",
613685
614064
  sessionId: `${Date.now()}`,
613686
614065
  repoRoot,
613687
614066
  failureType: "runtime-error",
613688
- fingerprint: createHash20("sha256").update(errMsg.slice(0, 200)).digest("hex").slice(0, 16),
614067
+ fingerprint: createHash21("sha256").update(errMsg.slice(0, 200)).digest("hex").slice(0, 16),
613689
614068
  filePath: null,
613690
614069
  errorMessage: errMsg.slice(0, 500),
613691
614070
  context: null,
@@ -613928,11 +614307,19 @@ ${c3.dim("(Use /quit to exit)")}
613928
614307
  showPrompt();
613929
614308
  };
613930
614309
  }
614310
+ async function shouldRunFirstRunSetup(config, firstRun = isFirstRun()) {
614311
+ if (config.backendType !== "ollama") return false;
614312
+ if (!firstRun) return false;
614313
+ try {
614314
+ return !await isModelAvailable(config);
614315
+ } catch {
614316
+ return false;
614317
+ }
614318
+ }
613931
614319
  async function runWithTUI(task, config, repoPath, callbacks) {
613932
614320
  const repoRoot = resolve39(repoPath ?? cwd());
613933
614321
  initOaDirectory(repoRoot);
613934
- const needsSetup = isFirstRun() || !await isModelAvailable(config);
613935
- if (needsSetup && config.backendType === "ollama") {
614322
+ if (await shouldRunFirstRunSetup(config)) {
613936
614323
  const setupModel = await runSetupWizard(config);
613937
614324
  const freshConfig = loadConfig();
613938
614325
  config = { ...config, ...freshConfig, model: setupModel ?? freshConfig.model };